Secrets Manager 비밀 관리
"DB 비밀번호를 .env 파일이 아닌 안전한 금고에 보관하자"
이 문서는 강사용 답안입니다. 학생에게 공유하지 마세요. 각 단계의 정확한 답과 해설, 트러블슈팅 가이드가 포함되어 있습니다.
Step 1: Secrets Manager에 DB 비밀번호 저장
답안
웹 콘솔 방법
- AWS 콘솔에서 Secrets Manager 서비스로 이동
- 새 보안 암호 저장 클릭
- 보안 암호 유형: 다른 유형의 보안 암호 선택
- 키/값 쌍에 다음 5개 항목을 입력:
키 값 hostshopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.comport3306usernameadminpasswordShopEasy2024!dbnameecommerce - 암호화 키: aws/secretsmanager (기본 AWS 관리형 키) 유지
- 다음 클릭
- 보안 암호 이름:
ShopEasy/DB입력 - 설명:
ShopEasy RDS MySQL credentials입력 (선택) - 다음 클릭
- 교체 구성: 자동 교체 비활성화 유지 (이번 실습에서는 설정하지 않음)
- 다음 클릭
- 검토 화면에서 내용 확인 후 저장 클릭
AWS CLI 방법
# 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 권한 추가
답안
웹 콘솔 방법
- AWS 콘솔에서 IAM 서비스로 이동
- 좌측 메뉴 → 역할 →
ShopEasy-EC2-Role클릭 - 권한 추가 → 인라인 정책 생성
- JSON 탭을 클릭하고 아래 정책을 붙여넣기:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SecretsManagerAccess",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-northeast-2:*:secret:ShopEasy/DB*"
}
]
}
- 다음 클릭
- 정책 이름:
ShopEasy-SecretsManager-Access입력 - 정책 생성 클릭
AWS CLI 방법
# 인라인 정책 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
| 항목 | 값 | 의미 |
|---|---|---|
Action |
secretsmanager:GetSecretValue |
비밀 값 조회만 허용. 생성/삭제/수정은 불가 |
Resource |
...secret:ShopEasy/DB* |
ShopEasy/DB 비밀에만 접근 가능. 다른 비밀은 접근 불가 |
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에서 비밀 값 조회 테스트
답안
기본 비밀 값 조회
# EC2 접속
ssh -i your-key.pem ec2-user@{EC2_PUBLIC_IP}
# 비밀 값 조회 (전체 응답)
aws secretsmanager get-secret-value \
--secret-id ShopEasy/DB \
--region ap-northeast-2
정상 응답 예시:
{
"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"
}
비밀 값만 깔끔하게 조회
# SecretString만 추출하여 보기 좋게 출력
aws secretsmanager get-secret-value \
--secret-id ShopEasy/DB \
--region ap-northeast-2 \
--query SecretString --output text | python3 -m json.tool
정상 응답 예시:
{
"host": "shopeasy-db.xxxx.ap-northeast-2.rds.amazonaws.com",
"port": "3306",
"username": "admin",
"password": "ShopEasy2024!",
"dbname": "ecommerce"
}
# 특정 필드만 추출 (예: 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 버전도 유지됩니다 |
실제 앱 코드에서는 AWS SDK를 사용하여 Secrets Manager에서 비밀을 조회합니다. 다음은 Node.js 예시입니다 (참고용, 이번 실습에서는 구현하지 않음):
// 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 | AWSPENDING을 AWSCURRENT로 승격, 이전 버전은 AWSPREVIOUS로 변경 |
교체 과정에서 기존 비밀번호(AWSCURRENT)는 새 비밀번호 테스트가 성공할 때까지 유효합니다.
앱이 AWSCURRENT 버전을 요청하면 항상 동작하는 비밀번호를 받습니다.
단, 앱은 비밀번호를 캐싱하되, DB 접속 실패 시 Secrets Manager에서 최신 비밀번호를 다시 조회하는 로직이 필요합니다.
교체를 구현하려면 필요한 것들 (참고)
- Lambda 함수: Secrets Manager가 제공하는 교체 템플릿 사용 가능
- VPC 구성: Lambda가 RDS(프라이빗 서브넷)에 접근하려면 같은 VPC에 배포 필요
- Lambda 실행 역할: Secrets Manager, RDS 네트워크 접근 권한 필요
- 교체 일정: 30일, 60일, 90일 등 주기 설정
- 프로덕션에서는 반드시 자동 교체를 활성화하세요. 수동 비밀번호 관리는 보안 사고의 원인입니다
- 교체 주기는 30~90일이 일반적입니다. PCI-DSS는 90일 이내 교체를 요구합니다
- 앱에서는 비밀번호를 캐싱하되, DB 접속 실패 시 재조회하는 로직을 구현하세요
- AWS SDK에서 제공하는 Secrets Manager Caching Client 라이브러리를 사용하면 캐싱이 자동 처리됩니다
트러블슈팅 가이드
자주 발생하는 문제와 해결 방법
원인: EC2의 IAM Role에 Secrets Manager 접근 권한이 없거나, Resource ARN이 잘못됨
해결 방법:
- IAM 콘솔에서
ShopEasy-EC2-Role에ShopEasy-SecretsManager-Access정책이 있는지 확인 - 정책의 Resource ARN 끝에
*(와일드카드)가 포함되어 있는지 확인 - EC2 인스턴스에 IAM Role이 연결되어 있는지 확인
# 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
원인: 비밀 이름이 잘못되었거나, 다른 리전에 생성됨
해결 방법:
- 비밀 이름이 정확히
ShopEasy/DB인지 확인 (대소문자 구분) --region ap-northeast-2가 포함되어 있는지 확인- Secrets Manager 콘솔에서 비밀이 존재하는지 확인
# 서울 리전의 모든 비밀 목록 확인
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
원인: 동일한 이름의 비밀이 이미 존재하거나, 삭제 대기 중인 비밀이 있음
해결 방법:
- 기존 비밀이 있다면 값을 업데이트하거나, 삭제 후 재생성
- 삭제 대기 중인 비밀은 복원하거나, 삭제를 강제 완료
# 방법 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 인스턴스에 IAM Role이 연결되지 않음
해결 방법:
- EC2 콘솔 → 인스턴스 선택 → 작업 → 보안 → IAM 역할 수정
ShopEasy-EC2-Role을 선택하고 저장- IAM Role 변경 후 바로 적용됩니다 (EC2 재시작 불필요)
# 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 형식을 확인
- JSON 문법이 잘못된 경우 업데이트
# 현재 비밀 값 확인
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
원인: IAM 정책 전파 지연 (최대 수 초~수십 초), 또는 정책이 다른 역할에 추가됨
해결 방법:
- 정책 추가 후 30초~1분 대기 후 재시도
- EC2에 연결된 역할 이름이
ShopEasy-EC2-Role이 맞는지 확인 - 인라인 정책이 아닌 관리형 정책으로 추가한 경우 확인 방법이 다름
# 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
- 비밀 이름 오타:
ShopEasy/DB에서 대소문자, 슬래시 위치를 정확히 입력해야 합니다 - Resource ARN에 와일드카드 누락:
ShopEasy/DB가 아닌ShopEasy/DB*로 끝나야 합니다 (랜덤 접미사 때문) - 리전 누락:
--region ap-northeast-2를 빼먹으면 기본 리전(us-east-1)에서 찾으려 합니다 - JSON 키/값 입력 실수: 콘솔에서 키/값을 입력할 때 "행 추가"를 눌러 5개 모두 입력해야 합니다. host만 넣고 나머지를 빼먹는 경우가 있습니다
- IAM Role과 인스턴스 프로파일 혼동: EC2에는 인스턴스 프로파일을 통해 역할이 연결됩니다. 역할을 만들었지만 EC2에 연결하지 않은 경우가 있습니다
- 비밀 유형 잘못 선택: "RDS 데이터베이스에 대한 자격 증명"이 아닌 "다른 유형의 보안 암호"를 선택해야 합니다 (이번 실습에서는)