Terraform のバージョンをアップグレードしようとしたら謎の差分に悩まされた話

※この記事は[ミクシィグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mixi)の11日目の記事です

先日弊プロジェクトで作成している Terraform コードを、最新の 1.0.11 にアップグレードしました。
その際に、バージョンを上げようとすると謎の差分が発生し、どういうこっちゃ？ということで調べた時のメモ。

# 要約
* Terraform コードを 0.14.11 -> 1.0.11 にアップグレードしようと plan を実行したところ、謎の差分が発生
* 調べると、 AWS Security Manager を使用して設定した値で差分が発生している
* どうも 0.15 以降は provider_sensitive_attrs の機能があり、sensitive な値を変数に入れて使うと、使っている resource の値も sensitive 扱いになってくれるらしい
* sensitive の扱いが変更されると、物によっては Terraform はそれを resource の差分として認識する（っぽい）
* というわけでアップグレードに伴って変更される resource はなかったので、みんな幸せ

# 発端
事の起こりは Terraform をアップグレードしようと Terraform の required_version を変更しただけの簡単な PR を作成した時に発生。

![スクリーンショット 2021-12-09 13.57.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69711/11e10452-d498-49b2-5ed6-36a7f0e3531e.png)

CodeBuild で実行した terraform plan 結果に謎の差分が発生。
差分の発生箇所は AWS WAF で設定している rule。
そして肝心の差分の内容は、

```diff_hcl
      ~ rule {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
```

sensitive だから差分は見せられないときてる。
さて困った :thinking: 

# 調査
なんとか差分を調べる方法がないかと思ってググってみると、以下のフォーラムの投稿が見つかる。
https://discuss.hashicorp.com/t/how-to-show-sensitive-values/24076

それによると、下記のコマンドを実行すれば詳細な plan の実行計画を json として吐き出すことができて、差分を確認できるとのこと。

```bash
terraform plan -out=tfplan
terraform show -json tfplan
```

json の中身は以下の公式のドキュメントで詳しく説明されている。
https://www.terraform.io/docs/internals/json-format.html

これによると、 terraform plan で出力した時の構成は大まかには以下の通り。

* 現在 Terraform で管理している resources の状態（Values Representation）
* 現在の Terraform の設定（Configuration Representation）
* 今回の変更点（Change Representation）

この中の Change Representation の中に、before / after という項目でそれぞれ変更点が詳細に記述される。
今回の差分はここの before と after を見比べれば良さそう。
なので、before と after を抜き出して diff をとってみる。

```diff
14c14
<         "before": {
---
>         "after": {
37c37
<                       "custom_request_handling": []
---
>                       "custom_request_handling": null
116c116
<         "before_sensitive": {
---
>         "after_sensitive": {
132,134c132
<                     {
<                       "custom_request_handling": []
<                     }
---
>                     {}
187c185
<         },
---
>         }
203c201
<         "before": {
---
>         "after": {
305c303,304
<         "before_sensitive": {
---
>         "after_unknown": {},
>         "after_sensitive": {
316,368c315
<           "rule": [
<             {
<               "action": [
<                 {
<                   "allow": [
<                     {}
<                   ],
<                   "block": [],
<                   "count": []
<                 }
<               ],
<               "override_action": [],
<               "statement": [
<                 {
<                   "and_statement": [],
<                   "byte_match_statement": [
<                     {
<                       "field_to_match": [
<                         {
<                           "all_query_arguments": [],
<                           "body": [],
<                           "method": [],
<                           "query_string": [],
<                           "single_header": [
<                             {}
<                           ],
<                           "single_query_argument": [],
<                           "uri_path": []
<                         }
<                       ],
<                       "text_transformation": [
<                         {}
<                       ]
<                     }
<                   ],
<                   "geo_match_statement": [],
<                   "ip_set_reference_statement": [],
<                   "managed_rule_group_statement": [],
<                   "not_statement": [],
<                   "or_statement": [],
<                   "rate_based_statement": [],
<                   "regex_pattern_set_reference_statement": [],
<                   "rule_group_reference_statement": [],
<                   "size_constraint_statement": [],
<                   "sqli_match_statement": [],
<                   "xss_match_statement": []
<                 }
<               ],
<               "visibility_config": [
<                 {}
<               ]
<             }
<           ],
---
>           "rule": true,
374c321
<         },
---
>         }
```

これをみると、before_sensitive にある WAF の rule がまるっと after_sensitive で true に置き換わっていることがわかる。
before/after_sensive については、

```hcl
  // "before_sensitive" and "after_sensitive" are object values with similar
  // structure to "before" and "after", but with all sensitive leaf values
  // replaced with true, and all non-sensitive leaf values omitted. 
```

とあり、どうやら sensitive 扱いになるパラメータは、すべて true という値になる模様。
つまり、AWS WAF の rule がまるっと sensitive 扱いされたということがわかる。

はて、 rule に sensitive なデータなんかあったっけ…？ :thinking: と思ったのだが、ここで rule に、AWS Secret Manager で取得した値を入れていたことを思い出す。

今回のシステムは、固定の値をリクエストヘッダーにもつシステムからのみを許可する WAF の rule を設定しており、その固定の値を Secret Manager で管理していたのでした。

# 0.15 で導入された provider_sensitive_attrs
となると、アップグレードに伴って sensitive value の扱いが変わったのかしら？と思って調べると、 0.15 で provider_sensitive_attrs というのが GA になったことがわかる。
https://dev.classmethod.jp/articles/terraform-015/#toc-4

provider_sensitive_attrs というのは、ざっくりいうと sensitive な値を使っている resource も同様に sensitive 扱いにするよ！という機能。
元々は sensitive な値を変数に入れて使いまわしていたりすると、 terraform plan とかで sensitive の値がプレーンテキストとして表示されちゃうという問題があり、それを解決するために導入された機能とのこと。

Security Manager の値は元々 sensitive 扱いなので、これにより WAF の rule がまるっと sensitive 扱いにされたことがわかる。

# resource の変更？
実は、通常は sensitive 扱いの変更だけなら、下記のような `The value is unchanged.` 表示が出るようになっているらしい。

```diff_hcl
      # Warning: this block will be marked as sensitive and will not
      # display in UI output after applying this change. The value is unchanged. <= ココ！
      ~ rule {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
```

この文字が出ていれば値は変更されないことがわかるのだが、今回はこれが表示されていなかった。
どうも resource によっては [`The value is unchanged.` の文字が表示されないことがある](https://github.com/hashicorp/terraform/issues/28839#issuecomment-852376920)っぽい？

# 結局どうしたか
`The value is unchanged.` の表示がない上に、今回の環境が production 環境だったので、エイヤッと apply するのはさすがにちょっと怖かった。
そのため、今回は問題となる WAF と Secret Manager をだけを作成する簡単な Terraform を作成し、実際にどうなるか検証した。

## 検証
まずは 0.14.11 で作成。

```hcl:main.tf
terraform {
  required_version = "~> 0.14.11"
}

provider "aws" {
  profile = "test"
  region  = "ap-northeast-1"
}

provider "aws" {
  profile = "test"
  region  = "us-east-1"
  alias   = "virginia"
}

data "aws_secretsmanager_secret" "secret" {
  name = "test"
}

data "aws_secretsmanager_secret_version" "secret" {
  secret_id = data.aws_secretsmanager_secret.secret.id
}

resource "aws_wafv2_web_acl" "waf" {
  provider    = aws.virginia
  name        = "test-waf-allow-header"
  scope       = "CLOUDFRONT"
  description = "test"
  tags        = {}

  visibility_config {
    cloudwatch_metrics_enabled = false
    metric_name                = "test-waf-allow-header"
    sampled_requests_enabled   = false
  }

  default_action {
    block {}
  }

  rule {
    name     = "test-waf-rule-allow-header"
    priority = 1

    visibility_config {
      cloudwatch_metrics_enabled = false
      metric_name                = "test-waf-rule-allow-header"
      sampled_requests_enabled   = false
    }

    action {
      allow {}
    }

    statement {
      byte_match_statement {
        field_to_match {
          single_header {
            name = "x-cdn-key"
          }
        }
        positional_constraint = "EXACTLY"
        search_string         = jsondecode(data.aws_secretsmanager_secret_version.secret.secret_string)["x-cdn-key"]
        text_transformation {
          priority = 0
          type     = "NONE"
        }
      }
    }
  }
}
```

作った main.tf を apply。

```shell
 % terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_wafv2_web_acl.waf will be created
  + resource "aws_wafv2_web_acl" "waf" {
      + arn         = (known after apply)
      + capacity    = (known after apply)
      + description = "test"
      + id          = (known after apply)
      + lock_token  = (known after apply)
      + name        = "test-waf-allow-header"
      + scope       = "CLOUDFRONT"
      + tags_all    = (known after apply)

      + default_action {

          + block {
            }
        }

      + rule {
          + name     = "test-waf-rule-allow-header"
          + priority = 1

          + action {
              + allow {
                }
            }

          + statement {

              + byte_match_statement {
                  + positional_constraint = "EXACTLY"
                  + search_string         = "hogehoge"

                  + field_to_match {

                      + single_header {
                          + name = "x-cdn-key"
                        }
                    }

                  + text_transformation {
                      + priority = 0
                      + type     = "NONE"
                    }
                }
            }

          + visibility_config {
              + cloudwatch_metrics_enabled = false
              + metric_name                = "test-waf-rule-allow-header"
              + sampled_requests_enabled   = false
            }
        }

      + visibility_config {
          + cloudwatch_metrics_enabled = false
          + metric_name                = "test-waf-allow-header"
          + sampled_requests_enabled   = false
        }
    }

Plan: 1 to add, 0 to change, 0 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: yes

aws_wafv2_web_acl.waf: Creating...
aws_wafv2_web_acl.waf: Creation complete after 2s [id=ebd18307-4665-4bfa-8807-a53ae9df4779]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

この時に WAF に設定された sensitive な値は以下。

```
                  + search_string         = "hogehoge"
```


required_version を変更する。

```diff
diff --git a/main.tf b/main.tf
index 699ddc8..e8af984 100644
--- a/main.tf
+++ b/main.tf
@@ -1,5 +1,5 @@
 terraform {
-  required_version = "~> 0.14.11"
+  required_version = "~> 1.0.11"
 }

 provider "aws" {
```

そして terraform plan。

```
 % terraform plan
aws_wafv2_web_acl.waf: Refreshing state... [id=ebd18307-4665-4bfa-8807-a53ae9df4779]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":

  # aws_wafv2_web_acl.waf has been changed
  ~ resource "aws_wafv2_web_acl" "waf" {
        id          = "ebd18307-4665-4bfa-8807-a53ae9df4779"
        name        = "test-waf-allow-header"
      + tags        = {}
        # (6 unchanged attributes hidden)



        # (3 unchanged blocks hidden)
    }

Unless you have made equivalent changes to your configuration, or ignored the relevant attributes
using ignore_changes, the following plan may include actions to undo or respond to these changes.

────────────────────────────────────────────────────────────────────────────────────────────────────

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_wafv2_web_acl.waf will be updated in-place
  ~ resource "aws_wafv2_web_acl" "waf" {
        id          = "ebd18307-4665-4bfa-8807-a53ae9df4779"
        name        = "test-waf-allow-header"
        tags        = {}
        # (6 unchanged attributes hidden)


      # Warning: this block will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ rule {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

        # (2 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly
these actions if you run "terraform apply" now.
```

やはり `The value is unchanged.` の表記はない。
このまま apply すると値はどうなるか確認する。

```
 % terraform apply                                                                          [ main ]
aws_wafv2_web_acl.waf: Refreshing state... [id=ebd18307-4665-4bfa-8807-a53ae9df4779]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":

  # aws_wafv2_web_acl.waf has been changed
  ~ resource "aws_wafv2_web_acl" "waf" {
        id          = "ebd18307-4665-4bfa-8807-a53ae9df4779"
        name        = "test-waf-allow-header"
      + tags        = {}
        # (6 unchanged attributes hidden)



        # (3 unchanged blocks hidden)
    }

Unless you have made equivalent changes to your configuration, or ignored the relevant attributes
using ignore_changes, the following plan may include actions to undo or respond to these changes.

────────────────────────────────────────────────────────────────────────────────────────────────────

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_wafv2_web_acl.waf will be updated in-place
  ~ resource "aws_wafv2_web_acl" "waf" {
        id          = "ebd18307-4665-4bfa-8807-a53ae9df4779"
        name        = "test-waf-allow-header"
        tags        = {}
        # (6 unchanged attributes hidden)


      # Warning: this block will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ rule {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

        # (2 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 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: yes

aws_wafv2_web_acl.waf: Modifying... [id=ebd18307-4665-4bfa-8807-a53ae9df4779]
aws_wafv2_web_acl.waf: Modifications complete after 0s [id=ebd18307-4665-4bfa-8807-a53ae9df4779]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
```

AWS コンソール上で WAF に設定された値を見ると、変更はなさそう。
![スクリーンショット 2021-12-09 13.53.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69711/27c5b101-3a69-86cf-18cb-f781c320ce06.png)

CloudTrail 上も特に何も記録されておらず、AWS 上の変更は何もされていなさそう。

以上より問題ないと判断して、applyを実行しました。
特に問題は発生しなかったので、良かったですね。

# おわりに
この記事が書いているうちに Terraform 1.1.0 が出ちゃったので、またアップグレードしないといけない :innocent: