2重ループ的な処理を Terraform で実現する (Azure)

# やりたいこと
業務で Terraform を使っている中で、2重ループ的な処理を実現する必要があった。
分かってしまえば簡単だが、わりと手こずったので備忘録兼他の方の参考になればと思い書いておく。

# どういう処理をしたいか
複数のリソースに対して、複数のユーザ (AzureAD で管理) のアクセス権を割り当てる、というようなケース。
具体的には、5つのストレージアカウント内のコンテナへの `Blob Data Contributor` ロールを2人のユーザに割り当てる、というケース。

:::note warn
本来は、グループを作成してそのグループに `Blob Data Contributor` を割り当てる方が良い。
最初に上記の形で割り当てたのでその内容で話を進める。現在はグループに割り当てる形にしている。
:::

# 通常の Terraform でのループ処理 (単一ループ)
以下のような感じ。`for_each` を使う。`st_names` 内の値がストレージアカウント名として使用される。

``` single_loop_example.tf
local {
  st_names = ["st1", "st2", "st3", "st4", "st5"]
}

# ストレージアカウントを作成
resource "auzrerm_storage_account" "storages" {
  for_each = local.st_names

  name                     = each.value
  resource_group           = "rg-sample"
  location                 = "japaneast"
  account_tier             = "Standard"
  account_replication_type = "GRS"
}
```

これらのストレージアカウントは Terraform 内では、以下のようなリソースとして管理されている。
それぞれのリソースは、コードを通して一意である必要があり重複は許されない。

```
auzrerm_storage_account.storages["st1"]
azurerm_storage_account.storages["st2"]
azurerm_storage_account.storages["st3"]
azurerm_storage_account.storages["st4"]
azurerm_storage_account.storages["st5"]
```

この状態で各ストレージアカウントの `Blob Data Contributor` をユーザに割り当てようとする以下のような感じ (力技)。

``` assign_role_example.tf
local {
  st_names = ["st1", "st2", "st3", "st4", "st5"]
  users    = ["user1@example.com", "user2@example.com"]
}

# ストレージアカウントを作成
resource "auzrerm_storage_account" "storages" {
  for_each = local.st_names

  name                     = each.value
  resource_group           = "rg-sample"
  location                 = "japaneast"
  account_tier             = "Standard"
  account_replication_type = "GRS"
}

# AzureAD からユーザ情報を取得
data "azuread_users" "blob_access_users" {
  user_pricipal_names = local.users
}

# st1 の Blob Data Contributor に各ユーザを割り当てる
resource "azurerm_role_assignment" "st1" {
  for_each = data.azuread_users.blob_access_users.object_ids

  scope                = azurerm_storage_account.sotrages["st1"]
  role_definition_name = "Blob Data Contributor"
  principal_id         = each.value
}

# st2 の Blob Data Contributor に各ユーザを割り当てる
resource "azurerm_role_assignment" "st2" {
  for_each = data.azuread_users.blob_access_users.object_ids

  scope                = azurerm_storage_account.sotrages["st2"]
  role_definition_name = "Blob Data Contributor"
  principal_id         = each.value
}

# st3~5 も同様
# st1~5 をループで処理したくなる
```

数が少ないうちは上記のような書き方でも問題はないかもしれないが、数が多くなってくると大変になる。
その一方、可読性は高いのでそれはそれでありなのかも？ :thinking: 
個人的には同じようなコードは何度も書きたくないので、ループで処理するようにしたい。その方がかっこ良いし！

# 2重ループを実現するには
こうする。

``` multi_loop_example.tf
local {
  st_names = ["st1", "st2", "st3", "st4", "st5"]
  users    = ["user1@example.com", "user2@example.com"]
}

# ストレージアカウントを作成
resource "auzrerm_storage_account" "storages" {
  for_each = local.st_names

  name                     = each.value
  resource_group           = "rg-sample"
  location                 = "japaneast"
  account_tier             = "Standard"
  account_replication_type = "GRS"
}

# AzureAD からユーザ情報を取得
data "azuread_users" "blob_access_users" {
  user_pricipal_names = local.users
}

# st1~5 の Blob Data Contributor に各ユーザを割り当てる
# ２重ループ部分
resource "azurerm_role_assignment" "blob_data_contributor" {
  for_each = {
    for item in setproduct(
      [for st_name in local.st_names : st_name],
      [for user_id in data.azuread_users.object_ids]
    ) : join("-", item) => item
  }

  scope                = each.value[0]           # st_name
  role_definition_name = "Blob Data Contributor"
  principal_id         = each.value[1]            # user_id
  # each.key は st_name と user_id を '-' で join した文字列 (下記参考)
}
```

上記で使っている `setproduct` が今回のポイント。詳細は [公式ドキュメント](https://www.terraform.io/docs/language/functions/setproduct.html) を。要は、引数で与えられたリストの直積を作成する関数。
これによって、以下のような形の `map` のリストを作成して、`for_each` に与えている。


```
[
  "st1-user1@example.com" = {"st1", "user1@example.com"},
  "st1-user2@example.com" = {"st1", "user2@example.com"},
  "st2-user1@example.com" = {"st2", "user1@example.com"},
  "st2-user2@example.com" = {"st2", "user2@example.com"},
  ...
]
```

こうすることで、`auzrerm_role_assignment` の各ストレージへのロールアサインのリソースは以下のようになり、重複しなくなるため実行できる。

```
azurerm_role_assignment.blob_data_contributor["st1-user1@example.com"]
azurerm_role_assignment.blob_data_contributor["st1-user2@example.com"]
azurerm_role_assignment.blob_data_contributor["st2-user1@example.com"]
azurerm_role_assignment.blob_data_contributor["st2-user2@example.com"]
azurerm_role_assignment.blob_data_contributor["st3-user1@example.com"]
...
```

3重ループとかも実現可能なはずだが、そこまではやっていない。そこまでいくと可読性が下がりそうなので、別の方法でやった方が良いのかも？

これが最良の方法なのかは分からないので、他の方法などあればコメントいただけると嬉しいです！

以上です。

# 参考
* https://qiita.com/sekai/items/93aa4978f8cfc87e0913
* https://www.yasuhisay.info/entry/2021/08/03/173000