Terraform 1.11 の Write-only Arguments と HCP Vault Secret で Terraform の状態ファイルから機密情報を排除

:::note alert
[2025/06/21 追記]

HCP Vault Secret の廃止がアナウンスされていますので、この記事の方式は**今後利用できません。**

https://support.hashicorp.com/hc/en-us/articles/41802449287955-HCP-Vault-Secrets-End-Of-Life
:::

[2025/03/06 追記] terraform 1.11.1 でバグ修正があり、variable ブロックの定義に `sensitive = true` と `ephemeral = true` が同時に指定できるようになったため修正しました

## はじめに

terraform でパスワード等の機密情報を扱う場合、[state](https://developer.hashicorp.com/terraform/language/state) に情報が載ってしまうとセキュリティリスクに繋がるケースがあります。

terraform 1.10 で GA になった Ephemeral values を利用し、HashiCorp Vault 上の機密情報を安全に一元管理した上で環境構築できると嬉しいのですが、現在は vault provider が ephemeral resource に対応しておらず、 state に情報が載らないようにするのは 2025/02/28 時点では不可能な状態です。

一方、terraform 1.11 では Write-only Arguments がリソースブロックで利用可能になりました。

https://developer.hashicorp.com/terraform/plugin/framework/resources/write-only-arguments

これを使用できるリソースを利用すれば、HCP Vault Secret から同期した Variable を Write-only Arguments をサポートしたリソースに流し込むパターンであれば state に記録されなくなります。

今回は state を記録されないコードの例を示します。

## 構成例

HCP Vault Secret には、HCP Terraform に Secret を同期する仕組みがあります。

https://developer.hashicorp.com/hcp/docs/vault-secrets/integrations/hcp-terraform

この仕組みで同期し作成された Variable の値を Write-only Arguments に書き込む場合は state に Variable 値が書かれませんので、「Vault で安全に管理された機密情報が state に書き込まれず環境に反映される」という構成をとることができます。
実際に AWS Systems Manager パラメータストアを作成する場合のサンプルコードで確認してみましょう。

## サンプルコード

AWS Systems Manager パラメータストアを作成する場合、[`aws_ssm_parameter` リソース](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter)を使用します。

従来は `value` 属性でパラメータ値を設定する必要があり、この値は state に書かれていました。
これを `value_wo` 属性でパラメータ値を設定する形にすれば、state に書かれなくなります。

```hcl:main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.87.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

locals {
  token_version = 1
}

resource "aws_ssm_parameter" "wo_token" {
  name             = "test_param"
  type             = "SecureString"
  value_wo         = var.token
  value_wo_version = local.token_version
}
```

```hcl:variables.tf
variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "token" {
  type      = string
  sensitive = true
  ephemeral = true # 追記: terraform 1.11.1 以降は指定可能
}
```

`value_wo_version` 属性は `value_wo` 属性のバージョンを示し、この値が変化したら SSM Parameter の変更が実施されます。
state に記録されなくなることで設定した値が変更されたかどうか terraform 的に判断できなくなるので、別の属性で判断するということですね。

~~Variable には `ephemeral = true` を指定したいところですが、HCP Terraform 上で Sensitive な Variable を使って動かすと下記のエラーが出るので `sensitive = true` の指定のみとしました。
現時点では ephemeral と sensitive をどちらも指定することが想定されていないようです。~~

```
Error: failed to encode aws_ssm_parameter.wo_token in state: .value_wo: cannot serialize value marked as cty.NewValueMarks(marks.Ephemeral, marks.Sensitive) for inclusion in a state snapshot (this is a bug in Terraform)
Error: Changes.Encode: new value .value_wo: can't serialize value marked with cty.NewValueMarks(marks.Ephemeral, marks.Sensitive) (this is a bug in Terraform)
```

~~どちらも指定可能になるか、HCP Terraform の Workspace Variable で ephemeral の指定ができるようになることで解消する理解なので、今後のアップデートに期待ですね。~~

**(2025/03/06 追記)**
このエラーについて [Issue](https://github.com/hashicorp/terraform/issues/36603) を作成したところ、v1.11.1 で修正いただきました！

HCP Vault Secret から同期される Variable は Sensitive になるので、`sensitive = true` を指定します。
また、この値は state に記録されてほしくないので、 `ephemeral = true` も指定しておきます。

## Workspace の作成

上記のコードを動かす Workspace を作成します。
使用する terraform のバージョンは 1.11 以降を指定するのを忘れないようにしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/2b5ebde4-8094-4ffa-a356-24dde0915007.png)

AWS に対する操作が必要なので、aws provider 用に AWS アクセスキー、シークレットアクセスキーの Variable を設定するか、dynamic credentials の設定を実施して plan 実行可能にしておきます。

## HCP Vault Secret に Secret を作成

下記画像のように HCP Vault Secret に Secret を作成します。
今回は変数 `token` の値を Vault から設定したいので、Name には `token` を指定します。

![スクリーンショット 2025-02-23 145220.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/98d91ed6-c310-414c-a573-8f353edc738d.png)

## HCP Vault Secret からのシークレット値の同期

あとは `token` 変数を Vault から指定するようにすれば OK ですね。

下記に記述がある通り設定を進め、HCP Vault Secret に定義した Secret を HCP Terraform の Workspace Variables として同期しておきます。

https://developer.hashicorp.com/hcp/docs/vault-secrets/integrations/hcp-terraform

HCP Terraform で Organization Token または Team Token を作成し、そのトークンを指定することで実現できます。
今回は Free 版の HCP Terraform を使用しており Team が使用できないため、Organization Token を使用して連携してみました。

同期できれば、下記のように Workspace variables の一覧に追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/7f882c89-d7bd-4dff-a922-9fc9a684de58.png)

## 動作確認

ここまでの構成を実施することで、

- トップレベルの Variable 属性は state に記録されない
- aws_ssm_parameter リソースに指定した機密情報は state に記録されない
  となるので、state に機密情報が載らない想定です。
  動作を確認していきましょう。

### state に記録されないことの確認

Workspace で apply してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/4a94554d-097d-459d-a3e8-935e2a7f92da.png)

state には機密情報が見当りません！

```json
{
  ~ 略 ~
  "resources": [
    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "wo_token",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:xxxx:parameter/test_param",
            "data_type": "text",
            "description": "",
            "has_value_wo": true,
            "id": "test_param",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "test_param",
            "overwrite": null,
            "tags": null,
            "tags_all": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "",
            "value_wo": null,
            "value_wo_version": 1,
            "version": 1
          },
          ~ 略 ~
}
```

しかしパラメータストアを確認すると、HCP Vault Secret に登録したトークン: `this_is_sample_token` が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/ea65d79f-594c-4f72-aee5-e3c4e89f35ad.png)

### 値の更新に関する確認

HCP Vault Secret の値を変えて同期しただけだと No changes となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/dbf83a10-0c72-4e5d-8f1e-8869227bf9ee.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/5a156dd1-61b0-443b-b280-dae86e7590c1.png)

value_wo_version の値を更新してみましょう。

```hcl
~ 略 ~

locals {
  token_version = 2
}

~ 略 ~
```

すると無事 Apply でき、パラメータストアで変更が確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/390c8ea0-1e45-4ce9-a70f-24ace9ecfccd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/e5342de4-83fa-463e-975d-01dbe564ac80.png)

## Secret の変更があった際のバージョンの値は自動的に更新できるか ？

実験として Secret の変更を検知して `value_wo_version` の値を自動的に変更する方法として下記を考えました。
`value_wo_version` に設定する値を [`time_static` リソース](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/static) の結果とし、`time_static` の `keepers` に機密情報のハッシュ値を指定する方法です。

```hcl
variable "token" {
  type      = string
  sensitive = true
  # ephemeral = true  ## ephemeral を指定すると random_integer の keepers に指定できない
}

resource "time_static" "token_update" {
  triggers = {
    # トークンのハッシュ値を使用してトリガー
    token_hash = sha256(var.token)
  }
}

resource "aws_ssm_parameter" "wo_token" {
  name             = "test_param"
  type             = "SecureString"
  value_wo         = var.token
  value_wo_version = time_static.token_update.unix
}
```

`time_static` リソースの Output: `unix` は現在日時の UNIX 時間なので、今回のような使用方法であれば重複するリスクもなく更新できる認識です。
state には `time_static` リソースの `keepers` 属性に指定した機密情報のハッシュ値しか記録されないため、state の安全性もある程度確保されます。

ただ、variable の値を random_integer の keepers に指定できるようにするため variable に `ephemeral` を指定できなくなるので、もし **variable が意図しない場所で参照されると state に機密情報が記録されてしまうリスク** があります。
この実装の採用を検討する場合は十分注意してください。

また、terraform のコード変更ではなく Vault の値の変更がきっかけで修正が実施されるので、 **意図しないタイミングで terraform の plan 結果に差分が発生する** 可能性があります。
これは CI/CD パイプラインでの自動デプロイ時の予期せぬ変更や、コードレビューの複雑化など、運用上不都合になると思われますので、ローカル変数等でバージョンを持っておく形が良いケースが多いのではないかと思われます。