목차
20 장

Secrets Manager / Parameter Store

AWS의 시크릿 / 설정 관리를 정리합니다. Secrets Manager와 SSM Parameter Store의 역할 차이, 자동 회전, 코드에서 가져오기(boto3 / 캐싱 / Powertools), ECS와 Lambda 통합, IaC 연결, 비밀과 설정의 분리, 비용 비교까지 다룹니다.

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

본 챕터는 둘의 차이, 자동 회전, 코드에서 가져오는 패턴, ECS / Lambda와의 통합, IaC 연결, 비용 비교까지 시크릿 / 설정 관리의 기준을 한 번에 정리합니다. 직전 19장 EventBridge / SQS / SNS 까지가 컴포넌트 사이의 통신이었다면, 본 챕터는 그 컴포넌트들이 안전하게 들고 있어야 할 비밀을 다룹니다. 여기서 잡는 패턴은 15장 ECS와 Fargate의 Task Definition secret 주입, 4부 22장 ECS Fargate 배포 골격, 23장 RDS 연동에서 그대로 쓰입니다.

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

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()
    ...

17장 Lambda 기초의 전역 변수 패턴과 같은 맥락입니다. 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"]만 보면 됩니다.

필요한 권한입니다 (15장 ECS와 Fargate의 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가 회전으로 관리합니다. Terraform 자체는 25장 Terraform 입문에서 다룹니다.

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 / 월 vs 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도 자동 동기화됩니다. 멀티 리전·재해 복구는 30장 재해 복구·백업에서 다룹니다.

자주 만나는 함정 #

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 빈 문자열이 됩니다. 콘솔에서 그대로 복사해 씁니다.

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 또는 캐싱을 씁니다.

연습문제 #

  1. 본인의 앱이 들고 있는 비밀과 설정을 모두 적고, §“비밀과 설정의 분리"의 패턴을 따라 어느 것을 Secrets Manager에, 어느 것을 Parameter Store SecureString에 둘지 나눠 보세요. 자동 회전이 필요한 항목과 아닌 항목을 가르는 기준을 한 줄로 적습니다.
  2. ECS Task에 시크릿이 안 주입되는 “ResourceInitializationError"가 났을 때 점검해야 할 두 권한이 무엇인지 §“ECS / Lambda와의 통합"과 15장 ECS와 Fargate의 Execution Role을 근거로 적어 보세요.
  3. §“비용"의 수치를 근거로, 100개 시크릿을 모두 Secrets Manager에 둘 때와 회전이 필요한 5개만 Secrets Manager에 두고 나머지 95개를 Parameter Store SecureString에 둘 때의 월 비용 차이를 계산해 보세요.

한 줄 요약: 시크릿은 코드·git·README·Dockerfile·평문 환경변수에 두지 않고 Secrets Manager 또는 Parameter Store에 둔다. 자동 회전(RDS templates)과 항상 KMS가 필요하면 Secrets Manager(secret 당 $0.40)를, 회전이 필요 없는 설정·시크릿은 무료인 Parameter Store SecureString을 쓴다. 코드에서는 모듈 전역에서 한 번 + Powertools 캐싱으로 가져오고, ECS는 Task Definition의 secrets로 환경변수에 자동 주입하되 Execution Role 권한이 필수다. IaC에는 평문 비밀을 박지 말고 random_password{{resolve:...}}를 쓴다.

다음 챕터 #

다음 21장 Step Functions 입문은 3부의 마지막입니다. 여러 Lambda / ECS / 외부 API 호출을 묶어 하나의 워크플로우로 만드는 방식을 다룹니다. State machine / Task / Choice / Parallel / Map, Standard vs Express, Lambda 오케스트레이션, 에러 처리와 retry까지 AWS 워크플로우 엔진을 정리합니다.

X