背景
AWS Secrets Managerは認証情報を安全に管理できる便利なサービスですが、Terraformで扱うとtfstateに値が平文で残るリスクがあります。IaCによる再現性や一元管理の利点を活かしつつ、この課題をどう回避するかが実運用のポイントです。
本記事では、筆者が考える実践的な管理方法を紹介します。
課題:tfstateと機密情報のリスク
Terraformはリソースの状態を terraform.tfstate ファイルに保存します。これによりインフラの差分管理や再現性が担保されますが、同時に「シークレットの値」もそのまま記録されてしまう可能性があります。
たとえば aws_secretsmanager_secret_version リソースによってシークレット作成をTerraformで行うと、パスワードやAPIキーが平文でstateファイルに残り、S3バックエンドやGit管理を通じてチーム全体に共有されてしまいます。これは情報漏洩につながる大きなリスクです。
悪い例
例えば、次のようなterraformのコード(main.tf)を作成し、Secrets Manager Secretを作成してみます。
resource "aws_secretsmanager_secret" "example" {
name = "example-secret"
}
resource "aws_secretsmanager_secret_version" "example" {
secret_id = aws_secretsmanager_secret.example.id
secret_string = jsonencode({
username = "admin"
password = "P@ssw0rd123!"
})
}
コード内に機密情報をハードコードしている点が目に見えて良くないポイントです。
しかしもう一つ、見落としがちなポイントがあります。それがリソースの状態を記すterraform.tfstateファイルです。

リソースを作成したときに更新された、tfstateファイルの中身も確認してみます。見ての通り、secret_stringにシークレットの値が平文で記録されてしまいます。
tfstateファイルを厳重に管理しなければ、思いがけない情報漏洩を招いてしまいかねません。
このような課題に対処するためのアイデアを、本記事では紹介したいと思います。
実際に作成してみた
ディレクトリ構成は以下の通りです。
.
├── main.tf
├── modules
│ └── secrets-manager
│ ├── main.tf
│ └── variables.tf
├── provider.tf
├── terraform.tfvars.json
└── variables.tf
main.tfからmodules/secrets-managerを呼び出してSecrets Managerリソースを作成します。
シークレット値をプログラム中にハードコードするのは避け、terraform.tfvars.jsonに記述して外部から注入する構成としています。
terraform.tfvars.jsonは .gitignore に追加し、配布や保管にも注意してください。
本記事では簡単のために tfvars.json を利用しますが、理想的には GitHub Actions などのCI/CDでSecretsを安全に渡す運用 を推奨します。
provider "aws" {
region = var.aws_region
}
- variables.tf
variablesにはsecretの値を格納するためのsecrets_payloadsという変数を作成します。
データ型はmapを2重の構造にすることで、複数のシークレットをまとめて管理できるようにしています。
variable "aws_region" {
type = string
description = "AWS region"
default = "ap-northeast-1"
}
variable "secrets_payloads" {
type = map(map(any))
sensitive = true
}
- terraform.tfvars.json
terraform.tfvars.json を使うと、変数に外部ファイルから値を渡せます。
JSONのキーをTerraformの変数名と揃えることで、自動的にその変数に値が割り当てられます。 JSONのオブジェクトは Terraformでは map 型として解釈される仕組みです。
{
"secrets_payloads": {
"example_secret_1": {
"username": "user1",
"password": "pass1"
},
"example_secret_2": {
"username": "user2",
"password": "pass2"
}
}
}
- modules/secrets-manager/main.tf
Secrets Managerのリソースを2つ定義しています。aws_secretsmanager_secretリソースでシークレットの入れ物を作成するイメージです。
aws_secretsmanager_secret_version リソースで実際の値を登録しますが、その際に secret_string_wo 属性を利用することで、値が tfstate に保存されないようにしているところがポイントです。
また、Secrets Managerが受け取れる文字列にするために、jsonencodeを使用してmap型文字列をJSON文字列に変換します。
さらに var.secret_version を指定することで、バージョンを明示的に管理できるようにしています。
resource "aws_secretsmanager_secret" "main" {
name = var.secret_name
}
resource "aws_secretsmanager_secret_version" "main" {
secret_id = aws_secretsmanager_secret.main.id
secret_string_wo = jsonencode(var.secret_payloads)
secret_string_wo_version = var.secret_version
}
variable "secret_name" {
type = string
}
variable "secret_payloads" {
type = map(any)
}
variable "secret_version" {
type = string
}
- main.tf
ルートモジュール側のmain.tfです。
localsのブロックでは、作成するシークレットごとに変更検知用のバージョンを作成しています。
具体的には、sha256のハッシュ関数を使って、シークレットの値からハッシュを作成します。
このとき、ハッシュ計算を行うためにnonsensitive関数を使って機密フラグを外しています。値そのものはsecrets-managerモジュールでwrite-onlyで渡すため、値がtfstate等で露見することはありません。
さらに、substr関数でハッシュ値の先頭8桁を抽出し、parseint関数を使って数値型に変換しています。substr関数を使わないと桁数が多すぎるため先頭8文字を切り出す処理を入れています。
このようにして、シークレットの値ごとに決定的なバージョニングを実装しています。
for_eachを使用して、作成するシークレットごとにモジュールを増やしています。
読み込んだterraform.tfvars.jsonから、secrets_name, secret_payloadsを抽出し、secretsmanagerモジュールに渡します。
さらに、secret_versionはlocalsブロックで生成したバージョン値を渡す処理をしています。
locals {
secrets_keys = keys(var.secrets_payloads)
secret_versions = {
for key in local.secrets_keys :
key => parseint(substr(nonsensitive(sha256(jsonencode(var.secrets_payloads[key]))), 0, 8), 16)
}
}
module "secretsmanager" {
source = "./modules/secrets-manager"
for_each = toset(nonsensitive(local.secrets_keys))
secret_name = each.key
secret_payloads = var.secrets_payloads[each.key]
secret_version = local.secret_versions[each.key]
}
動作確認
上記のコードでterraform applyを実行すると、Secrets Managerリソースが2つ作成されます。
% terraform apply


tfstateの中身も確認してみます。下図に示す通り、write only属性を指定しているため、tfstateの中には値が記録されなくなりました。
write only属性を有効化した場合、tfstateにシークレット値が記録されなくなるため、secret_string_wo_versionという値を使用してリソースの変更を管理する仕組みとなっています。

作成したterraformコードでは、シークレット値から作成したハッシュ値をもとにしてバージョン値を生成しています。
シークレットの値を変更した際に、バージョン値が変わることも確認してみましょう。
terraform.tfvars.jsonの、example_secret_1のpasswordをpass1からpass11に変更して、terraform applyを実行してみます。
{
"secrets_payloads": {
"example_secret_1": {
"username": "user1",
"password": "pass11"
},
"example_secret_2": {
"username": "user2",
"password": "pass2"
}
}
}
terraform applyを実行すると、module.secretsmanager[“example_secret_1”].aws_secretsmanager_secret_version.main must be replaced
の表示がでて、シークレット値の変更によるバージョンの更新が検知されたことが確認できるはずです。
% terraform apply
(中略)
# module.secretsmanager["example_secret_1"].aws_secretsmanager_secret_version.main must be replaced
(中略)
Plan: 1 to add, 0 to change, 1 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
作成されたリソースをみると、値が更新されることが確認できます。

tfstateをみると、バージョンが自動的に更新されたことがわかります。
シークレットの値を更新する場合は、このようにバージョンを自動で更新する仕組みを作るか、手動でバージョンを更新する必要があります。
この点は、write only属性を使用する際の注意点だと思います。

コメント