Terraform 実践 #6 シークレット管理 — SSM vs Secrets Manager、write-only 引数
前回の最後の確認は、落ち着かないものでした。sensitive を付けたのに、DB パスワードは state に平文で残り、tfvars にも平文で残っています。今回はこの問題を 3 段階で解決します。シークレットを置く場所を決め(保管先)、Terraform がシークレットを扱いながらも記録に残さないようにし(write-only)、アプリケーションがシークレットを受け取る道筋を用意します(ランタイム参照)。3 つのうち 1 つでも欠けると、どこかに平文が残ります。
保管先の選択 — SSM Parameter Store vs Secrets Manager #
AWS でシークレットの保管先候補は 2 つです。
| 項目 | SSM Parameter Store | Secrets Manager |
|---|---|---|
| 料金 | 標準パラメータは無料 | シークレットごとの月額 + API 呼び出し |
| 暗号化 | SecureString(KMS) | デフォルトで KMS 暗号化 |
| 自動ローテーション | なし(自前で実装) | RDS などと統合されたローテーションを内蔵 |
| 向いている用途 | 設定値、小規模なシークレット | ローテーションが必要な DB 認証情報 |
選択基準はローテーションです。パスワードを定期的に自動交換する要件があるなら Secrets Manager がその機能を内蔵していますし、その要件がなければ無料の SSM SecureString で十分です。この実習はコストの原則に従って SSM で進めますが、コードの構造はどちらでも同じなので、あとから移すのは難しくありません。
ephemeral と write-only — 記録に残らない値 #
Terraform 1.10 と 1.11 で、この問題のための言語機能が正式に入りました。部品は 2 つです。
- ephemeral リソース: plan にも state にも記録されない一回限りの値です。
ephemeral "random_password"は、実行中にだけ存在するパスワードを作ります。 - write-only 引数: 値を provider に渡すだけで記録しない引数です。
aws_db_instanceのpassword_woがそれで、通常のpassword引数と違って state に保存されません。
2 つを組み合わせると、パスワードの一生が記録の外で完結します。
# secrets.tf(新しいファイル)
ephemeral "random_password" "db" {
length = 24
special = false
}
resource "aws_ssm_parameter" "db_password" {
name = "/myapp/${var.env}/db-password"
type = "SecureString"
value_wo = ephemeral.random_password.db.result
value_wo_version = 1
}# db.tf を修正
resource "aws_db_instance" "main" {
# ... 残りは前回のまま ...
password_wo = ephemeral.random_password.db.result
password_wo_version = 1
}前回の password = var.db_password と variable "db_password" は削除します。もうパスワードは、人が決めることも、ファイルに書かれることもありません。実行中に生成されて RDS と SSM に渡され、消えます。terraform state show aws_db_instance.main をもう一度実行してみると、password は見えません。
_wo_version という見慣れない相方が付く理由も覚えておきます。write-only の値は state にないため、Terraform は値の変更を検知できません。そのため、パスワードを交換したいときは値を変えるのではなく、password_wo_version を 1 から 2 に上げます。バージョン番号の変更が plan に載り、provider が新しい値を渡す仕組みです。
アプリケーションはランタイムに読みます #
シークレットが SSM に安全に入っても、それを user_data で取り出してファイルに埋め込めば、インスタンスの中に平文のコピーが生まれて振り出しに戻ります。原則は、アプリが起動時に保管先から直接読むことです。
# アプリケーションの起動スクリプトで
DB_PASSWORD=$(aws ssm get-parameter \
--name "/myapp/dev/db-password" \
--with-decryption \
--query Parameter.Value --output text)この構造の利点は、シークレットの交換がデプロイと切り離されることです。SSM の値を変えてアプリを再起動すれば終わりで、イメージや launch template を作り直す必要はありません。残るパズルのピースは権限です。インスタンスが ssm:GetParameter を呼ぶには IAM ロールが必要ですが、今のインスタンスにはロールがありません。これがまさに次回のテーマで、SSH なしの接続(Session Manager)まで一緒に解決します。
残る記録とリスクの整理 #
この構成で何が消えて何が残るのか、正直に整理しておきます。
- 消えたもの: コード、tfvars、plan、state のパスワード平文です。人がパスワードを知る段階そのものがありません。
- 残るもの: SSM パラメータ(KMS 暗号化で保存)と RDS の内部です。この 2 つはシークレットが本来あるべき場所で、アクセスは IAM で統制します。
- 変わらない原則: 基礎 #9 の state バケットのアクセス制御は、これまでどおり重要です。write-only が DB パスワードを消しても、state にはインフラの構造全体が入ったままだからです。
まとめ #
今回扱った内容です。
- 保管先の選択基準はローテーションです。必要なら Secrets Manager、不要なら無料の SSM SecureString で十分です
- ephemeral リソースは実行中にだけ存在する値を作り、write-only 引数(password_wo、value_wo)は値を渡すだけで state に残しません。どちらも Terraform 1.11 の正式機能です
- write-only の値の交換は、値の変更ではなく
_wo_versionの増加でトリガーします。state に値がなく、変更を検知できないためです - アプリはシークレットをランタイムに保管先から直接読みます。user_data に埋め込むと平文のコピーが復活します
- シークレットは今や SSM と RDS の中にだけ存在し、残る課題は読み取り権限を与える IAM です
次回(#7 IAM のコード化)では、インスタンスロールで SSM アクセスと Session Manager 接続を完成させ、GitHub Actions がアクセスキーなしで AWS に入る OIDC 連携まで作ります。