忙しい人のための結論
結論だけ知りたい人のために、タイトルの答えだけ先に貼っておきます。
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "2.15.0"
}
}
}
locals {
images = [
{ name = "foo", image = "alpine" },
{ name = "bar", image = "debian" },
]
}
resource "docker_container" "this" {
# for_each = local.images ここにmapのlistを渡したいがエラーになる
for_each = { for i in local.images : i.name => i } # こう書くのが正しい
name = each.value.name
image = each.value.image
}
これ terraform-jpのSlackコミュニティの質問で見かけて回答したのだけど、初見で正しく理解して使うのほぼ無理だよねと思ったので、ググって辿りやすそうな場所に解説を放流しておきます。
はじめに
Terraformで複数のresourceを作るのに、 for_each というループっぽい機能があります。
古い記事で count を使っている例もありますが、countは途中のリソースを消すとインデックスがずれるという致命的な問題を抱えているので、Terraform v0.13以降なら基本的にfor_eachを使うべきです。countを使ってよいのは 0/1 でリソースの作成有無を変数で切り替えるというような使い方ぐらいで、2つ以上のリソースを作成するなら常にfor_eachを使うべきです。
for_each 便利ですが、構文が分かりづらいので素朴にループだと思ってちょっと複雑なことをしようとすると、難解なエラーメッセージが出て多少のHCLの知識が必要です。for_eachにmapのlistを渡してループしたいというありがちなケースでも、直感に反してそれほど単純ではありません。
文法的にはresource for_eachはTerraform v0.12.6から、module for eachはTerraform v0.13.0から使えますが、この記事のサンプルコードを稼働確認した手元の環境を一応貼っておくと、以下のとおりです。
$ terraform -v
Terraform v1.0.7
on darwin_amd64
+ provider registry.terraform.io/kreuzwerker/docker v2.15.0
問題
for_eachで回すときに、resourceの各属性をカスタマイズしたいので、外からmapのlistで渡したいというケースを考えてみます。
直感的に書くとこんなかんじですが、
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "2.15.0"
}
}
}
locals {
images = [
{ name = "foo", image = "alpine" },
{ name = "bar", image = "debian" },
]
}
resource "docker_container" "this" {
for_each = local.images # ここにmapのlistを渡したい。
name = each.value.name
image = each.value.image
}
実際には以下のような構文エラーが出ます。
$ terraform validate
╷
│ Error: Invalid for_each argument
│
│ on main.tf line 19, in resource "docker_container" "this":
│ 19: for_each = local.images
│ ├────────────────
│ │ local.images is tuple with 2 elements
│
│ The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and
│ you have provided a value of type tuple.
╵
解答
for_eachにmapのlistを渡すには、以下のように書くのが正しい。
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "2.15.0"
}
}
}
locals {
images = [
{ name = "foo", image = "alpine" },
{ name = "bar", image = "debian" },
]
}
resource "docker_container" "this" {
# for_each = local.images ここにmapのlistを渡したいがエラーになる
for_each = { for i in local.images : i.name => i } # こう書くのが正しい
name = each.value.name
image = each.value.image
}
解説
The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type tuple.
エラーメッセージに書いてますが、for_eachが受け取れる型は map
か set(string)
です。 tuple
や list
ではありません。混乱を招く原因の1つはHCLで []
とコレクションのリテラルを書いた場合、内部的には set
, tuple
, list
の型が区別されていますが、他のプログラミング言語経験からの類推で、 []
はすべて配列っぽい何かと理解しがちです。setは要素が同じ型の順序のない集合、tupleは要素が異なる型の順序のある配列、listは要素が同じ型の順序のある配列です。
setは要するにkeyしかない集合で、このkeyはresourceのアドレスに使われます。この例だと生成されるリソースアドレスは docker_container.this[“foo”]
というかんじになります。 set(string)
を要求しているのはこのためです。 ちなみに map
の key
は string
しか使えません。
この例ではnameをkeyとして使っていますが、これがそのままTerraform上のリソースアドレスになるので、重複しないかつリソースのライフサイクルに対して安定的な属性である必要があります。何をkeyにすべきかはリソースタイプにより異なります。for_eachに配列を渡す場合に toset([ ... ])
するというのがfor_eachのよくサンプルコードに載ってますが、これは各要素をkeyに変換するためです。この例では tuple
になってしまってるので単純に toset
に渡せません。エラーメッセージが難解ですが、 {}
は map
のリテラルのように見えますが、これは厳密には object
リテラルで、この例では local.images
の型は以下のように解釈されています。
tuple([
object({ image: string, name: string}),
object({ image: string, name: string}),
])
object
は属性の名前が定義されている構造体で、 map
はただのkey-valueペアでkeyの名前は型定義に含まれません。ただ {}
でobjectリテラルで書くと同じ属性を持った型でも区別されており、結果として [{},{}]
は tuple[object,object]
と解釈されます。 そのままだとリソースアドレスに使えないので、 この tuple
は for_eachの引数には渡せないということをエラーメッセージは言っていますが、ちょっと分かりづらいですよね。
じゃあどうするかというと { for }
で for式 から object
を生成できるのでこれを使います。
for i in local.images
で { name = "foo", image = "alpine" }
が 変数 i
に束縛されるので、 :
の右辺で各要素の key => value を組み立てます。 この例では i.name => i
とすると、
{
"foo" = { name = "foo", image = "alpine" },
"bar" = { name = "bar", image = "debian" },
}
という object
ができるので、引数として map
が期待されているfor_eachに渡すと、 map
に暗黙の型変換がされて型が合います。
あとは each.key
に foo
が入るのでこれがリソースアドレスとして使われ、 each.value
に { name = "foo", image = "alpine" }
が入ってくるので、あとは欲しい属性を参照すればよいというかんじです。
というのが for_each = { for i in local.images : i.name => i }
の1行に凝縮されています。
これ初見で正しく理解して使うのほぼ無理だよね。。。