Terraformで配列をloopする時はfor_eachを使った方がいい

f:id:cloudfish:20200217183335p:plain

AWSのパラメータストアをTerraformで作成したのですが、for_eachを知らずにcountを使って作成したところ、追加や削除の際に色々と意図しない挙動になったので、回避策について備忘録を残しておきたいと思います。

環境

terraform(0.12.07)

やりたかったこと

パラメータストアに登録するデータが複数あったため、配列のように定義して一括登録したかった。

発生した問題

登録後にさらにパラメータストアを追加したところ、追加分だけでなく他のパラメータについても差分と判定された。

はじめに書いたtfファイル

以下が最初に書いたパラメータストアの設定になります。作成は問題なく完了しました。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  count       = "${ length( var.ssm_parameter_app_list.params ) }"
  name        = "/${var.env}/${values(var.ssm_parameter_app_list.params)[count.index].name}"
  value       = "${values(var.ssm_parameter_app_list.params)[count.index].value}"
  type        = "SecureString"
}

variable.tf

variable "env" {
  default = "dev" 
}

variable "ssm_parameter_app_list"{
  default = {
    params = {
      param1 = {
        name = "param1"
        value = "xxxxxxxxxxxx"
      }
      param2 = {
        name = "param2"
        value = "xxxxxxxxxxxx"
      }
      param3 = {
        name = "param3"
        value = "xxxxxxxxxxxx"
      }
         ・
         ・
         ・
      param10 = {
        name = "param10"
        value = "xxxxxxxxxxxx"
      }
    }
  }
}
この書き方の問題点
  • 追加の際に追加分以外の差分が発生する
  • 途中の要素の削除においても、削除分以外の差分が発生する

例えば、param11を追加した場合、param2以降の全ての要素が差分として発生します。また、削除においてもparam5を削除した場合も同様にparam5以降の要素が差分となります。

なぜこのような結果になるかというのはstateファイルを確認することで分かりました。以下stateファイルの抜粋になります。
要素のkeyが数字となっており、param1-10でソートされていることが分かります。
stateファイル(抜粋)

      "instances": [
        {
          "index_key": 0,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 1,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 2,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 3,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param3",
            "description": "",
            "id": "/dev/param3",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param3",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },


配列のキー(param1-10)でソートされた結果としては以下のようになります。param10がparam1の次に並び替えされてしまうため、上記のような差分が発生することになります。param1ではなくparam01と定義することでソートの問題は解決できるのですが、追加は正常にできるものの削除の問題は残ったままとなります。

param1
param10
param2
 ・
 ・
param9

このように意図しない差分を防ぐためには、for_eachを使えば解決できることがわかりました。

for_eachを使った書き方(その1)

variableに key=value連想配列を定義し、for_eachを利用して以下のようにセットすることができます。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  for_each = var.ssm_parameter_app_list

  name        = "/${var.env}/${each.key}"
  type        = "SecureString"
  value       = each.value
}

variable.tf

variable "ssm_parameter_app_list"{
  default = {
      "param1"  = "xxxxxxxxxxxx"
      "param2"  = "xxxxxxxxxxxx"
      "param3"  = "xxxxxxxxxxxx"
       ・
       ・
      "param10" = "xxxxxxxxxxxx"
  }
}

上記の書き方でparameter storeを作成すると以下のようなstateファイルになります。index_keyにparam1-10が設定されるため、追加や削除の際に意図しない差分が発生することはありません。

      "instances": [
        {
          "index_key": "param1",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param10",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param2",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },

ただし、このようにkeyとvalueのセットでリソースを定義できるのであればこの書き方で問題ないのですが、各要素ごとに複数の設定を持つのであればこの方法では難しくなります。

for_eachを使った書き方(その2)

例えば、これまではtypeは「SecureString」固定でしたが、これも変数で定義したいとなった場合は上記の方法では対応できません。
この場合については以下の書き方で対応ができました。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  for_each = var.ssm_parameter_app_list

  name        = "/${var.env}/${lookup(each.value, "name")}"
  value       = lookup(each.value, "value")
  type        = lookup(each.value, "type")
}

variable.tf

variable "ssm_parameter_app_list"{
  type = map(map(string))
  default = {
      param1 = {
        name = "param1"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
      param2 = {
        name = "param2"
        value = "xxxxxxxxxxxx"
        type  = "String"
      }
      param3 = {
        name = "param3"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
       ・
       ・
      param10 = {
        name = "param10"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
   }
}
      "instances": [
        {
          "index_key": "param1",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param10",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param2",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "String",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },

まとめ

今回は配列をループしたかったためにcountを使いましたが、あまりよくない使い方だったため思わぬところでハマってしまいました。幸いまだ本番リリースされていないサービスだったので再作成することが可能でしたが、terraformの制御文は気をつけるポイントが多いですね。