AWS上級 #6 Secrets Manager / Parameter Store

DB パスワード、外部 API キー、OAuth client secret — どこに置くか。コード / git は絶対 NG、環境変数に平文も危険。AWS には 2 つの選択肢があります — Secrets ManagerSSM Parameter Store

この記事は両方の違い、自動ローテーション、コードから取得するパターン、ECS / Lambda との統合、IaC 接続、費用比較まで — シークレット / 設定管理の基準を一気に整理します。

シークレットが入らない場所 #

基礎 #6 セキュリティ基本 の延長。絶対にやらないこと:

  • コードの中の平文 (PASSWORD = "abc123")
  • git の .env (削除した後も history に永遠)
  • README、Slack、Wiki、メールに貼り付け
  • Dockerfile の環境変数 / ビルド引数 (イメージ layer に刻まれる)
  • ECS Task Definition の平文 environment variables (コンソール / IaC / ログに露出)

代わりに使うもの: Secrets Manager または SSM Parameter Store

2 つの違い — 一行比較 #

Secrets ManagerSSM Parameter Store
正体シークレット専用マネージド設定 + シークレット兼用
自動ローテーションあり (Lambda ベース)なし (手動)
Crypto常に KMS 暗号化標準 (平文) / SecureString (KMS 暗号化)
バージョン管理自動 (ステージ: AWSCURRENT、AWSPREVIOUS)自動 (整数バージョン)
サイズ上限64 KBStandard 4 KB / Advanced 8 KB
費用secret 当たり $0.40 / 月 + API callStandard 無料 / Advanced 有料
統合RDS / Redshift の自動ローテーションテンプレート広範囲 (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 は高い + 遅い。

グローバルで 1 回 + キャッシング パターン:

Lambda モジュールグローバルで 1 回
import boto3, json

# Lambda コンテナの INIT 段階で 1 回
_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 / 多重シークレットを 1 行で:

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 をテンプレートで提供 (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: 1 ユーザーのパスワードを差し替え。ローテーション瞬間に短い接続切れの可能性
  • Multi-user: 2 ユーザーを交互に使用。無停止ローテーション可能。運用推奨

Parameter Store (SSM) #

Standard vs Advanced #

StandardAdvanced
サイズ4 KB8 KB
ポリシー (有効期限、通知)なしあり
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"] だけ見れば OK。

必要な権限 (#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、2 番目の : の次は 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/月 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 も自動同期。

よく出会う落とし穴 #

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 が暴走、費用 + 遅延。モジュールグローバルで 1 回 + キャッシング。

3) JSON フィールド抽出 ARN の形のタイポ #

arn:...:db-AbCdEf:password:: — コロンの数 / 位置が 1 文字でも間違えば 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 またはキャッシング。

まとめ #

今回つかんだもの:

  • シークレットが入らない場所 — コード、git、README、Dockerfile、平文環境変数
  • Secrets Manager — 自動ローテーション (RDS テンプレート)、常に KMS、secret 当たり $0.40
  • Parameter Store — 設定 + シークレット。Standard 無料、SecureString も無料
  • 選択ガイド — 自動ローテーション必要 → Secrets Manager。それ以外 → Parameter Store SecureString
  • boto3 + キャッシング — モジュールグローバルで 1 回 + 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
  • IaCrandom_password 生成、CloudFormation の {{resolve:...}}
  • 秘密 / 設定分離 — ローテーション必要な核心だけ Secrets Manager、残りは Parameter Store
  • Cross-Account / Cross-Region — Resource Policy / Replication
  • 落とし穴 — Execution Role 権限、呼び出し暴走、ARN タイポ、IaC 平文、ローテーション中切断、throughput 上限

次回 — Step Functions #

このシリーズの最後。複数の Lambda / ECS / 外部 API 呼び出しを束ねて 1 つのワークフローに する方式です。

#7 Step Functions 入門 では State machine / Task / Choice / Parallel、Standard vs Express、Lambda オーケストレーション、エラー処理と retry まで — AWS ワークフローを一気に整理します。

X