/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

Lambda 環境変数にシークレットを入れるか Secrets Manager SDK で取るかの判断基準

Lambda にシークレットを渡すとき、環境変数にそのまま入れるか、ランタイムで Secrets Manager から取るか、悩んだんですけど——正直「KMS で暗号化されるんだし環境変数でいいのでは?」と思っていました。

調べてみると、これがけっこう違う問題でした。

「保存時の暗号化」と「取り扱い中の露出」は別の話

Lambda 環境変数は静止時に KMS で暗号化されます。そこは本当です。ただしデフォルト(AWS managed key)の場合、lambda:GetFunctionConfiguration 権限を持つ人やツールからは平文で読めます。CMK(Customer Managed Key)を使えば kms:Decrypt 権限でさらに制御できますが、ツールチェーン側の露出は残ります。

具体的な露出経路はこんな感じです。

環境変数方式の露出経路(Lambda 外)

  構成管理ツール側
  ├─ Terraform state  → JSON 内に平文で記録
  ├─ terraform plan   → diff 出力に平文で表示
  ├─ lambroll diff    → 同上
  └─ CloudFormation   → パラメータで直値を渡すと平文
                         (dynamic reference 利用時は露出経路が異なる)

  CI/CD 側
  ├─ デプロイパイプラインの履歴 → plan 出力が保存される
  └─ PR 上の plan 結果コメント  → GitHub に平文が残る

lambroll の diff コマンドで Secrets Manager の値が平文表示される、というのが自分でハマったきっかけです。でもこれは lambroll だけの問題ではなくて、「Lambda を取り巻くツールチェーン全体」の問題です。

Lambda 自体のセキュリティとツールチェーンのセキュリティは別レイヤー。環境変数の KMS 暗号化は前者しかカバーしません。

本質は「いつシークレットを解決するか」

「環境変数は危険だからやめよう」というシンプルな話ではありません。正しい問いは「シークレットの解決をデプロイ時にやるか、ランタイム時にやるか」です。

デプロイ時解決(環境変数方式)

  Secrets Manager → Terraform/lambroll
    → 環境変数として埋め込み → Lambda 実行

  メリット: コードがシンプル / レイテンシゼロ
  コスト  : パイプライン全体に平文が流れる
            ローテーション時に再デプロイが必要

---

ランタイム解決(SDK / Extension 方式)

  Lambda 実行 → SDK/Extension
    → Secrets Manager から取得 → 利用

  メリット: パイプラインに平文が流れない
            ローテーション時に再デプロイ不要
  コスト  : 取得時のレイテンシ / API 呼び出しコスト
            コードの複雑性が増す

どちらを選ぶかはセキュリティ要件だけでなく、「そのシークレットがローテーションされるか」「パイプラインのログが永続化されるか」で決まります。

レイテンシの問題は Parameters and Secrets Extension でかなり解消された

「SDK で取るとレイテンシが増える」はかつては事実でした。決め手になったのは AWS Parameters and Secrets Lambda Extension(2022年リリース)の存在です。

Extension は Lambda 実行環境に常駐するプロセスで、ローカル HTTP サーバー(port 2773)からシークレットをキャッシュして返してくれます。ただしハンドラ内(INVOKE フェーズ)からのみ呼べます。グローバル初期化コードからの取得は SDK 直接呼び出しに切り替える必要があります。

取得方式ごとのレイテンシ比較(目安)

  方式                              コールドスタート  ウォームスタート
  ──────────────────────────────────────────────────────────────────
  環境変数                           0 ms             0 ms
  SDK 直接(キャッシュなし)           50-200 ms        50-200 ms
  SDK + アプリ内キャッシュ            50-200 ms          ~0 ms
  Parameters and Secrets Extension   ~50 ms (初回)     ~12 ms

(レイテンシは計測値の一例です。公式仕様値ではなく、環境やリージョンによって異なります)

ウォームスタート時で約 12ms。ほとんどのユースケースでは無視できる水準です。メモリのオーバーヘッドは約 50MB なので、レイテンシもメモリも致命的というほどではありません。

判断フロー

「このシークレット、どう渡す?」と自分に問うとき、こういう順で考えています。

そのシークレットはローテーションされるか?
  │
  ├─ Yes → ランタイム解決(SDK / Extension)一択
  │         再デプロイなしでローテーションに追従できる
  │
  └─ No → デプロイパイプラインに plan 出力や state が残るか?
            │
            ├─ Yes(Terraform / lambroll / CloudFormation)
            │    │
            │    ├─ plan 出力が PR コメントや CI ログに残る?
            │    │    │
            │    │    ├─ Yes → ランタイム解決を推奨
            │    │    │         平文が GitHub/CI 履歴に永続化される
            │    │    │
            │    │    └─ No  → sensitive + state 暗号化を前提に
            │    │              環境変数方式も許容可能
            │    │
            │    └─ state の保管先にアクセス制御があるか?
            │         ├─ S3 + SSE + バケットポリシー → 許容範囲
            │         └─ ローカル state → ランタイム解決を推奨
            │
            └─ No(手動設定のみ)
                 → 環境変数方式でも実害は小さい
                   ただし GetFunctionConfiguration の
                   権限管理は必要

ローテーションがあるなら迷わずランタイム解決です。ないケースでも、Terraform や lambroll を使っていて plan 出力が CI ログに残るなら、ランタイム解決の方が後悔が少ないです。

まとめ

Lambda 環境変数の KMS 暗号化は「Lambda に保存されているときだけ」の話で、パイプラインや state を流れる間は平文です。この差を意識しておくと、「環境変数で十分?」の問いに自分なりの答えを出せます。

  • ローテーションあり → ランタイム解決一択
  • plan/diff/state が CI/PR に残る構成 → ランタイム解決を推奨
  • 手動設定のみ・ローテーションなし → 環境変数方式も許容範囲(GetFunctionConfiguration の権限管理は必要)

Parameters and Secrets Lambda Extension のおかげで「SDKで取るとレイテンシが」という懸念はかなり薄れました。どちらを使うかは、レイテンシよりも「シークレットがいつ・どこで平文になるか」で判断するのが実態に合っています。

参考