Chapter 11 - 강사용 답안

Secrets Manager 비밀 관리

"DB 비밀번호를 .env 파일이 아닌 안전한 금고에 보관하자"

강사 전용 문서

이 문서는 강사용 답안입니다. 학생에게 공유하지 마세요. 각 단계의 정확한 답과 해설, 트러블슈팅 가이드가 포함되어 있습니다.

Step 1: Secrets Manager에 DB 비밀번호 저장

답안

웹 콘솔 방법

  1. AWS 콘솔에서 Secrets Manager 서비스로 이동
  2. 새 보안 암호 저장 클릭
  3. 보안 암호 유형: 다른 유형의 보안 암호 선택
  4. 키/값 쌍에 다음 5개 항목을 입력:
    host shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com
    port 3306
    username admin
    password ShopEasy2024!
    dbname ecommerce
  5. 암호화 키: aws/secretsmanager (기본 AWS 관리형 키) 유지
  6. 다음 클릭
  7. 보안 암호 이름: ShopEasy/DB 입력
  8. 설명: ShopEasy RDS MySQL credentials 입력 (선택)
  9. 다음 클릭
  10. 교체 구성: 자동 교체 비활성화 유지 (이번 실습에서는 설정하지 않음)
  11. 다음 클릭
  12. 검토 화면에서 내용 확인 후 저장 클릭

AWS CLI 방법

bash
# Secrets Manager에 비밀 생성
aws secretsmanager create-secret \
  --name ShopEasy/DB \
  --description "ShopEasy RDS MySQL credentials" \
  --secret-string '{"host":"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com","port":"3306","username":"admin","password":"ShopEasy2024!","dbname":"ecommerce"}' \
  --region ap-northeast-2

# 생성 확인
aws secretsmanager describe-secret \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2

# 비밀 값 조회 (생성 확인용)
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query SecretString --output text | python3 -m json.tool
해설: 비밀 유형 선택

Secrets Manager에서 비밀을 만들 때 두 가지 유형을 선택할 수 있습니다:

  • Amazon RDS 데이터베이스에 대한 자격 증명: RDS를 직접 선택하면 자동 교체가 더 쉽게 설정됩니다
  • 다른 유형의 보안 암호: 자유로운 키/값 쌍을 저장. 이번 실습에서는 이 방식 사용

"다른 유형의 보안 암호"를 선택한 이유는 비밀의 구조를 직접 확인하고 이해하기 위해서입니다. 프로덕션에서는 RDS 유형을 선택하면 자동 교체 설정이 더 간편합니다.

해설: 암호화 키 선택

aws/secretsmanager는 AWS가 관리하는 기본 KMS 키입니다. 추가 비용이 없으며 대부분의 경우 충분합니다. Chapter 10에서 만든 커스텀 KMS 키를 사용하면 키 정책으로 더 세밀한 접근 제어가 가능하지만, 이번 실습에서는 기본 키를 사용합니다.

Step 2: IAM 역할에 Secrets Manager 권한 추가

답안

웹 콘솔 방법

  1. AWS 콘솔에서 IAM 서비스로 이동
  2. 좌측 메뉴 → 역할ShopEasy-EC2-Role 클릭
  3. 권한 추가인라인 정책 생성
  4. JSON 탭을 클릭하고 아래 정책을 붙여넣기:
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SecretsManagerAccess",
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:ap-northeast-2:*:secret:ShopEasy/DB*"
    }
  ]
}
  1. 다음 클릭
  2. 정책 이름: ShopEasy-SecretsManager-Access 입력
  3. 정책 생성 클릭

AWS CLI 방법

bash
# 인라인 정책 JSON 파일 생성
cat > /tmp/secrets-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SecretsManagerAccess",
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:ap-northeast-2:*:secret:ShopEasy/DB*"
    }
  ]
}
EOF

# IAM 역할에 인라인 정책 추가
aws iam put-role-policy \
  --role-name ShopEasy-EC2-Role \
  --policy-name ShopEasy-SecretsManager-Access \
  --policy-document file:///tmp/secrets-policy.json

# 정책 확인
aws iam get-role-policy \
  --role-name ShopEasy-EC2-Role \
  --policy-name ShopEasy-SecretsManager-Access
해설: IAM 정책 분석
항목 의미
Action secretsmanager:GetSecretValue 비밀 값 조회만 허용. 생성/삭제/수정은 불가
Resource ...secret:ShopEasy/DB* ShopEasy/DB 비밀에만 접근 가능. 다른 비밀은 접근 불가
해설: Resource ARN 끝의 * (와일드카드)

arn:aws:secretsmanager:ap-northeast-2:*:secret:ShopEasy/DB*에서 끝의 *는 반드시 필요합니다. Secrets Manager는 비밀을 생성할 때 이름 뒤에 6자리 랜덤 접미사를 자동으로 추가합니다.

예: 비밀 이름이 ShopEasy/DB이면 실제 ARN은 arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:ShopEasy/DB-aB3cDe처럼 됩니다. * 없이 정확한 이름만 지정하면 권한이 거부됩니다.

Step 3: EC2에서 비밀 값 조회 테스트

답안

기본 비밀 값 조회

bash
# EC2 접속
ssh -i your-key.pem ec2-user@{EC2_PUBLIC_IP}

# 비밀 값 조회 (전체 응답)
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2

정상 응답 예시:

json
{
  "ARN": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:ShopEasy/DB-aB3cDe",
  "Name": "ShopEasy/DB",
  "VersionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "SecretString": "{\"host\":\"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com\",\"port\":\"3306\",\"username\":\"admin\",\"password\":\"ShopEasy2024!\",\"dbname\":\"ecommerce\"}",
  "VersionStages": ["AWSCURRENT"],
  "CreatedDate": "2024-12-01T10:00:00+09:00"
}

비밀 값만 깔끔하게 조회

bash
# SecretString만 추출하여 보기 좋게 출력
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query SecretString --output text | python3 -m json.tool

정상 응답 예시:

json
{
    "host": "shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com",
    "port": "3306",
    "username": "admin",
    "password": "ShopEasy2024!",
    "dbname": "ecommerce"
}
bash
# 특정 필드만 추출 (예: password만)
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query SecretString --output text | python3 -c "
import sys, json
secret = json.load(sys.stdin)
print('Host:', secret['host'])
print('Port:', secret['port'])
print('User:', secret['username'])
print('Pass:', secret['password'])
print('DB:  ', secret['dbname'])
"
해설: 응답 필드 설명
필드 설명
ARN 비밀의 고유 식별자. IAM 정책의 Resource에 사용됩니다
Name 비밀 이름 (ShopEasy/DB)
VersionId 현재 버전의 UUID. 비밀이 교체되면 새 버전이 생성됩니다
SecretString 실제 비밀 값 (JSON 문자열). 이 값을 파싱하여 DB 접속에 사용합니다
VersionStages AWSCURRENT는 현재 활성 버전을 의미. 교체 시 AWSPREVIOUS 버전도 유지됩니다
해설: 앱에서 Secrets Manager 사용하는 방법

실제 앱 코드에서는 AWS SDK를 사용하여 Secrets Manager에서 비밀을 조회합니다. 다음은 Node.js 예시입니다 (참고용, 이번 실습에서는 구현하지 않음):

javascript
// Node.js에서 Secrets Manager 사용 예시 (참고용)
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

const client = new SecretsManagerClient({ region: 'ap-northeast-2' });

async function getDbCredentials() {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: 'ShopEasy/DB' })
  );
  return JSON.parse(response.SecretString);
}

// 사용 예시
const creds = await getDbCredentials();
// creds.host, creds.port, creds.username, creds.password, creds.dbname

Step 4: 비밀번호 교체(로테이션) 개념 이해

답안

이 단계는 개념 이해가 목표이며, 실제 구현은 하지 않습니다. 학생들에게 다음 내용을 설명합니다.

자동 교체의 4단계 프로세스

Secrets Manager의 자동 교체는 Lambda 함수가 4단계를 순서대로 실행합니다:

단계 이름 동작
1 createSecret 새로운 랜덤 비밀번호를 생성하고, Secrets Manager에 AWSPENDING 버전으로 저장
2 setSecret RDS에 접속하여 DB 사용자의 비밀번호를 새 값으로 변경
3 testSecret 새 비밀번호로 RDS 접속 테스트. 실패하면 교체 중단
4 finishSecret AWSPENDINGAWSCURRENT로 승격, 이전 버전은 AWSPREVIOUS로 변경
해설: 교체 중 서비스 중단이 없는 이유

교체 과정에서 기존 비밀번호(AWSCURRENT)는 새 비밀번호 테스트가 성공할 때까지 유효합니다. 앱이 AWSCURRENT 버전을 요청하면 항상 동작하는 비밀번호를 받습니다.

단, 앱은 비밀번호를 캐싱하되, DB 접속 실패 시 Secrets Manager에서 최신 비밀번호를 다시 조회하는 로직이 필요합니다.

교체를 구현하려면 필요한 것들 (참고)

  • Lambda 함수: Secrets Manager가 제공하는 교체 템플릿 사용 가능
  • VPC 구성: Lambda가 RDS(프라이빗 서브넷)에 접근하려면 같은 VPC에 배포 필요
  • Lambda 실행 역할: Secrets Manager, RDS 네트워크 접근 권한 필요
  • 교체 일정: 30일, 60일, 90일 등 주기 설정
실무 Best Practice
  • 프로덕션에서는 반드시 자동 교체를 활성화하세요. 수동 비밀번호 관리는 보안 사고의 원인입니다
  • 교체 주기는 30~90일이 일반적입니다. PCI-DSS는 90일 이내 교체를 요구합니다
  • 앱에서는 비밀번호를 캐싱하되, DB 접속 실패 시 재조회하는 로직을 구현하세요
  • AWS SDK에서 제공하는 Secrets Manager Caching Client 라이브러리를 사용하면 캐싱이 자동 처리됩니다

트러블슈팅 가이드

자주 발생하는 문제와 해결 방법

AccessDeniedException: User is not authorized to perform secretsmanager:GetSecretValue

원인: EC2의 IAM Role에 Secrets Manager 접근 권한이 없거나, Resource ARN이 잘못됨

해결 방법:

  1. IAM 콘솔에서 ShopEasy-EC2-RoleShopEasy-SecretsManager-Access 정책이 있는지 확인
  2. 정책의 Resource ARN 끝에 *(와일드카드)가 포함되어 있는지 확인
  3. EC2 인스턴스에 IAM Role이 연결되어 있는지 확인
bash
# EC2에서 현재 IAM Role 확인
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/

# IAM Role에 연결된 정책 확인
aws iam list-role-policies --role-name ShopEasy-EC2-Role
aws iam get-role-policy \
  --role-name ShopEasy-EC2-Role \
  --policy-name ShopEasy-SecretsManager-Access

# 비밀 ARN 확인 (콘솔 또는 CLI)
aws secretsmanager describe-secret \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query ARN --output text
ResourceNotFoundException: Secrets Manager can't find the specified secret

원인: 비밀 이름이 잘못되었거나, 다른 리전에 생성됨

해결 방법:

  1. 비밀 이름이 정확히 ShopEasy/DB인지 확인 (대소문자 구분)
  2. --region ap-northeast-2가 포함되어 있는지 확인
  3. Secrets Manager 콘솔에서 비밀이 존재하는지 확인
bash
# 서울 리전의 모든 비밀 목록 확인
aws secretsmanager list-secrets \
  --region ap-northeast-2 \
  --query "SecretList[].Name" --output table

# 비밀이 없으면 다시 생성
aws secretsmanager create-secret \
  --name ShopEasy/DB \
  --description "ShopEasy RDS MySQL credentials" \
  --secret-string '{"host":"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com","port":"3306","username":"admin","password":"ShopEasy2024!","dbname":"ecommerce"}' \
  --region ap-northeast-2
InvalidRequestException: You can't create this secret because a secret with this name already exists

원인: 동일한 이름의 비밀이 이미 존재하거나, 삭제 대기 중인 비밀이 있음

해결 방법:

  1. 기존 비밀이 있다면 값을 업데이트하거나, 삭제 후 재생성
  2. 삭제 대기 중인 비밀은 복원하거나, 삭제를 강제 완료
bash
# 방법 1: 기존 비밀 값 업데이트
aws secretsmanager update-secret \
  --secret-id ShopEasy/DB \
  --secret-string '{"host":"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com","port":"3306","username":"admin","password":"ShopEasy2024!","dbname":"ecommerce"}' \
  --region ap-northeast-2

# 방법 2: 삭제 대기 중인 비밀 복원
aws secretsmanager restore-secret \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2

# 방법 3: 강제 삭제 후 재생성 (즉시 삭제, 복구 불가)
aws secretsmanager delete-secret \
  --secret-id ShopEasy/DB \
  --force-delete-without-recovery \
  --region ap-northeast-2

# 잠시 대기 후 재생성
sleep 5
aws secretsmanager create-secret \
  --name ShopEasy/DB \
  --description "ShopEasy RDS MySQL credentials" \
  --secret-string '{"host":"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com","port":"3306","username":"admin","password":"ShopEasy2024!","dbname":"ecommerce"}' \
  --region ap-northeast-2
EC2에서 aws 명령어 실행 시 "Unable to locate credentials"

원인: EC2 인스턴스에 IAM Role이 연결되지 않음

해결 방법:

  1. EC2 콘솔 → 인스턴스 선택 → 작업 → 보안 → IAM 역할 수정
  2. ShopEasy-EC2-Role을 선택하고 저장
  3. IAM Role 변경 후 바로 적용됩니다 (EC2 재시작 불필요)
bash
# EC2에서 IAM Role 확인 (응답이 비어있으면 Role 미연결)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/

# CLI로 IAM Role 연결 (인스턴스 ID 필요)
aws ec2 associate-iam-instance-profile \
  --instance-id i-xxxxxxxxxxxxxxxxx \
  --iam-instance-profile Name=ShopEasy-EC2-Role
비밀 값의 JSON 형식이 잘못됨 (파싱 에러)

원인: 비밀 생성 시 JSON 문법 오류 (따옴표 누락, 쉼표 위치 등)

해결 방법:

  1. 비밀 값을 조회하여 JSON 형식을 확인
  2. JSON 문법이 잘못된 경우 업데이트
bash
# 현재 비밀 값 확인
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query SecretString --output text

# JSON 유효성 검사
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2 \
  --query SecretString --output text | python3 -m json.tool

# 올바른 JSON으로 업데이트
aws secretsmanager update-secret \
  --secret-id ShopEasy/DB \
  --secret-string '{"host":"shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com","port":"3306","username":"admin","password":"ShopEasy2024!","dbname":"ecommerce"}' \
  --region ap-northeast-2
정책을 추가했는데 여전히 AccessDenied가 발생

원인: IAM 정책 전파 지연 (최대 수 초~수십 초), 또는 정책이 다른 역할에 추가됨

해결 방법:

  1. 정책 추가 후 30초~1분 대기 후 재시도
  2. EC2에 연결된 역할 이름이 ShopEasy-EC2-Role이 맞는지 확인
  3. 인라인 정책이 아닌 관리형 정책으로 추가한 경우 확인 방법이 다름
bash
# EC2에 연결된 역할 이름 확인
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 출력된 역할 이름이 ShopEasy-EC2-Role인지 확인

# 해당 역할의 인라인 정책 목록 확인
aws iam list-role-policies --role-name ShopEasy-EC2-Role

# 해당 역할의 관리형 정책 목록 확인
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role

# 30초 대기 후 재시도
sleep 30
aws secretsmanager get-secret-value \
  --secret-id ShopEasy/DB \
  --region ap-northeast-2
강사 참고: 학생들이 자주 실수하는 포인트
  1. 비밀 이름 오타: ShopEasy/DB에서 대소문자, 슬래시 위치를 정확히 입력해야 합니다
  2. Resource ARN에 와일드카드 누락: ShopEasy/DB가 아닌 ShopEasy/DB*로 끝나야 합니다 (랜덤 접미사 때문)
  3. 리전 누락: --region ap-northeast-2를 빼먹으면 기본 리전(us-east-1)에서 찾으려 합니다
  4. JSON 키/값 입력 실수: 콘솔에서 키/값을 입력할 때 "행 추가"를 눌러 5개 모두 입력해야 합니다. host만 넣고 나머지를 빼먹는 경우가 있습니다
  5. IAM Role과 인스턴스 프로파일 혼동: EC2에는 인스턴스 프로파일을 통해 역할이 연결됩니다. 역할을 만들었지만 EC2에 연결하지 않은 경우가 있습니다
  6. 비밀 유형 잘못 선택: "RDS 데이터베이스에 대한 자격 증명"이 아닌 "다른 유형의 보안 암호"를 선택해야 합니다 (이번 실습에서는)