Terraformで総当たりの組み合わせtupleを生成する

Google IAMのメンバー設定を記述していて、いちいち `resource` を繰り返し定義するのがバカらしくなったので調べてみました。

# やりたいこと

2つのサービスアカウント、3つのロールについて、 [google_project_iam_member](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_member) を設定する

サービスアカウント

- `compute-engine@hogehoge.iam.gserviceaccount.com`
- `cloud-functions@hogehoge.iam.gserviceaccount.com`

ロール

- `roles/bigquery.jobUser`
- `roles/bigquery.dataViewer`
- `roles/bigquery.dataEditor`

# 愚直に書くと…

```terraform
resource "google_project_iam_member" "computeEngine-bigquery_jobUser" {
  role   = "roles/bigquery.jobUser"
  member = "serviceAccount:compute-engine@hogehoge.iam.gserviceaccount.com"
}
```

## 面倒くさいこと

- これを2 * 3 = 6回書かなければならない
  - もし 3 * 4 になると12回も書かなければならない
- それぞれユニークに名付ける
- 抜け漏れ確認
- 対象ロールやサービスアカウントが増えたらまたコピペ地獄

# ループしちゃえ

## サービスアカウント、ロールの配列を定義

```terraform
locals {
  # サービスアカウント、ロールの配列を定義
  service_accounts = [
    "compute-engine@hogehoge.iam.gserviceaccount.com",
    "cloud-functions@hogehoge.iam.gserviceaccount.com"
  ]
  roles = [
    "roles/bigquery.jobUser",
    "roles/bigquery.dataViewer",
    "roles/bigquery.dataEditor",
  ]

  # setproduct で総当りの組み合わせをループしながらtupleを作る（ついでにmemberを加工）
  iam_members = [
    for pair in setproduct(local.roles, local.service_accounts) : {
      role   = pair[0]
      member = "serviceAccount:${pair[1]}"
    }
  ]
}
```

この時点で `output` で確認してみるとこのようになります。

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

Terraform will perform the following actions:

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

Changes to Outputs:
  + iam_members = [
      + {
          + member = "serviceAccount:compute-engine@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.jobUser"
        },
      + {
          + member = "serviceAccount:cloud-functions@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.jobUser"
        },
      + {
          + member = "serviceAccount:compute-engine@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.dataViewer"
        },
      + {
          + member = "serviceAccount:cloud-functions@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.dataViewer"
        },
      + {
          + member = "serviceAccount:compute-engine@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.dataEditor"
        },
      + {
          + member = "serviceAccount:cloud-functions@hogehoge.iam.gserviceaccount.com"
          + role   = "roles/bigquery.dataEditor"
        },
    ]
```

ちゃんと6つの要素ができています。

## `resource` で利用する

```terraform
# "${item.member}__${item.role}" はリソースの名前になるため、ユニークなキーにしなければならない

resource "google_project_iam_member" "iam_members" {
  for_each = { for item in local.iam_members : "${item.member}__${item.role}" => item }

  # each.key は "${item.member}__${item.role}"
  role = each.value.role
  member = each.value.member
}
```

もちろん、 `locals.iam_members` を定義せず、 `resource` 内でインライン利用することもできます。
`locals` にロジックを書きたくない場合はこちらもアリかもですね。

```terraform
resource "google_project_iam_member" "iam_members" {
  for_each = {
    for item in [
      for pair in setproduct(local.roles, local.service_accounts) : {
        role   = pair[0]
        member = "serviceAccount:${pair[1]}"
      }
    ] : "${item.member}__${item.role}" => item
  }

  role   = each.value.role
  member = each.value.member
}
```


# 参考

https://www.terraform.io/docs/language/functions/setproduct.html