👻

Terraformでのloop処理の書き方(for, for_each, count)

2021/01/24に公開

環境

$ terrraform -v
Terraform v1.4.2
on darwin_arm64

for, for_each, countの違い

まずforはExpression(式)であり、for_eachcountはMeta-Argumentといった明確な違いがあります。
forは式なので値を返しますが、for_eachとcountは返しません。
for_eachはresourceやmoduleでしか書けず、イメージとしてはresourceブロックごと繰り返すという感じになります。

# これは書ける
resource "aws_instance" "name" {
  for_each = []
}

# これは書けない
resource "aws_instance" "name2" {
  for = []
}

なのでforのみを使って複数リソースを作ることはできません。

forの使い方

forは他のプログラミング言語のforと同じ感じで回せます。Pythonのリスト内包表記に似ています。
mapとlistをforで回せるのでそれぞれ見ていきます。

listをforで回してみる

まずは単純に配列の文字列をすべて大文字にしてみます。

locals {
  list = [
    "hoge",
    "fuga"
  ]
}

output "output_list" {
  value = [for l in local.list : upper(l)]
}
Changes to Outputs:
  + output_list = [
      + "HOGE",
      + "FUGA",
    ]

ifを使ってフィルタリングもできます。

locals {
  list = [
    "hoge",
    "fuga"
  ]
}

output "output_list" {
  value = [for l in local.list : upper(l) if l != "fuga"]
}
Changes to Outputs:
  + output_list = [
      + "HOGE",
    ]

indexが欲しい場合はこうします。

locals {
  list = [
    "hoge",
    "fuga"
  ]
}

output "output_list" {
  value = [for i, l in local.list : "${i}_${l}"]
}
Changes to Outputs:
  + output_list = [
      + "0_hoge",
      + "1_fuga",
    ]

listからmapを生成することもできます。

locals {
  list = [
    "hoge",
    "fuga"
  ]
}

output "output_map" {
  # value = {} の形になっている
  value = {for i, l in local.list : i => l}
}
Changes to Outputs:
  + output_map = {
      + 0 = "hoge"
      + 1 = "fuga"
    }

forで回すと辞書順にソートされます。なのでforで生成されるのはsetでなくlistになります。
厳密にはmapやobjectの場合はキーや属性名で要素がソートされ、文字列のsetの場合はその値でソートされます。
その他の型のsetの場合は任意の順序付けになりますが将来のバージョンで変更される可能性があります。

locals {
  set = toset(["b", "a", "c", "fjpoe"])
}

output "list" {
  value = [for v in local.set : v]
}
Changes to Outputs:
  + list = [
      + "a",
      + "b",
      + "c",
      + "fjpoe",
    ]

mapをforで回してみる

key, valueともに大文字にしてみます。

locals {
  map = {
    a = "about"
    b = "blow"
  }
}

output "output_map" {
  value = {for k, v in local.map : upper(k) => upper(v)}
}
Changes to Outputs:
  + output_map = {
      + A = "ABOUT"
      + B = "BLOW"
    }

mapでもifが使えます。

locals {
  map = {
    a = "about"
    b = "blow"
  }
}

output "output_map" {
  value = {for k, v in local.map : upper(k) => upper(v) if v != "blow"}
}
Changes to Outputs:
  + output_map = {
      + A = "ABOUT"
    }

mapからlistを生成することもできます。

locals {
  map = {
    a: "about"
    b: "blow"
  }
}

output "output_list" {
  value = [for k, v in local.map : v]
}
Changes to Outputs:
  + output_map = [
      + "about",
      + "blow",
    ]

forで回してmapを作成するときはキーはユニークでないといけません。

locals {
  users = {
    Mike = {
      role = "Admin"
    }
    Bob = {
      role = "Admin"
    }
    Alice = {
      role = "Developer"
    }
  }
}

output "role_user" {
  # Adminが重複するのでエラーになる
  value = { for name, user in local.users : user.role => name }
}

その場合は結果をグループ化できます。

output "role_user" {
  # name -> name...
  value = { for name, user in local.users : user.role => name... }
}
Changes to Outputs:
  + list = {
      + Admin     = [
          + "Bob",
          + "Mike",
        ]
      + Developer = [
          + "Alice",
        ]
    }

forまとめ

listやmapを繰り返し処理して新たなlistやmapを作成できます。

count

v0.13からresource, moduleの両方で使えます。それ以前のバージョンではresourceのみです。

Tip: Terraform 0.13 supports count on both resource and module blocks. Prior versions only supported it on resource blocks.

resourceブロックに count = 数値 を指定することで指定した数値分のリソースを作成できます。
count.index でインデックスを取得できます。

resource "aws_iam_user" "example" {
  count = 2
  name = "user_${count.index}"
}

output "user_ids" {
  value = aws_iam_user.example.*.id
}
Outputs:

users = [
  "user_0",
  "user_1",
]

これだけだと数値インデックスしか変数が使えないので、基本的にはlistと組み合わせて使います。

locals {
  names = [
    "hoge_user",
    "fuga_user"
  ]
}

resource "aws_iam_user" "example" {
  count = length(local.names)
  name = local.names[count.index]
}

for_each

for_eachはv0.12.6で追加されて、moduleでも使えるようになったのでv0.13のようです。

Version note: for_each was added in Terraform 0.12.6. Module support for for_each was added in Terraform 0.13, and previous versions can only use it with resources.

listをfor_eachで回してみる

count + listで行っていたことと同様のことがfor_eachでもできます。
値を取り出すときにインデックスを指定しなくてよくなるのでシンプルに書けます。

locals {
  names = [
    "hoge_user",
    "fuga_user"
  ]
}

resource "aws_iam_user" "example" {
  for_each = toset(local.names)
  name = each.key # each.value でも可
}

for_eachはmapか文字列のsetしか受け付けないためlistをそのまま使えず toset() を使ってsetにする必要があります。
each.keyeach.value でkey-valueを参照でき、setの場合はどちらも同じ値になります。

mapをfor_eachで回してみる

mapを使うことでkeyとvalueのそれぞれを参照できるようになり、より柔軟にリソースの設定が行なえます。

locals {
  users = {
    hoge_user = "/hoge/",
    fuga_user = "/fuga/"
  }
}

resource "aws_iam_user" "example" {
  for_each = local.users
  name = each.key
  path = each.value
}

dynamic blockとfor_each

特定のリソースでは繰り返し可能な設定があります。

resource "aws_autoscaling_group" "example" {
  # ...

  tag {
    key                 = "Name"
    value               = "example-asg-name"
    propagate_at_launch = true
  }

  tag {
    key                 = "Component"
    value               = "user-service"
    propagate_at_launch = true
  }

  tag {
    key                 = "Environment"
    value               = "production"
    propagate_at_launch = true
  }
}

これらをfor_eachを使って書くことができます。

locals {
  standard_tags = {
    Name        = "example-asg-name"
    Component   = "user-service"
    Environment = "production"
  }
}

resource "aws_autoscaling_group" "example" {
  
  min_size = 0
  max_size = 0

  dynamic "tag" {
    for_each = local.standard_tags
		
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

いままでのfor_eachだと each.key で参照できていましたが、 dynamic blockだと tag.key のように block名.key でないと参照できません。

forとfor_eachの組み合わせ

forは式でlistやmapを返すのでfor_eachと組み合わせることができます。

locals {
  names = [
    "hoge",
    "fuga"
  ]
}

resource "aws_iam_user" "example" {
  for_each = {for name in local.names : name => upper(name) }
  name = each.key
  tags = {
    "Name" = each.value
  }
}

loopをネストさせたい

Terraformでネストしたloopを書く

まとめ

forは式なので値を返すことができます。
resourceを繰り返したい場合はfor_eachかcountを使用します。
forとfor_eachは組み合わせることができます。

Discussion