tech.guitarrapc.cóm

Technical updates

terraform-provider-sopsとEphemeral valuesを使ってTerraformでシークレットを安全に扱う

AWSにはSSM Parameter StoreやSecrets Managerなど、機密情報を安全に管理するためのサービスが提供されています。しかし、TerraformでAWS環境を構築する際、SSM Parameter StoreやSecrets Managerに機密情報を登録する方法は悩ましいものがありました。Stateファイルに平文で保存されてしまう懸念から、手動で登録するパターンを使っているケースも多く見かけます。

この記事では、Terraform 1.10.0で導入されたEphemeral resourceterraform-provider-sopsを組み合わせて、機密情報を安全にコード管理する方法を紹介します。この方法には次のメリットがあります。

  • 機密情報をSOPSで暗号化してGit管理できる
  • TerraformのStateファイルに機密情報が保存されない
  • 機密情報もTerraformコードベースで把握できる

Ephemeral resourceについては、使い方の記事を以前書きました。合わせてどうぞ。

はじめに

2025年にリリースされたTerraform 1.10.0でEphemeral resourceが導入され、その値はStateファイルに残らなくなりました。Ephemeral resourceが利用できるかはリソースの対応を待つ必要があるのですが、2025年10月にリリースされたterraform-provider-sops 1.3.0でEphemeral resource対応しました。

これにより、SOPSで暗号化したファイルをGit管理しつつ、TerraformのStateファイルから機密情報を排除する構成をとれます。これまで難しかったTerraformでのシークレット管理がコード管理できるようになりました。

従来の方法と課題

TerraformでAWS環境を構築する際、SSM Parameter StoreやSecrets Managerに機密情報を登録する方法として、従来はTerraformでダミーの値を登録し、後からAWSコンソールやaws cliで機密情報に置き換える方法がよく使われていました。

resource "aws_ssm_parameter" "main" {
  name  = "my_secret_parameter"
  type  = "String"
  value = "DUMMY_VALUE"  # 後でAWSコンソールやaws cliで置き換える

  lifecycle {
    ignore_changes = [value]  # valueの変更を無視する
  }
}

運用でカバーする方法としては悪くないのですが、2つ課題があります。

AWSコンソールやaws cliで直接機密情報を登録する課題

Ephemeral resourceが登場するまでは、Terraformでこれらの値を入れるとStateファイルに平文で保存されてしまうため、AWSコンソールやaws cliで直接登録する方法がよく使われていました。しかしこの方法には以下のような問題点があります。

  • 機密情報の登録を手動で行う必要があり、環境構築の自動化が難しい
  • 誰がいつどのような値を登録したかの履歴がCloud Trailで確認するしかない
  • どのような値が登録されているかをコードベースで把握できない
  • 権限管理が必要なため、できる人、できない人が発生する

いずれもIaCが解決するべき課題であり、Terraformで機密情報を管理したいニーズは高いです。 Terraform Sateファイルに平文で保存されない方法があれば、Terraformで機密情報を管理することが可能になります。このため、Ephemeral valuesの登場は非常に大きな意味があります。

Terraformで機密情報を登録する課題

Ephemeral resourceが登場しても、機密情報をTerraformに渡す方法が課題でした。いくつか方法がありますが、代表的なものとして以下が挙げられます。

  • 実行環境から環境変数経由で渡す
  • Terraform実行時にコマンドライン引数で渡す
  • 暗号化されたファイルから読み込む

環境変数経由やコマンドライン引数で渡す方法は、実行環境に機密情報を保持する必要があり、実行環境の管理が煩雑になる問題があります。また、どのような値を渡しているかをコードベースで把握できない問題もあります。

暗号化されたファイルから読み込む方法は、コードベースで把握できます。しかし、terraform-provider-sopsはEphemeral resourceを実装していなかったため、dataリソースで読み込んだシークレットがTerraform Stateファイルに平文で保存されてしまう問題がありました。

terraform-provider-sopsがEphemeral resourceに対応したことで、SOPSで暗号化したファイルをgitに置きつつ、TerraformのStateファイルには機密情報を保存しない構成をとれます。

terraform-provider-sopsとEphemeral valuesを使って機密情報を安全に扱う

シークレットをコード管理しつつ、TerraformのStateファイルに平文で保存しない。両者の課題を解決できるのが、terraform-provider-sopsとEphemeral valuesを組み合わせた方法です。今回は、AWS SSM Parameter Storeに機密情報を登録する例1を紹介します。

SOPSをTerraformで使う手順を見てみましょう。1-3までは事前準備なので、一度構成してしまえば以降は再実施不要です。普段の運用は、いつも通りTerraformリソースを書いて実行するだけです。

  1. KMSキーの作成 (事前設定)
  2. SOPSで暗号化されたファイルの作成 (事前設定)
  3. TerraformでSOPSプロバイダーの設定 (事前設定)
  4. Terraformリソースの定義
  5. Terraformの実行

実行環境

次の実行環境を用います。

事前にTerraformとSOPSをインストールしておいてください。 sopsは、scoopやHomebrew、aquaなどでインストールできます。

KMSキーの作成

SOPSの暗号化・復号に使用するKMSキーを作成します。KMSを用いることで、sopsで暗号化されたファイルへのアクセス権限を、KMSアクセスできるIAM RoleやIAM User単位で管理できます。 以下のTerraformコードでKMSキーを作成します。

data "aws_caller_identity" "current" {}

resource "aws_kms_key" "main" {
  description             = "Terraform managed."
  deletion_window_in_days = 7
  enable_key_rotation     = true
  key_usage               = "ENCRYPT_DECRYPT"
  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "Enable IAM User Permissions",
        "Effect" : "Allow",
        "Principal" : {
          "AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        },
        "Action" : [
          "kms:*"
        ],
        "Resource" : "*"
      }
    ]
  })
}

resource "aws_kms_alias" "main" {
  name          = "alias/terraform-provider-sops"
  target_key_id = aws_kms_key.main.key_id
}

output "arn" {
  description = "KMS Key arn"
  value       = aws_kms_key.main.arn
}

KMS ARNは後でSOPSの設定で使用するため、出力しておきます。

次に、SOPSを実行するIAM RoleやIAM Userに、このKMSキーを使用する権限を付与します。以下のポリシーはKMS暗号化・復号に必要な権限を付与します。このポリシーを付けたIAM Role/ユーザーがSOPSでシークレットを暗号化・復号できます。

{
  "Sid": "Allow use of the key",
  "Effect": "Allow",
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "*",
  "Principal": {
    "AWS": [
      "arn:aws:iam::123456789012:role/sops-dev-xyz"
    ]
  }
}

SOPSで暗号化されたファイルの作成

SOPSで暗号化されたファイル.sops.yamlを作成します。今回はfooフォルダでTerraformリソースを定義していると仮定して、./foo/secrets.yamlファイルを作成します。secrets.yamlにSOPSで暗号化・復号するシークレットを設定、指定したKMS鍵で暗号化・復号するように設定します。

  • path: 暗号化するシークレットが書かれたファイルパス
  • kms: 先ほど作成したKMSキーのARN
creation_rules:
  - path: ./foo/secrets.yaml
    kms: >-
      arn:aws:kms:ap-northeast-1:123456789012:key/01234567-1234-abcd-abcd-1234567890ab

SOPSで暗号化・復号操作する前に、事前にAWSアカウントの認証を取得しておきましょう。KMSキーへのアクセス権限を持つプロファイルを使用してください。

# Identity Center (SSO) を使っている場合
aws sso login --profile YOUR_PROFILE

# aws loginを使っている場合
aws login --profile YOUR_PROFILE

sops editコマンドを使うと、指定したファイルを自動的に復号してエディタで開きます。編集後に保存すると、自動的に暗号化されます。secrets.yamlファイルがなくても作成してくれるので、初回から使えて万能です。

AWS_PROFILE=YOUR_PROFILE sops edit ./foo/secrets.yaml

コマンドを実行すると、初期状態なら次のような内容でエディタが開きます。

hello: Welcome to SOPS! Edit this file as you please!
example_key: example_value
# Example comment
example_array:
    - example_value1
    - example_value2
example_number: 1234.56789
example_booleans:
    - true
    - false

雑に編集して、次のようにfooとbarの2キーを持つYAMLファイルにします。

foo: thisisasecretvalue
bar: 123456789

保存するとsecrets.yamlファイルが暗号化されます。内容を確認してみましょう。

$ cat ./foo/secrets.yaml
foo: ENC[AES256_GCM,data:Gm/K9H+V+tM8TFrO329WYlHh,iv:75K+UGidrBPFGGdseZvvoWkGRNG32LvTDAy59O2ZvsI=,tag:nueHHxln7AWZWVuE+JGlgw==,type:str]
bar: ENC[AES256_GCM,data:VAKgrrKJQzpK,iv:ZDiCPV247Om6xIV6EBLUFRaQuXoBazvogckLWoX+1Vo=,tag:nXiLbNX0qwWL4UgxYS/5DQ==,type:int]
sops:
    kms:
        - arn: arn:aws:kms:ap-northeast-1:123456789012:key/01234567-1234-abcd-abcd-1234567890ab
          created_at: "2026-01-06T16:57:47Z"
          enc: AQICAHiU+HnowUKfgMmUO2S0Jj9ScLimZ37vJyD1AVgeLYaUSgFIHwSL2H1eF7HzbrHGSAmSAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMhgseKZ423Pb8ehrdAgEQgDuTdy3uTRjrppPambOmjwKH2eIS3JK6+6LhD/scB2K2eaSAq6+sNpI3p6fepoAS3EjeaCMTd7n5ieYFkg==
          aws_profile: ""
    lastmodified: "2026-01-06T16:59:12Z"
    mac: ENC[AES256_GCM,data:gpVk67OIWAWg+iyWUu9q7cmI/DcviR70f5e+cI7kvCnBj0bVPNKPrIs2zDg5eD1OnCo+PbQXexX8YTBt0uqFx2Wm5iJ3Dpa6zCru4F3si92lVCUswNyfDeHHp7eGoGO9CpDGYi5HkBpf6cqOLS7F/rigqmIEtkF0clFbpOMj1ak=,iv:SLzttBfFg4lNjI/XpRbHokUcJTe4PrsGqqRi2Fz6UyY=,tag:1IbgK2p2S6V8bUWawLdY9Q==,type:str]
    unencrypted_suffix: _unencrypted
    version: 3.11.0

TerraformでSOPSプロバイダーの設定

TerraformでSOPSを扱えるterraform-provider-sopsプロバイダーを設定しましょう。sopsプロバイダーv1.3.0以降でEphemeral valuesに対応しています。 Terraformの設定ファイルにcarlpett/sopsプロバイダーを追加します。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.27.0"
    }
    sops = {
      source = "carlpett/sops"
      version = "= 1.3.0"
    }
  }
  required_version = "~> 1.14.0"
}

terraform initを実行すると、sopsプロバイダーがダウンロードされます。

$ cd foo
$ terraform init
... 省略
Initializing provider plugins...
- Finding carlpett/sops versions matching "1.3.0"...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing carlpett/sops v1.3.0...
- Installed carlpett/sops v1.3.0 (self-signed, key ID 1468AC14E6819667)
- Using previously-installed hashicorp/aws v6.27.0
... 省略

これでTerraform実行時に自動的にsopsで復号されます。

Terraformリソースの定義

暗号化したシークレットファイルsecrets.yamlから値を取得し、SSM Parameter Storeに登録するTerraformリソースを定義します。SOPSプロバイダーにv1.3.0で追加されたエフェメラルリソースephemeral "sops_file"を用いると、Stateファイルに値が保存されません。従来のdata "sops_file"リソースはStateファイルに値が保存されてしまうため避けましょう。

aws_ssm_parameterはEphemeral valuesに対応しているため、ephemeralリソースから取得した値をそのまま渡せます。今回はわかりやすいよう、secrets.yamlの内容を復号したものをそのままSSM Parameter Storeに登録します。

ephemeralリソースから取得した値はraw属性でアクセスできます。rawしかないので個別の値へのアクセスはAPIとして提供されていません。 aws_ssm_parameterにEphemeral valuesを渡すには、value_wo属性を使います。value_woは"write-only"の略で、この属性に渡した値はStateファイルに保存されません。値を変更したときはvalue_wo_versionも変更すると、Terraformが更新を検知します。

ephemeral "sops_file" "secrets" {
  source_file = "secrets.yaml"
}

resource "aws_ssm_parameter" "main" {
  name  = "sops_secrets"
  type  = "String"
  value_wo = ephemeral.sops_file.secrets.raw
  value_wo_version = 1
}

terraform planを実行して、問題ないことを確認します。

$ terraform plan

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ssm_parameter.main will be created
  + resource "aws_ssm_parameter" "main" {
      + arn              = (known after apply)
      + data_type        = (known after apply)
      + has_value_wo     = (known after apply)
      + id               = (known after apply)
      + insecure_value   = (known after apply)
      + key_id           = (known after apply)
      + name             = "sops_secrets"
      + region           = "ap-northeast-1"
      + tier             = (known after apply)
      + type             = "String"
      + value            = (sensitive value)
      + value_wo         = (write-only attribute)
      + value_wo_version = 1
      + version          = (known after apply)
    }

terraform applyすると、SSM Parameter Storeに機密情報が登録されます。値が、secrets.yamlの内容と同じであることを確認しましょう。

foo: thisisasecretvalue
bar: 123456789

SSM Parameterにsecrets.yamlの内容が入る

tfstateファイルを確認してみましょう。ephemeralリソースの値がvaluevalue_woに保存されていないことがわかります。valueは空文字列、value_woはnullと、機密情報が除外されています。これがEphemeral valuesの利点です。

{
  "mode": "managed",
  "type": "aws_ssm_parameter",
  "name": "main",
  "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
        "allowed_pattern": "",
        "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/sops_secrets",
        "data_type": "text",
        "description": "",
        "has_value_wo": true,
        "id": "sops_secrets",
        "insecure_value": null,
        "key_id": "",
        "name": "sops_secrets",
        "overwrite": null,
        "region": "ap-northeast-1",
        "tags": null,
        "tier": "Standard",
        "type": "String",
        "value": "",
        "value_wo": null,
        "value_wo_version": 1,
        "version": 1
      },
      "sensitive_attributes": [
        [
          {
            "type": "get_attr",
            "value": "value"
          }
        ],
        [
          {
            "type": "get_attr",
            "value": "value_wo"
          }
        ]
      ],
      "identity_schema_version": 0,
      "identity": {
        "account_id": "123456789012",
        "name": "sops_secrets",
        "region": "ap-northeast-1"
      },
      "private": "bnVsbA==",
      "dependencies": [
        "ephemeral.sops_file.secrets"
      ]
    }
  ]
}

個別の値を登録したい

terraform-provider-sopsのephemeralリソースはraw属性しか提供していないため、個別の値に直接アクセスできません。そこで、yamldecodejsondecode関数を使って、rawで取得したYAML/JSONコンテンツを分解します。

先ほどのsecrets.yamlは、YAMLフォーマットでfoobarのキーを持っています。それぞれを個別のSSM Parameterとして登録したい場合、yamldecode関数を使います。この方法なら、secrets.yamlに複数の値を持たせつつ、必要な値だけを個別のParameterとして登録できます。アプリケーション側で必要な値だけを取得できるので、アクセス権限が管理しやすくなります。

ephemeral "sops_file" "secrets" {
  source_file = "secrets.yaml"
}

resource "aws_ssm_parameter" "foo" {
  name  = "sops_foo"
  type  = "String"
  value_wo = yamldecode(ephemeral.sops_file.secrets.raw).foo
  value_wo_version = 1
}

resource "aws_ssm_parameter" "bar" {
  name  = "sops_bar"
  type  = "String"
  value_wo = yamldecode(ephemeral.sops_file.secrets.raw).bar
  value_wo_version = 1
}

terraform applyを実行すると、SSM Parameter Storeに個別の値が登録されます。

sops_fooには、secrets.yamlのfooの値が入っています。

fooの値が入っていることが確認できる

sops_barには、secrets.yamlのbarの値が入っています。

barの値が入っていることが確認できる

value_wo_versionを自動化する

value_wo_version属性は、値を変更したときに更新を検知するために使います。value_wo_versionを手動で管理していると、値を更新したときにバージョンを上げ忘れることがあります。私は一度やらかしたので、自動化を推奨します。

値が変更されたら自動的にversionも変わるように、ハッシュ値を使った自動化が良いでしょう。ただ、次のように復号した値ハッシュ計算に使おうとすると実行時怒られます。

locals {
  sops_secrets = yamldecode(ephemeral.sops_file.secrets.raw)
  sops_secrets_hash = sha256(local.sops_secrets) # これはダメ
}

復号した値を計算式に直接渡すことはできないため、シークレットファイル全体のハッシュ値を用います。シークレットを更新するときにファイルハッシュが書き変わります。SSM Parameter Storeに登録されている値を更新しても特に影響がないので、対象のシークレット以外もTerraformで差分検出されても大きな問題はありません。

ephemeral "sops_file" "secrets" {
  source_file = local.sops_secrets_path
}

locals {
  sops_secrets_path = "secrets.yaml"
  sops_secrets_hash = substr(filesha256(local.sops_secrets_path), 0, 8)
}

resource "aws_ssm_parameter" "main" {
  name  = "sops_secrets"
  type  = "String"
  value_wo = local.sops_secrets
  value_wo_version = local.sops_secrets_hash
}

resource "aws_ssm_parameter" "foo" {
  name  = "sops_foo"
  type  = "String"
  value_wo = local.sops_secrets.foo
  value_wo_version = local.sops_secrets_hash
}

resource "aws_ssm_parameter" "bar" {
  name  = "sops_bar"
  type  = "String"
  value_wo = local.sops_secrets.bar
  value_wo_version = local.sops_secrets_hash
}

シークレットファイルのハッシュを使う方法の注意点

1つの値を更新したり、KMSキーのローテーションでもシークレットファイルは書き変わり、全シークレットが更新されます。これが気になる場合、個別にephemeralリソースを作成して分割する方法もあります。シークレットファイルが多数増えるのは運用しにくいのでオススメしにくいですが、要件次第ではよいでしょう。

あるいは、あきらめてvalue_wo_versionを手動で管理する方法もあります。Copilot Agentを使って、シークレットの更新に合わせてバージョンも更新させるのは悪くないでしょう。

運用上の注意

この方法の鍵はAWS KMSのアクセス権限管理です。KMSキーのkms:Decryptkms:Encryptにアクセスできるユーザー・ロールが、SOPSで暗号化されたファイルを閲覧、操作できるユーザーです。もちろんTerraform実行環境もKMSキーにアクセスできる必要があります。

また、Ephemeral valuesはStateに残りませんが、Stateファイル自体の管理は引き続き注意が必要です。構成が丸見えになるため、Stateファイルの保存先やアクセス権限管理は適切に行いましょう。

まとめ

terraformにおけるシークレット管理は、Ephemeral valuesの登場で大きく変わりました。terraform-sops-providerがEphemeral resourceに対応したことで、SOPSで暗号化されたファイルをコードベースで管理しつつ、TerraformのStateファイルに平文で保存しない構成がとれます。

インフラのコード化を進める上で、シークレット管理は避けて通れない課題です。今回の方法は、万が一Gitリポジトリが漏洩しても、KMSキーで保護されたシークレットファイルが守られるため、安全にコードベースでシークレットを管理できます。

私もこの構成をとってしばらく経ちますが、運用しやすくいい感じです。

参考

GitHub


  1. Secrets Managerも同様ですが、簡単のためSSM Parameter Storeを使います。