19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-08-25

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

結論

どういうときか

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

※ イメージ(動きません)

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つの集合があったとして……

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

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

[
  ["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 という関数がある。

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

やってみる(ダメなパターン)

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

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_eachmapset(string) じゃないとダメ。
setproduct が返しているのは list(tuple) である。

やってみる(うまくいくパターン)

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

なので、ループを回す前にユニークなキーを作ってあげればよい。
Terraformには for という便利なExpressionがある。
ちょうどPythonの内包表記みたいなヤツ。

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してることになる。

{
  "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.

おまけ(うまくいくと思いきやダメなパターン)

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

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

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 使えばいい感じになるという話でした。

19
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?