Terraformで2重ループっぽいことをしたい

**Terraformで2重ループっぽいことをしたい**ときがあるかもしれない。

### 結論

- [setproduct](https://www.terraform.io/docs/language/functions/setproduct.html) をうまく使ってください。
  - [サンプルコードはここ](#やってみるうまくいくパターン)にあります。

## どういうときか

例えば、**2種類**のロールを**2人**に付与したい場合。
2つなら普通に書けばいいんだけど、量が多くなってくるといい感じに書きたくなる。

※ イメージ（動きません）

```hcl
loop {
  for_each = toset([
    "roles/storage.admin",
    "roles/bigquery.admin",
  ])

  resource "google_project_iam_member" "roles" {
    for_each = toset([
      "user:aaaa@example.com",
      "user:bbbb@example.com",
    ])

    role   = each1.key
    member = each2.key
  }
}
```

## どうするか

2つの集合があって、これらを使って2重ループをするいうのは、それらの集合からそれぞれ取り出した値の組み合わせ全てに対してループすることと同じ。

さっきの例のような2つの集合があったとして……

```hcl
[
  "roles/storage.admin",
  "roles/bigquery.admin",
]
```

```hcl
[
  "user:aaaa@example.com",
  "user:bbbb@example.com",
]
```

この場合、以下のような値のペアに対して1回ループすれば良い。

```hcl
[
  ["roles/storage.admin", "user:aaaa@example.com"],
  ["roles/storage.admin", "user:bbbb@example.com"],
  ["roles/bigquery.admin", "user:aaaa@example.com"],
  ["roles/bigquery.admin", "user:bbbb@example.com"],
]
```

このように、2つの集合からそれぞれ取り出した値の組み合わせを作るというのは、要は**2つの集合の直積**を求めている。
Terraformには2つのセットの直積を求める [setproduct](https://www.terraform.io/docs/language/functions/setproduct.html) という関数がある。

これを使うと当初の目的（2重ループっぽいこと）が達成できそう。

## やってみる（ダメなパターン）

ここまでの知識でやってみようとすると、こうなる。

```hcl
resource "google_project_iam_member" "roles" {
  for_each = setproduct(
    [
      "roles/storage.admin",
      "roles/bigquery.admin",
    ],
    [
      "user:aaaa@example.com",
      "user:bbbb@example.com",
    ],
  )

  role   = each.value[0]
  member = each.value[1]
}
```

だがしかし、残念ながらこれは動かない。

```
│ Error: Invalid for_each argument
│
│   on test.tf line 7, in resource "google_project_iam_member" "roles":
│    7:   for_each = setproduct(
│    8:     [
│    9:       "roles/storage.admin",
│   10:       "roles/bigquery.admin",
│   11:     ],
│   12:     [
│   13:       "user:aaaa@example.com",
│   14:       "user:bbbb@example.com",
│   15:     ],
│   16:   )
│
│ 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 list of tuple.
```

書いてあるとおりだが、Terraformの `for_each` は `map` か `set(string)` じゃないとダメ。
`setproduct` が返しているのは `list(tuple)` である。

## やってみる（うまくいくパターン）

なんでtypeの制約があるのかというと、resourceの集合を作るときにキーが被ると困るから。

なので、ループを回す前にユニークなキーを作ってあげればよい。
Terraformには [for](https://www.terraform.io/docs/language/expressions/for.html) という便利なExpressionがある。
ちょうどPythonの内包表記みたいなヤツ。

```hcl
resource "google_project_iam_member" "roles" {
  for_each = { for v in setproduct(
    [
      "roles/storage.admin",
      "roles/bigquery.admin",
    ],
    [
      "user:aaaa@example.com",
      "user:bbbb@example.com",
    ],
  ) : join(",", v) => v }

  role   = each.value[0]
  member = each.value[1]
}
```

これで以下のmapに対してfor_eachしてることになる。

```hcl
{
  "roles/storage.admin,user:aaaa@example.com"  = ["roles/storage.admin", "user:aaaa@example.com"],
  "roles/storage.admin,user:bbbb@example.com"  = ["roles/storage.admin", "user:bbbb@example.com"],
  "roles/bigquery.admin,user:aaaa@example.com" = ["roles/bigquery.admin", "user:aaaa@example.com"],
  "roles/bigquery.admin,user:bbbb@example.com" = ["roles/bigquery.admin", "user:bbbb@example.com"],
}
```

確かにキーがユニークになっていて、イケそう。
これはうまく動く。

```
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_project_iam_member.roles["roles/bigquery.admin,user:aaaa@example.com"] will be created
  + resource "google_project_iam_member" "roles" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "user:aaaa@example.com"
      + project = (known after apply)
      + role    = "roles/bigquery.admin"
    }

  # google_project_iam_member.roles["roles/bigquery.admin,user:bbbb@example.com"] will be created
  + resource "google_project_iam_member" "roles" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "user:bbbb@example.com"
      + project = (known after apply)
      + role    = "roles/bigquery.admin"
    }

  # google_project_iam_member.roles["roles/storage.admin,user:aaaa@example.com"] will be created
  + resource "google_project_iam_member" "roles" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "user:aaaa@example.com"
      + project = (known after apply)
      + role    = "roles/storage.admin"
    }

  # google_project_iam_member.roles["roles/storage.admin,user:bbbb@example.com"] will be created
  + resource "google_project_iam_member" "roles" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "user:bbbb@example.com"
      + project = (known after apply)
      + role    = "roles/storage.admin"
    }

Plan: 4 to add, 0 to change, 0 to destroy.
```

## おまけ（うまくいくと思いきやダメなパターン）

**ユニークなキーを作って**、という話なので、勘の鋭い人は気がついていると思うが、キーが重複すると死ぬ。

めちゃくちゃな例だが、これとか。

```hcl
resource "google_project_iam_member" "roles" {
  for_each = { for v in setproduct(
    [
      "a,",
      "a",
    ],
    [
      "a",
      ",a",
    ],
  ) : join(",", v) => v }

  role   = each.value[0]
  member = each.value[1]
}
```

これは `a,,a` というキーをもつ要素が2つできてしまう。

```
│ Error: Duplicate object key
│ 
│   on test.tf line 16, in resource "google_project_iam_member" "roles":
│    7:   for_each = { for v in setproduct(
│    8:     [
│    9:       "a,",
│   10:       "a",
│   11:     ],
│   12:     [
│   13:       "a",
│   14:       ",a",
│   15:     ],
│   16:   ) : join(",", v) => v }
│ 
│ Two different items produced the key "a,,a" in this 'for' expression. If duplicates are expected, use the ellipsis (...) after the value expression to enable grouping by key.
```

## おわり

Terraformでは2重ループはできないが、どうしてもしたくなったら [setproduct](https://www.terraform.io/docs/language/functions/setproduct.html) 使えばいい感じになるという話でした。