AWS 고급 #6 Secrets Manager / Parameter Store

8 분 소요

DB 비밀번호, 외부 API 키, OAuth client secret. 어디에 둘까. 코드 / git은 절대 안 되고, 환경 변수에 평문도 위험합니다. AWS에는 두 가지 선택지가 있습니다. Secrets ManagerSSM Parameter Store입니다.

이 글은 둘의 차이, 자동 회전, 코드에서 가져오는 패턴, ECS / Lambda와의 통합, IaC 연결, 비용 비교까지, 시크릿 / 설정 관리의 기준을 한 번에 정리하겠습니다.

시크릿이 들어가면 안 되는 곳 #

기초 #6 보안 기본의 연장. 절대 하면 안 되는 것:

  • 코드 안 평문 (PASSWORD = "abc123")
  • git에 .env (지운 뒤에도 history에 영원)
  • README, 슬랙, 위키, 이메일에 붙여넣기
  • Dockerfile 안 환경변수 / 빌드 인자 (이미지 layer에 박힘)
  • ECS Task Definition의 평문 environment variables (콘솔 / IaC / 로그 노출)

대신 쓰는 곳: Secrets Manager 또는 SSM Parameter Store.

둘의 차이: 한 줄 비교 #

Secrets ManagerSSM Parameter Store
정체시크릿 전용 매니지드설정 + 시크릿 겸용
자동 회전있음 (Lambda 기반)없음 (수동)
Crypto항상 KMS 암호화표준 (평문) / SecureString (KMS 암호화)
버전 관리자동 (스테이지: AWSCURRENT, AWSPREVIOUS)자동 (정수 버전)
크기 한도64KBStandard 4KB / Advanced 8KB
비용secret당 $0.40 / 월 + API callStandard 무료 / Advanced 유료
통합RDS / Redshift의 자동 회전 templates광범위 (CloudFormation, ECS, Lambda)

한 줄 결정 가이드 #

결정 트리
DB 비밀번호 / 외부 API 키 + 자동 회전 필요 → Secrets Manager
일반 설정 값 (DB 호스트, region, feature flag) → Parameter Store
회전 필요 없는 시크릿 (예: 외부 API 키) → Parameter Store SecureString (저렴)

Secrets Manager #

만들기 #

JSON 시크릿 (DB 자격증명)
aws secretsmanager create-secret \
  --name myapp/prod/db \
  --description "Postgres for myapp prod" \
  --secret-string '{
    "username": "myapp",
    "password": "very-strong-secret",
    "host": "myapp-prod.abc123.ap-northeast-2.rds.amazonaws.com",
    "port": 5432,
    "dbname": "myapp"
  }'

ARN 반환:

arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:myapp/prod/db-AbCdEf

ARN 끝의 6자 랜덤 (AbCdEf)은 시크릿 무단 enumeration 방지용. IAM 정책에서 arn:.../myapp/prod/db-* 같이 와일드카드 사용.

읽기: boto3 #

boto3로 시크릿 가져오기
import boto3, json

client = boto3.client("secretsmanager")
resp = client.get_secret_value(SecretId="myapp/prod/db")
creds = json.loads(resp["SecretString"])

print(creds["username"], creds["password"])

캐싱: 매 호출마다 가져오면 비용 #

API call당 과금이라 매 요청마다 get_secret_value를 호출하면 비용도 늘고 속도도 느려집니다.

전역에서 한 번 + 캐싱 패턴:

Lambda 모듈 전역에서 한 번
import boto3, json

# Lambda 컨테이너의 INIT 단계에서 한 번
_secrets = None

def get_db_creds():
    global _secrets
    if _secrets is None:
        resp = boto3.client("secretsmanager").get_secret_value(
            SecretId="myapp/prod/db")
        _secrets = json.loads(resp["SecretString"])
    return _secrets

def handler(event, context):
    creds = get_db_creds()
    ...

Powertools의 Parameters가 캐싱 / TTL / 다중 시크릿을 한 줄로:

aws-lambda-powertools
from aws_lambda_powertools.utilities import parameters

# TTL 5분 캐싱
creds = parameters.get_secret("myapp/prod/db", transform="json",
                              max_age=300)

자동 회전 #

Secrets Manager의 가장 큰 장점입니다. 일정 주기마다 새 비밀번호를 생성하고 DB의 사용자 비밀번호를 자동 업데이트합니다.

회전 활성화 (RDS 통합)
aws secretsmanager rotate-secret \
  --secret-id myapp/prod/db \
  --rotation-lambda-arn arn:aws:lambda:ap-northeast-2:...:SecretsManagerRDSPostgreSQLRotationSingleUser \
  --rotation-rules AutomaticallyAfterDays=30

AWS가 매니지드 회전 Lambda를 templates으로 제공합니다 (RDS / Redshift / DocumentDB / Aurora). 30일마다 새 비밀번호로 자동 회전됩니다.

회전의 4 단계 (이해 차원) #

rotation의 흐름
1. createSecret      → 새 비밀번호 생성, AWSPENDING으로 저장
2. setSecret         → DB에 새 비밀번호 적용
3. testSecret        → 새 비밀번호로 연결 테스트
4. finishSecret      → AWSPENDING → AWSCURRENT, 옛 것 → AWSPREVIOUS

코드는 항상 AWSCURRENT (기본)을 가져옴. 회전 중간 잠깐 AWSPREVIOUS도 유효.

Single user vs Multi-user 회전 #

  • Single user: 한 사용자의 비밀번호를 갈아끼움. 회전 순간 짧은 connection 끊김 가능
  • Multi-user: 두 사용자를 번갈아 사용. 무중단 회전 가능. 운영 권장

Parameter Store (SSM) #

Standard vs Advanced #

StandardAdvanced
크기4KB8KB
정책 (만료, 알림)없음있음
Throughput40 / 초1,000 / 초 (옵션)
비용무료파라미터 $0.05 / 월

처음에는 Standard로 시작하면 충분합니다.

만들기 #

평문 (설정)
aws ssm put-parameter \
  --name /myapp/prod/aws-region \
  --value ap-northeast-2 \
  --type String

# 계층 (자유 : '/' 로 구조화)
aws ssm put-parameter \
  --name /myapp/prod/feature/new-checkout \
  --value enabled \
  --type String
암호화 (시크릿)
aws ssm put-parameter \
  --name /myapp/prod/external-api-key \
  --value sk_live_abc123 \
  --type SecureString \
  --key-id alias/aws/ssm

SecureString은 KMS로 암호화. AWS 관리 키 (alias/aws/ssm)가 무료.

읽기 #

boto3
import boto3

ssm = boto3.client("ssm")

# 단일
resp = ssm.get_parameter(
    Name="/myapp/prod/external-api-key",
    WithDecryption=True
)
api_key = resp["Parameter"]["Value"]

# 다중 (계층)
resp = ssm.get_parameters_by_path(
    Path="/myapp/prod/",
    Recursive=True,
    WithDecryption=True
)
for p in resp["Parameters"]:
    print(p["Name"], p["Value"])
Powertools
from aws_lambda_powertools.utilities import parameters

# 단일
api_key = parameters.get_parameter(
    "/myapp/prod/external-api-key", decrypt=True, max_age=300)

# 계층
all_params = parameters.get_parameters(
    "/myapp/prod/", decrypt=True, max_age=300)

버전 관리 #

같은 이름에 새 값을 put 할 때마다 버전이 1, 2, 3 … 순으로 누적됩니다. 옛 값으로 롤백할 수 있습니다.

롤백
# 버전 5의 값 보기
aws ssm get-parameter-history --name /myapp/prod/db-host

# 버전 3으로 되돌리기 (복사해서 새 버전)
aws ssm put-parameter --name /myapp/prod/db-host \
  --value $(aws ssm get-parameter --name /myapp/prod/db-host:3 \
    --query 'Parameter.Value' --output text) --overwrite

ECS / Lambda와의 통합 #

가장 자주 만나는 방식입니다. Task Definition / 함수 환경변수에 시크릿을 직접 주입합니다.

ECS Task Definition #

Task Definition의 secrets 항목
{
  "containerDefinitions": [
    {
      "name": "web",
      "image": "...",
      "environment": [
        {"name": "ENV", "value": "production"}
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:myapp/prod/db-AbCdEf"
        },
        {
          "name": "STRIPE_KEY",
          "valueFrom": "arn:aws:ssm:ap-northeast-2:123456789012:parameter/myapp/prod/stripe-key"
        }
      ]
    }
  ]
}

ECS가 Task 시작 직전 시크릿을 가져와 컨테이너의 환경 변수로 주입합니다. 코드 안에서는 os.environ["DATABASE_URL"]만 읽으면 됩니다.

필요한 권한 (#1 Execution Role):

Execution Role 정책
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:myapp/prod/*"
    },
    {
      "Effect": "Allow",
      "Action": ["ssm:GetParameters"],
      "Resource": "arn:aws:ssm:ap-northeast-2:123456789012:parameter/myapp/prod/*"
    },
    {
      "Effect": "Allow",
      "Action": ["kms:Decrypt"],
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/*"
    }
  ]
}

JSON 시크릿의 특정 필드만 #

Secrets Manager의 시크릿이 JSON 일 때, ECS가 특정 필드만 환경변수로 줄 수 있음:

{
  "name": "DB_PASSWORD",
  "valueFrom": "arn:aws:secretsmanager:...:myapp/prod/db-AbCdEf:password::"
}

ARN 끝의 :password::가 JSON의 password 필드만 추출. 첫 :와 마지막 : 사이가 JSON key, 둘째 : 다음은 version.

Lambda의 시크릿 #

Lambda도 같은 패턴입니다. 함수 environment variables에 직접 시크릿을 박지 말고 코드에서 boto3로 가져오세요. 또는 AWS Parameters and Secrets Lambda Extension으로 캐싱 sidecar를 사용할 수도 있습니다. 이 경우 함수 안에서 localhost:2773으로 호출합니다.

Lambda Extension 사용 (HTTP)
import urllib.request, json, os

def get_secret(name):
    url = f"http://localhost:2773/secretsmanager/get?secretId={name}"
    req = urllib.request.Request(url,
        headers={"X-Aws-Parameters-Secrets-Token": os.environ["AWS_SESSION_TOKEN"]})
    with urllib.request.urlopen(req) as resp:
        data = json.loads(resp.read())
    return json.loads(data["SecretString"])

Extension이 자동 캐싱 (TTL 설정 가능). boto3 캐싱 코드 안 짜도 됨.

IaC와의 연결 #

Terraform #

Terraform : 시크릿 만들기 + 사용
resource "aws_secretsmanager_secret" "db" {
  name = "myapp/prod/db"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = "myapp"
    password = random_password.db.result
    host     = aws_db_instance.main.address
  })
}

resource "random_password" "db" {
  length  = 32
  special = false
}

비밀번호 자체를 IaC에 직접 박지 말 것. random_password로 생성하거나, AWS가 회전으로 관리.

CloudFormation의 dynamic reference #

CloudFormation 안에서 시크릿 참조
Resources:
  MyDB:
    Type: AWS::RDS::DBInstance
    Properties:
      MasterUsername: '{{resolve:secretsmanager:myapp/prod/db:SecretString:username}}'
      MasterUserPassword: '{{resolve:secretsmanager:myapp/prod/db:SecretString:password}}'

{{resolve:...}} 패턴이 stack 안에서 시크릿 / 파라미터를 직접 가져옴. 평문이 stack 템플릿 / 로그에 안 남음.

비용 #

Secrets Manager #

  • 시크릿 당 $0.40 / 월
  • API call 10,000당 $0.05

10개 시크릿 + 매일 100 호출 = $4 + 거의 0 = $4 / 월

Parameter Store (Standard) #

  • 무료 (Throughput 40 / 초까지)
  • KMS 사용 시 KMS API call 비용 (10,000당 $0.03)

10개 SecureString + 매일 100 호출 = 거의 0

가격 차이의 포인트 #

회전 / 매니지드 통합이 필요 없으면 Parameter Store SecureString이 압도적으로 저렴. 100개 시크릿을 관리한다면 Secrets Manager는 월 $40이지만 Parameter Store는 거의 $0입니다.

비밀과 설정의 분리 #

운영 권장 패턴:

둘 다 쓰기
Secrets Manager
├── /myapp/prod/db          (자동 회전)
└── /myapp/prod/jwt-signing (자동 회전)

Parameter Store
├── /myapp/prod/db-host
├── /myapp/prod/aws-region
├── /myapp/prod/feature/new-checkout   (feature flag)
├── /myapp/prod/log-level
└── /myapp/prod/external-api-key       (SecureString : 회전 안 하는 시크릿)

회전이 필요한 핵심 비밀만 Secrets Manager. 나머지는 Parameter Store로 비용 절감.

Cross-Account / Cross-Region #

Resource Policy #

다른 계정의 사용자에 시크릿 접근 허용:

Resource Policy
aws secretsmanager put-resource-policy \
  --secret-id myapp/prod/db \
  --resource-policy '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::222222222222:root"},
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*"
    }]
  }'

Replication #

다른 리전에 자동 복제 (Multi-region 운영):

aws secretsmanager replicate-secret-to-regions \
  --secret-id myapp/prod/db \
  --add-replica-regions Region=us-east-1

primary가 회전되면 replica도 자동 동기화.

자주 만나는 함정 #

1) 시크릿이 ECS Task에 안 주입됨 #

Task 시작 시 “ResourceInitializationError … not authorized to perform secretsmanager:GetSecretValue"가 뜨면 Execution Role 권한 누락입니다. 또는 SecureString의 KMS key에 kms:Decrypt가 빠져 있을 수 있습니다.

2) 매 요청마다 boto3 호출 #

코드의 핸들러 안에서 매번 get_secret_value를 호출하면 API call이 폭주해 비용과 지연이 늘어납니다. 모듈 전역에서 한 번 가져온 뒤 캐싱하세요.

3) JSON 필드 추출 ARN의 형태 오타 #

arn:...:db-AbCdEf:password::. 콜론 갯수 / 위치 한 자만 틀려도 silently 빈 문자열로 들어옵니다. 콘솔에서 ARN을 그대로 복사해 쓰세요.

4) IaC의 평문 비밀 #

Terraform secret_string = "abc123". state 파일에 평문. 항상 random_password 또는 외부 입력, state는 암호화 (S3 + KMS).

5) 회전 후 옛 비밀번호로 호출 실패 #

회전 중간 짧은 시간 동안 옛 비밀번호로 만든 connection이 끊길 수 있음. multi-user 회전 + connection retry.

6) 시크릿 이름 의도치 않은 공개 #

ARN의 6자 랜덤 (-AbCdEf)이 CloudTrail / 로그에 남으면 추측이 어렵지만, 시크릿 이름 자체는 (myapp/prod/db) 콘솔 권한 있는 사람이면 다 봄. 권한을 줄이고 (시크릿마다 별도 IAM) 노출 면을 좁힘.

7) Parameter Store throughput 한도 #

Standard의 40 req/s 한도에 부딪히면 throttling. 핫 패스에서 자주 호출하면 Advanced 또는 캐싱.

정리 #

이번 글에서 잡은 것:

  • 시크릿이 들어가면 안 되는 곳. 코드, git, README, Dockerfile, 평문 환경변수
  • Secrets Manager. 자동 회전 (RDS templates), 항상 KMS, secret당 $0.40
  • Parameter Store. 설정 + 시크릿. Standard 무료, SecureString도 무료
  • 선택 가이드. 자동 회전 필요 → Secrets Manager. 그 외 → Parameter Store SecureString
  • boto3 + 캐싱. 모듈 전역에서 한 번 + Powertools의 parameters
  • 자동 회전 4 단계. createSecret → setSecret → testSecret → finishSecret. multi-user가 무중단
  • ECS 통합. Task Definition의 secrets로 환경변수 자동 주입. Execution Role 권한 필수
  • JSON 시크릿의 특정 필드. ARN 끝의 :fieldName::
  • Lambda Extension. Parameters and Secrets Lambda Extension으로 캐싱 sidecar
  • IaC. random_password 생성, CloudFormation의 {{resolve:...}}
  • 비밀 / 설정 분리. 회전 필요한 핵심만 Secrets Manager, 나머지는 Parameter Store
  • Cross-Account / Cross-Region. Resource Policy / Replication
  • 함정. Execution Role 권한, 호출 폭주, ARN 오타, IaC 평문, 회전 중 끊김, throughput 한도

다음: Step Functions #

이 시리즈의 마지막입니다. 여러 Lambda / ECS / 외부 API 호출을 묶어 하나의 워크플로우로 만드는 방식입니다.

#7 Step Functions 입문에서는 State machine / Task / Choice / Parallel, Standard vs Express, Lambda 오케스트레이션, 에러 처리와 retry까지, AWS 워크플로우를 한 번에 정리하겠습니다.

X