
AWS Secrets Manager 완전 정복: 비밀번호를 코드에 넣지 않는 법
GitHub에 DB 비밀번호가 올라갔다. 그 순간 모든 것이 시작된다. 환경변수부터 AWS Parameter Store, Secrets Manager까지 — 비밀 관리의 단계별 진화를 코드와 함께 풀어본다. 자동 로테이션, Lambda 통합, 최소 권한 원칙까지.

GitHub에 DB 비밀번호가 올라갔다. 그 순간 모든 것이 시작된다. 환경변수부터 AWS Parameter Store, Secrets Manager까지 — 비밀 관리의 단계별 진화를 코드와 함께 풀어본다. 자동 로테이션, Lambda 통합, 최소 권한 원칙까지.

금요일 저녁 7시. 신입 개발자 J는 첫 프로젝트의 마감을 앞두고 있었다. Django 앱을 AWS에 배포해야 하는데, 로컬에서는 잘 돌아가던 코드가 서버에서 DB 연결이 안 됐다. 급한 마음에 settings.py에 이렇게 적었다:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'prod-db.cluster-abc123.ap-northeast-2.rds.amazonaws.com',
'NAME': 'myapp_production',
'USER': 'admin',
'PASSWORD': 'SuperSecret!2026', # TODO: 나중에 환경변수로 바꾸기
}
}
배포가 급했다. "월요일에 고치지 뭐." J는 git add .을 하고 커밋했다. 그리고 푸시했다.
그 "월요일"은 오지 않았다. 토요일 새벽 3시, 알 수 없는 IP에서 RDS 인스턴스에 대량 접속이 시작됐다. 일요일 오전, 프로덕션 데이터베이스의 모든 테이블이 삭제되고, 이런 메시지가 남아 있었다:
"Your database has been backed up. Send 0.5 BTC to restore."
GitHub 레포지토리가 퍼블릭이었다. 봇이 GitHub의 새 커밋을 실시간으로 스캔하여 AWS 키, DB 비밀번호, API 토큰을 탈취하는 데는 30초도 걸리지 않는다.
이 이야기는 픽션이 아니다. 매일 일어나는 현실이다.
답은 간단하다 — 편하니까. 로컬에서 config.py에 DB 비밀번호를 넣으면 바로 동작한다. 환경변수를 설정하거나 외부 서비스를 연동하는 것은 "나중에 할 일"이 된다.
git commit && git push — Git 히스토리에 영구 기록핵심 문제 3가지:
git rm으로 파일을 삭제해도 git log에 기록이 남는다..env 파일의 함정".env 파일에 넣으면 되지 않나?" — 하드코딩보다는 낫다. 하지만 .gitignore에 추가하지 않으면 역시 Git에 올라가고, 팀원 간에 슬랙이나 Confluence로 공유하는 경우가 많다. .env는 로컬 개발에서만 사용하고, 프로덕션에서는 전용 서비스를 써야 한다.
2011년, Heroku의 Twelve-Factor App 방법론은 "설정을 환경변수에 저장하라"고 권장했다. 코드에서 시크릿을 분리하는 첫 번째 단계다.
import os
db_password = os.environ['DB_PASSWORD']
이것만으로도 큰 발전이다. 하지만 환경변수만으로는 부족하다:
환경변수의 한계:
printenv로 확인 가능AWS Systems Manager Parameter Store는 설정 데이터와 시크릿을 계층적으로 저장하는 서비스다. 기본 사용은 무료이며, AWS에서 가장 먼저 접하게 되는 시크릿 관리 도구다.
| 타입 | 암호화 | 용도 |
|---|---|---|
| String | 없음 | 설정값, URL, 기능 플래그 |
| StringList | 없음 | 서버 목록, 허용 IP |
| SecureString | AWS KMS | 비밀번호, API 키, 토큰 |
# SecureString으로 DB 비밀번호 저장
aws ssm put-parameter \
--name "/myapp/prod/db/password" \
--value "SuperSecret!2026" \
--type SecureString
import boto3
ssm = boto3.client('ssm', region_name='ap-northeast-2')
response = ssm.get_parameter(
Name='/myapp/prod/db/password',
WithDecryption=True
)
db_password = response['Parameter']['Value']

AWS Secrets Manager는 시크릿의 전체 생명주기를 관리하는 전용 서비스다. Parameter Store가 "설정 관리 도구 + 시크릿 저장"이라면, Secrets Manager는 시크릿만을 위해 설계된 전용 서비스다.
Secrets Manager의 시크릿은 단순한 키-값이 아니다. JSON 구조를 저장할 수 있어서, DB 연결에 필요한 모든 정보를 하나로 묶는다:
| Parameter Store | Secrets Manager | |||
|---|---|---|---|---|
| 주 용도 | 설정 관리 + 간단한 시크릿 | 시크릿 전용 관리 | ||
| 자동 로테이션 | 없음 (수동 구현 필요) | 내장. Lambda 기반 자동 교체 | ||
| 암호화 | SecureString만 KMS 암호화 | 모든 시크릿 기본 KMS 암호화 | ||
| 교차 계정 공유 | Advanced만 가능 | 리소스 기반 정책으로 쉽게 공유 | ||
| 교차 리전 복제 | 없음 | 다중 리전 자동 복제 | ||
| 비용 | Standard: 무료 | $0.40/시크릿/월 + API 호출 비용 | ||
| API 처리량 | Standard: 40 TPS | 기본 5,000 TPS | ||
| RDS 네이티브 통합 | 없음 | RDS, Redshift, DocumentDB 로테이션 |
실무 팁: 많은 팀이 두 서비스를 함께 사용한다. 설정값은 Parameter Store에, 민감한 자격 증명은 Secrets Manager에 저장하는 것이 가장 흔한 패턴이다.
aws secretsmanager create-secret \
--name "prod/myapp/db-credentials" \
--secret-string '{
"username": "admin",
"password": "aB3$kL9mN2pQ7wX5",
"engine": "postgres",
"host": "prod-db.cluster-abc123.rds.amazonaws.com",
"port": 5432,
"dbname": "myapp_production"
}'
import json
import boto3
from botocore.exceptions import ClientError
def get_secret(secret_name: str, region: str = 'ap-northeast-2') -> dict:
"""AWS Secrets Manager에서 시크릿을 가져온다."""
client = boto3.client('secretsmanager', region_name=region)
try:
response = client.get_secret_value(SecretId=secret_name)
except ClientError as e:
code = e.response['Error']['Code']
if code == 'ResourceNotFoundException':
raise ValueError(f"시크릿 '{secret_name}'을 찾을 수 없습니다.")
elif code == 'AccessDeniedException':
raise PermissionError("접근 권한이 없습니다.")
raise
return json.loads(response['SecretString'])
# 사용 예시
db = get_secret('prod/myapp/db-credentials')
from sqlalchemy import create_engine
engine = create_engine(
f"postgresql://{db['username']}:{db['password']}"
f"@{db['host']}:{db['port']}/{db['dbname']}"
)
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'ap-northeast-2' });
async function getSecret(secretName) {
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
}
// 사용 예시
const db = await getSecret('prod/myapp/db-credentials');
import pg from 'pg';
const pool = new pg.Pool({
host: db.host,
port: db.port,
database: db.dbname,
user: db.username,
password: db.password,
});
매번 API를 호출하면 지연과 비용이 발생한다. AWS 공식 캐싱 라이브러리를 사용하자:
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig
cache = SecretCache(config=SecretCacheConfig(
secret_refresh_interval=300 # 5분 캐싱
))
secret_string = cache.get_secret_string('prod/myapp/db-credentials')
db = json.loads(secret_string)

비밀번호가 유출되었더라도, 이미 만료된 상태라면 피해를 막을 수 있다. 하지만 수동으로 매월 모든 DB 비밀번호를 바꾸는 것은 현실적으로 불가능하다. Secrets Manager는 이를 Lambda 함수 하나로 자동화한다.
AWSPENDING 레이블로 저장. 현재 비밀번호는 그대로 유효.
AWSPENDING → AWSCURRENT 승격. 이전 값은 AWSPREVIOUS로 이동.
핵심은 무중단(zero-downtime)이다. 새 비밀번호가 테스트를 통과한 후에야 공식 값으로 승격된다.
aws secretsmanager rotate-secret \
--secret-id "prod/myapp/db-credentials" \
--rotation-lambda-arn "arn:aws:lambda:ap-northeast-2:123456789:function:SecretsManagerRDSRotation" \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
from aws_cdk import aws_secretsmanager as sm, Duration
db_secret = sm.Secret(self, "DBSecret",
secret_name="prod/myapp/db-credentials",
generate_secret_string=sm.SecretStringGenerator(
secret_string_template='{"username": "admin"}',
generate_string_key="password",
password_length=32,
),
)
db_secret.add_rotation_schedule("Rotation",
automatically_after=Duration.days(30),
hosted_rotation=sm.HostedRotation.postgre_sql_single_user(),
)
| 싱글 유저 | 멀티 유저 (교대) | |||
|---|---|---|---|---|
| 동작 | 하나의 DB 사용자 비밀번호를 변경 | 두 DB 사용자를 번갈아가며 사용 | ||
| 순단 가능성 | 비밀번호 변경 순간 짧은 단절 가능 | 무중단 — 항상 하나는 유효 | ||
| 적합한 환경 | 개발, 소규모 서비스 | 프로덕션, 고가용성 필수 |
Lambda Extensions을 사용하면 코드 변경 없이 시크릿을 로컬 HTTP 캐시로 가져올 수 있다:
# Lambda 함수에 Extension 레이어 추가
aws lambda update-function-configuration \
--function-name my-function \
--layers "arn:aws:lambda:ap-northeast-2:044395824272:layer:AWS-Parameters-and-Secrets-Lambda-Extension:12"
Extension 없이 직접 호출도 가능하다:
import json, boto3
secrets = boto3.client('secretsmanager')
def lambda_handler(event, context):
response = secrets.get_secret_value(SecretId='prod/myapp/db-credentials')
db = json.loads(response['SecretString'])
# DB 작업 수행 ...
ECS는 Task Definition에서 시크릿을 직접 참조한다. 컨테이너 시작 시 환경변수로 자동 주입되므로, 시크릿 관리 코드를 아예 작성하지 않아도 된다:
{
"containerDefinitions": [{
"name": "myapp",
"image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/db-credentials:password::"
}
]
}]
}
앱에서는 os.environ['DB_PASSWORD']로 읽으면 된다.
EC2는 인스턴스 프로파일(IAM Role)을 통해 접근한다. AWS Access Key를 직접 설정하지 않고, SDK가 인스턴스 메타데이터에서 임시 자격 증명을 자동으로 획득한다.
| Lambda | ECS | EC2 | ||||
|---|---|---|---|---|---|---|
| 시크릿 주입 | 코드 호출 or Extension | Task Def 참조 (환경변수) | 코드에서 API 호출 | |||
| 캐싱 | Extension 자동 캐싱 | 시작 시 1회 주입 | 앱에서 직접 구현 | |||
| 코드 변경 | 최소~없음 | 없음 | 필요 |
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/*"
}
secretsmanager:* 와일드카드 금지. 읽기만 필요한 서비스에 삭제 권한까지 주면 사고 범위가 커진다.
모든 API 호출이 CloudTrail에 기록된다. 비정상적인 GetSecretValue 폭증, 예상치 못한 DeleteSecret, RotationFailed 이벤트에 CloudWatch Alarm을 설정한다.
/prod/myapp/db-credentials
/staging/myapp/db-credentials
/dev/myapp/db-credentials
IAM 정책에서 Resource: "arn:...:prod/*"로 환경별 접근 제어가 가능해진다.
printenv 덤프, Slack/이메일로 공유"password": "****"기본적으로 Secrets Manager API는 인터넷을 경유한다. VPC 엔드포인트를 생성하면 AWS 내부 네트워크만으로 통신하여 보안이 강화된다.
중앙 보안 계정에서 시크릿을 관리하고, 애플리케이션 계정에서 접근하는 것이 엔터프라이즈 패턴이다.
시크릿 삭제 시 기본 30일 복구 기간이 있다. 최소 7일 유지를 권장한다.
| 태그 키 | 값 예시 | 용도 |
|---|---|---|
Environment | prod, staging, dev | 환경 구분 |
Application | myapp | 소속 서비스 |
Owner | platform-team | 관리 주체 |
태그 기반 IAM(ABAC)으로 "Environment=prod인 시크릿은 SRE팀만 접근" 같은 규칙을 만들 수 있다.
시크릿이 코드에 들어가는 것 자체를 막자:
brew install git-secrets # macOS
git secrets --register-aws
git secrets --install
# 이후 커밋 시 AWS 키 포함되면 자동 차단
**4.88M — IBM 2024)를 예방할 수 있다면? 이건 보험이다.
이 아키텍처에서 코드에는 단 하나의 시크릿도 존재하지 않는다. GitHub에 레포를 퍼블릭으로 공개해도 안전하다.
이 글의 첫 부분에서 소개한 신입 개발자 J의 이야기로 돌아가 보자. settings.py에 남긴 # TODO: 나중에 환경변수로 바꾸기 주석 — 그 "나중에"가 오기 전에 사고가 터졌다.
보안에서 "나중에"는 없다. 하지만 다행히도, 지금 시작하는 것은 어렵지 않다.
.env가 .gitignore에 있는지 확인.
비밀번호를 코드에 넣는 것은 집 열쇠를 우편함 위에 올려놓는 것과 같다. 대부분의 날은 괜찮을 수도 있다. 하지만 한 번의 사고가 모든 것을 바꾼다.
AWS Secrets Manager는 그 열쇠를 금고에 넣어주는 서비스다. 금고는 잠겨 있고, 열쇠는 주기적으로 바뀌며, 누가 금고를 열었는지 기록이 남는다. 월 $25도 안 되는 비용으로.
코드에서 시크릿을 지우자. 오늘.
참고 자료