[Terraform] 誤解されがちなignore_changesの動き・機密情報はstateに保持されるのか?

自分も誤解してました。とりあえずignore_changesしたから安心するなかれ
2022.09.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

どうも、ちゃだいん(@chazuke4649)です。

「なんかいい感じに無視してくれるignore_changes、正しい動きを理解してる?雰囲気で理解してることない?」

詳しい人から教えてもらってめっちゃ驚いたのでブログに書きます。

先にまとめ

  • ignore_changesはあくまでTerraformコードによるstateの変更を無視するだけ。state上の値が以後変更されないことを保証するものではない
  • 現にその値がTerraform外で変更された場合、terraform applyによってstateの値は実体に合わせ更新されうる
  • ignore_changesに指定した場合の各AWSリソースの動きの違い
    • SSM Parameter Store は手動で変更したらstateは更新された
    • Secrets Manager は、ローテーションしたらstateは更新されなかった
    • RDS のパスワードは、手動でパスワード変更した後は更新されなかった

ignore_changesの正しい理解

こんなイメージです。

もう少し詳細に書くとさらにこんなイメージです。

繰り返しになりますが、 ignore_changes はあくまでTerraformコード(.tf)によるstateファイルの変更を無視するのみであり、登録された値がstateファイル上で以後変更されないことを保証するものでないようです。

つまり、Terraformコードで作成したあるリソースのある属性を ignore_changes で指定したとしても、stateファイル側は実際のリソースの変更された値を照合され、terraform apply(or terraform refresh) により上書きされる可能性が高いです。

ignore_changes 機能は、将来変更される可能性があるが、作成後のリソースには影響しないデータへの参照でリソースが作成される場合に使用されることを意図しています。稀にTerraform以外のプロセスでリモートオブジェクトの設定が変更され、Terraformが次の実行時に「修正」しようとすることがあります。Terraformが1つのオブジェクトの管理責任を別プロセスと共有するために、ignore_changesメタ引数で、関連するリモートオブジェクトの更新を計画する際にTerraformが無視すべきリソース属性を指定します。与えられた属性名に対応する引数は、作成操作の計画時に考慮されますが、更新の計画時には無視されます。

ignore_changes | Terraform by HashiCorp

また、以下記事は"Objects have change outside of Terraform"レポート機能に関するものですが、最後の方に ignore_changes に関する注意事項も書かれています。

この注意点は、ignore_changesがリモートシステムから入ってくる変更を無視するためのものではないことを明確にするためのものです。ignore_changes引数は、必要な新しいアクションを計画するために、Terraformの設定と状態を比較するTerraformの動作を無効にします。Objects have change outside of Terraform」は、事前の状態とリモートオブジェクトの差異を報告しています。

New Feature: "Objects have changed outside of Terraform" – HashiCorp Help Center

図にして、黄色(ignore_changesが影響する部分)と紫(Objects have change outside of Terraformが報告する部分)のように分けるとわかりやすいかもしれません。

Objects have change outside of Terraform について

ちょっと話はそれますが、この新機能の説明は、そもそものTerraformの仕様を理解する上で参考になります。
この図の紫部分の話です。

Terraform Version 0.15.4から、リモートオブジェクトの変更を検出した後にステートファイルに行われた更新を示す新しいレポートが追加されました。これは、Terraformが歴史的にバックグラウンドで静かにリフレッシュを行うために設計されているタスクです。この新しい機能により、この情報がユーザーの目に留まるようになりました。リモートシステムに合わせて状態がどのように変化しているかを確認することは重要です。この機能は、これまで隠されていた知識のギャップを解決し、悪影響を回避することができます。ユーザーは、予想されるリモートシステムの変化を確認したり、予想外の変化をキャッチしたりする機会を得ることができるようになりました。

New Feature: "Objects have changed outside of Terraform" – HashiCorp Help Center

つまり、もともとstateファイルはターミナル上にレポートがなくても、apply/refresh するタイミングに、裏側ではAWSリソースの最新情報をstateファイルへ取り込み更新していたのです。

考えてみると当たり前の挙動かもしれません。Terraformのソースコード側ではデフォルト値を持つ任意の属性として記述をはしょってたとしても、state側でそれを持っておかないと、新たにソースコード側で異なる設定を追加した際に、それを変更する計画が取れません。

ちなみに、この機能には更新があり、0.15.4-1.1.10までは「stateファイルの全ての変更」をレポートしていましたが、大規模なワークロードの場合そのレポート量が膨大すぎて逆に迷惑な場合があるとされたため、1.2.0 にて「現在ソースコード側の更新計画に関係している変更のみ」をレポートするようになったようです。

動きを確認してみる

概要

  • Terraform v1.2.8
  • AWS Provider v4.28.0

今回調査する対象は、「stateファイルに機密情報を保持しうるリソース」とします。

ちなみに、これらに関連して公式ドキュメントでは「stateファイルに機密情報を管理する場合はstate自体を機密情報として扱ってね」と書いてあります。

State: Sensitive Data | Terraform by HashiCorp

SSM Parameter Store

aws_ssm_parameter | Resources | hashicorp/aws | Terraform Registry

まず、以下のTerraformコードを適用します。

ssm.tf

resource "aws_ssm_parameter" "password" {
  name        = "/temp/password"
  type        = "SecureString"
  value       = "DUMMY"
}

現在のstateファイルを参照すると以下のようになりました。

temp.tfstate

    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "password",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:111122223333:parameter/temp/password",
            "data_type": "text",
            "description": "",
            "id": "/temp/password",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "/temp/password",
            "overwrite": null,
            "tags": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "DUMMY",
            "version": 1
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }

ignore_changesを追加し適用します。

ssm.tf

resource "aws_ssm_parameter" "password" {
  name        = "/temp/password"
  type        = "SecureString"
  value       = "DUMMY"
  lifecycle {
    ignore_changes = [
      value
    ]
  }
}

それでは手動でコンソールから値を変更します。

DUMMYからCHANGEDに変更しました。

terraform planを実行します。

% terraform plan
aws_ssm_parameter.password: Refreshing state... [id=/temp/password]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

ignore_changesにvalueを入れているので、変更が検出されませんでした。

また、plan段階ではstateは変更されていません。

% terraform state pull |grep value
            "value": "DUMMY",

そのまま、terraform applyを実行します。

% terraform apply -auto-approve
aws_ssm_parameter.password: Refreshing state... [id=/temp/password]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no
differences, so no changes are needed.

Terraformコードによる変更は反映されませんでした。ignore_changesが正しく機能していると言えます。

そして、stateファイルを再度見に行くと、ご覧の通りstateファイルではCHANGEDに変更されています。

% terraform state pull |grep value
            "value": "CHANGED",

ちなみに、stateファイル全体は以下です。

temp.tfstate

    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "password",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:111122223333:parameter/temp/password",
            "data_type": "text",
            "description": "",
            "id": "/temp/password",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "/temp/password",
            "overwrite": null,
            "tags": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "CHANGED",
            "version": 2
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }

value以外にもversionも実体に合わせて更新されています。

このように、基本的にはTerraformコードに明示的に定義している/いないに関わらず、stateファイルに保持している値は、適用のタイミングで更新されうる、と考えておくのが良さそうです。  

そして、今回の場合Parameter Store でパスワードなどを管理する場合は、ignore_changesに指定していても、stateファイルには最新情報が保持される、と考えて良さそうです。

ただし、他AWSサービスでいくつか例外がありました。

Secrets Manager

aws_secretsmanager_secret | Resources | hashicorp/aws | Terraform Registry

aws_secretsmanager_secret_version | Resources | hashicorp/aws | Terraform Registry

詳細な手順はSSMとほぼ同等のため割愛しますが、以下コードで同等のことを行いました。

ちなみに、SecretsManagerのシークレット本体は、バージョンと合わせてaws_secretsmanager_secret_versionリソースブロックで定義します。

secretsmanager.tf

resource "aws_secretsmanager_secret" "example" {
  name = "example"
}

resource "aws_secretsmanager_secret_version" "example" {
  secret_id     = aws_secretsmanager_secret.example.id
  secret_string = "APPLE_JUICE"
  lifecycle {
    ignore_changes = [
      secret_string, version_stages
    ]
  }
}

※最初に適用した段階では、ignore_changesは記述しておらず、適用後に追加しています。

SSMと同様にコンソールで変更したのちに、plan/applyを行ったところ、stateファイルは更新されていませんでした。

% terraform state pull |grep secret_string
            "secret_string": "APPLE_JUICE",

この挙動を説明するドキュメント該当箇所は見つけられていません。(見つけた方はぜひ教えてください!)

考えるに、Secrets Managerは機密情報管理に特化したサービスであり、AWS側かTerraform側の仕様として、状態管理する上でignore_changesのような手法を取れば、シークレット自体をstateに保持しないで済むよう考慮して設計されていると思われます。

なので、Secrets Managerの場合は、これら手法をとれば、stateファイルに保持されない、と考えて良さそうです。

RDS MySQL

RDSのPasswordも同様に調査しました。

aws_db_instance | Resources | hashicorp/aws | Terraform Registry

Terraformコードは以下です。

rds.tf

resource "aws_db_instance" "sample" {
  allocated_storage = 10
  engine            = "mysql"
  engine_version    = "5.7"
  instance_class    = "db.t3.micro"
  db_name           = "mydb"
  username          = "admin"
  password             = "FIRST_PASSWORD"
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot  = true
  db_subnet_group_name = "sample-vpc"

  lifecycle {
    ignore_changes = [
      password
    ]
  }
}

※最初に適用した段階では、ignore_changesは記述しておらず、適用後に追加しています。

上記を適用後、EC2経由でMySQLに接続しパスワードを SECOND_PASSWORDに変更しました。

その後、plan/applyを行ったところ、stateファイルは更新されませんでした。

この挙動は、RDSのパスワードはコンソールから更新することはできますが、現在のパスワードが何か参照することはできません。これによりTerraformが実体に合わせる動きをしたくてもパスワードはgetできないので、知りようがなく、最新情報に更新できないものと考えられます。

よって、RDSはTerraformコードで指定した初期パスワードから変更しignore_changesに指定すれば、以降はstateに最新のパスワードを保持せずに済みそうです。

調査としては以上です。

ignore_changesのユースケース

これらを踏まえて改めてignore_changesのユースケースを考えてました。

○: 実体を正としたい

例えば、以下のように「この値は常にTerraformコードの設定を保つ必要がなく、実体を正としたい」といった場合が最も適したユースケースと言えそうです。

  • AutoScalingGroupのEC2の起動数
    • 運用開始後は自由にスケールしてほしい
  • リソースへのタグ付け
    • 他要因によって付けられたタグをTerraformで削除してほしくない

△: 機密情報をstateファイルに保持したくない

今回は、主にこちらを想定したAWSサービスの検証を行いましたが、実現できるかどうかはサービスに依ります。

  • RDS、Secrets Managerは初期構築後に変更すれば保持せずに良さそう
  • SSM Parameter Storeはstateが常に最新情報に更新され保持されるので、NG

補足)各AWSサービス単位でどういった挙動になるか確認するポイント

stateファイルを見る

terraform state pull > temp.tfstate などで実際にstateファイルの中身を参照し、対象の値がstateファイルに含まれているかどうかを確認します。

各リソース単位でTerraform公式ドキュメントを読む

例えば、SSM Parameter Storeや以下リソースには注意書きがあります。

Note:The unencrypted value of a SecureString will be stored in the raw state as plain-text. Read more about sensitive data in state.

aws_ssm_parameter | Resources | hashicorp/aws | Terraform Registry

Note:All arguments including the username and password will be stored in the raw state as plain-text. Read more about sensitive data in state.

aws_db_instance | Resources | hashicorp/aws | Terraform Registry

Note:All arguments including the username and password will be stored in the raw state as plain-text. Read more about sensitive data in state.

aws_rds_cluster | Resources | hashicorp/aws | Terraform Registry

Note:All arguments including the plaintext be stored in the raw state as plain-text. Read more about sensitive data in state.

aws_kms_ciphertext | Resources | hashicorp/aws | Terraform Registry

一方で、Secrets Managerの場合だと、同様の注意書きが見られません。

aws_secretsmanager_secret | Resources | hashicorp/aws | Terraform Registry

このようにユーザーで取り扱いに注意してほしい値がある場合、ドキュメント側にあらかじめ注意喚起されていることがあります。

実際に試してみる

今回検証した通り、AWSリソースによって挙動が変わりうるので、最終的には手を動かして確認するのが間違いなさそうです。

終わりに

ignore_changesの動きを調査しました。自分は勘違いしてましたが、同様の勘違いをしている人がいる気がしたのでブログにまとめてみました。お役に立てば幸いです。

それでは今日はこの辺で。ちゃだいん(@chazuke4649)でした。

その他参考情報

セキュアなTerraformの使い方 ~ 機密情報をコードに含めず環境構築するにはどうしたらいいの? - Speaker Deck

Terraform で秘密情報を扱う – もばらぶエンジニアブログ

Terraform × パラメータストアでRDSの機密情報をセキュアに扱う - Qiita

state | Terraform by HashiCorp