for_eachで何ができるのか
for_eachはresouce
やmodule
ブロックで使える繰り返しのしくみです。同様のリソースを1つの定義で記述できます。
たとえば下記のようなリソースは、
resource "aws_iam_user" "ichiro" { name = "ichiro" } resource "aws_iam_user" "jiro" { name = "jiro" } resource "aws_iam_user" "saburo" { name = "saburo" }
下記のように1つにまとめて記述できます。
resource "aws_iam_user" "accounts" { for_each = toset( ["ichiro", "jiro", "saburo"] ) name = each.key }
for_eachで扱える型
If a resource or module block includes a for_each argument whose value is a map or a set of strings, Terraform creates one instance for each member of that map or set.
上記公式ドキュメントに記載の通りfor_each
で扱えるのは、Terraformで扱える型のうちset
とmap
の2つです。
for_eachのさまざまな使い方
aws_identitystore_userリソースを使ってAWSのIAM Identity Centerのユーザを作成する場合を例にさまざまな使い方を紹介します。
Terraformのインストール、AWS ProviderなどTerraformの実行に必要な基本的な設定は済んでいる前提です。
検証環境は
- Terraformバージョン: 1.5.7
- AWS Providerバージョン: 5.1.0
で実行しています。
1. listをループする
単純な1次元データでIAM Identity Centerのユーザを作成します。
下記のようにusersというローカル変数を定義し[]
にカンマ区切りでデータを入れます。
locals { users = [ "synergy.ichiro", "synergy.jiro", "synergy.saburo", ] }
上記のように書くとusersはlist
型になるので、toset関数でfor_each
で扱えるset
型に変換して渡します。
for_each
に渡した値はeach.key
で参照できます。
data "aws_ssoadmin_instances" "example" {} resource "aws_identitystore_user" "example" { identity_store_id = tolist(data.aws_ssoadmin_instances.example.identity_store_ids)[0] for_each = toset(local.users) display_name = each.key user_name = each.key name { family_name = replace(each.key, "/\\..+$/", "") given_name = replace(each.key, "/^.+\\./", "") } }
上記をterraform plan
した結果は下記のようになります。
# aws_identitystore_user.example["synergy.ichiro"] will be created + resource "aws_identitystore_user" "example" { + display_name = "synergy.ichiro" + external_ids = (known after apply) + id = (known after apply) + identity_store_id = "XXXXXXXXXXX" + user_id = (known after apply) + user_name = "synergy.ichiro" + name { + family_name = "synergy" + given_name = "ichiro" } } # aws_identitystore_user.example["synergy.jiro"] will be created + resource "aws_identitystore_user" "example" { + display_name = "synergy.jiro" + external_ids = (known after apply) + id = (known after apply) + identity_store_id = "XXXXXXXXXXX" + user_id = (known after apply) + user_name = "synergy.jiro" + name { + family_name = "synergy" + given_name = "jiro" } } # aws_identitystore_user.example["synergy.saburo"] will be created + resource "aws_identitystore_user" "example" { + display_name = "synergy.saburo" + external_ids = (known after apply) + id = (known after apply) + identity_store_id = "XXXXXXXXXXX" + user_id = (known after apply) + user_name = "synergy.saburo" + name { + family_name = "synergy" + given_name = "saburo" } }
なぜfor_each
にlist
のまま渡したらダメなの?と思う方がいるかもしれませんが、terraform plan
の結果を見ると理由がわかります。
下記のようにeach.key
はリソース名に使われています。リソース名は重複してはいけないのでユニークなset
型である必要があります。
# aws_identitystore_user.example["synergy.saburo"] will be created # aws_identitystore_user.example["synergy.jiro"] will be created # aws_identitystore_user.example["synergy.ichiro"] will be created
これで3つのリソースを1つの定義で書けてコードがスッキリしました。パチパチ👏
……なんて世の中そんなに甘くはありません。
いちいちローカル変数にカンマ区切りでデータを入れるのは面倒なのでリストはファイルから読み込みたいです。
そんな方は次をご覧ください。
2. ファイルをループする
下記のように1行毎にデータを持ったファイル(users.txt)を作成します。
synergy.ichiro synergy.jiro synergy.saburo
そして、次のようにしてlist
を生成してusersローカル変数に入れます。
locals { users = split("\n", chomp(file("users.txt"))) }
users.txtをfile関数で文字列に読み込み、chomp関数で末尾の改行を削除した上で、split関数を用いて改行で分割しています。
あとはresource
定義は1のままでterraform plan
を実行すれば1と同じ結果が返ってきます。
これで複数のリソースを1つの定義で書けて、ファイルも分かれてコードがスッキリしました。パチパチ👏
……なんて世の中そんなに甘くはありません。
データは1次元だけではないんです。IAM Identity CenterのユーザにはニックネームやEメールアドレスなど、もっとたくさんの情報を登録するので多次元のデータを使いたいです。
そんな方は次をご覧ください。
3. mapをループする
下記のようにユーザ名をキーとするmap
を作成します。値はそのユーザに関する情報のmap
です。2段階にネストされたmap
になります。
locals { users = { "synergy.ichiro" = { email = "synergy.ichiro@synergy101.jp" display_name = "シナジー 一郎" nickname = "いっさん" } "synergy.jiro" = { email = "synergy.jiro@synergy101.jp" display_name = "シナジー 二郎" nickname = "じろきち" } "synergy.saburo" = { email = "synergy.saburo@synergy101.jp" display_name = "シナジー 三郎" nickname = "さぶちゃん" } } }
これをfor_each
に渡すと、each.key
でユーザ名を、each.value.<ネストしたキー名>
でユーザに関する情報を参照できます。
data "aws_ssoadmin_instances" "example" {} resource "aws_identitystore_user" "example" { identity_store_id = tolist(data.aws_ssoadmin_instances.example.identity_store_ids)[0] for_each = local.users display_name = each.value.display_name user_name = each.key nickname = each.value.nickname name { family_name = replace(each.key, "/\\..+$/", "") given_name = replace(each.key, "/^.+\\./", "") } emails { primary = true type = "work" value = each.value.email } }
上記をterraform plan
した結果は下記のようになります。(1つ目のみ表示)
# aws_identitystore_user.example["synergy.ichiro"] will be created + resource "aws_identitystore_user" "example" { + display_name = "シナジー 一郎" + external_ids = (known after apply) + id = (known after apply) + identity_store_id = "XXXXXXXXXXX" + nickname = "いっさん" + user_id = (known after apply) + user_name = "synergy.ichiro" + email { + primary = true + type = "work" + value = "synergy.ichiro@synergy101.jp" } + name { + family_name = "synergy" + given_name = "ichiro" } }
これで多次元のデータも1つの定義で書けてコードがスッキリしました。パチパチ👏
……なんて世の中そんなに甘くはありません。
多次元のデータもファイルから読み込みたいです。
そんな方は次をご覧ください。
4. CSVファイルをループする
下記のようなCSVファイル(users.csv)を作成します。
user_name,email,display_name,nickname synergy.ichiro,synergy.ichiro@synergy101.jp,"シナジー 一郎","いっさん" synergy.jiro,synergy.jiro@synergy101.jp,"シナジー 二郎","じろきち" synergy.saburo,synergy.saburo@synergy101.jp,"シナジー 三郎","さぶちゃん"
そして、users.csvをfile関数で文字列に読み込み、それをcsvdecode関数に渡して、usersローカル変数に入れます。
locals { users = csvdecode(file("users.csv")) }
CSVファイルが複数ある場合は下記のようにconcat関数で結合することもできます。
locals { users = concat(local.dev1_users, local.dev2_users, local.dev3_users) dev1_users = csvdecode(file("dev1_users.csv")) dev2_users = csvdecode(file("dev2_users.csv")) dev3_users = csvdecode(file("dev3_users.csv")) }
csvdecode関数は、CSVのヘッダー行をmap
のキーとしてmap
のlist
を生成します。
consoleコマンドで見ると、csvdecode関数で読み込んだデータは下記のデータ構造になっていることがわかります。
$ terraform console > local.users tolist([ { "display_name" = "シナジー 一郎" "email" = "synergy.ichiro@synergy101.jp" "nickname" = "いっさん" "user_name" = "synergy.ichiro" }, { "display_name" = "シナジー 二郎" "email" = "synergy.jiro@synergy101.jp" "nickname" = "じろきち" "user_name" = "synergy.jiro" }, { "display_name" = "シナジー 三郎" "email" = "synergy.saburo@synergy101.jp" "nickname" = "さぶちゃん" "user_name" = "synergy.saburo" }, ])
そして3の定義のfor_each
の箇所だけ下記のように書き換え、その他の定義はそのままでterraform plan
を実行すれば3と同じ結果が返ってきます。
resource "aws_identitystore_user" "example" { identity_store_id = tolist(data.aws_ssoadmin_instances.example.identity_store_ids)[0] for_each = { for i in local.users : i.user_name => i } display_name = each.value.display_name user_name = each.key nickname = each.value.nickname name { family_name = replace(each.key, "/\\..+$/", "") given_name = replace(each.key, "/^.+\\./", "") } emails { primary = true type = "work" value = each.value.email } }
{ for i in local.users : i.user_name => i }
の部分がわかりにくいと思うので補足すると、csvdecode関数はmap
のlist
を生成するだけで、先ほどconsoleコマンドで見た通り、users変数に入っているのはlist
です。
これをfor_each
に渡すにはmap
に変換する必要があるので、for式を使って、user_name
をキーにしたmap
に変換しています。
map
に変換したものはconsoleコマンドで見ると下記のデータ構造になります。
$ terraform console > { for i in local.users : i.user_name => i } { "synergy.ichiro" = { "display_name" = "シナジー 一郎" "email" = "synergy.ichiro@synergy101.jp" "nickname" = "いっさん" "user_name" = "synergy.ichiro" } "synergy.jiro" = { "display_name" = "シナジー 二郎" "email" = "synergy.jiro@synergy101.jp" "nickname" = "じろきち" "user_name" = "synergy.jiro" } "synergy.saburo" = { "display_name" = "シナジー 三郎" "email" = "synergy.saburo@synergy101.jp" "nickname" = "さぶちゃん" "user_name" = "synergy.saburo" } }
お気づきの方がいるかもしれませんが、この場合user_name
はeach.key
でもeach.value.user_name
でもどちらを使っても参照できます。
これで多次元データもファイルに分けてコードがスッキリしました。パチパチ👏
……なんて世の中そんなに甘くはありません。
特定のキーだけループさせたい時もあるんです。
そんな方は次をご覧ください。
5. 特定のキーをループする
4の定義のfor_each
の箇所にif
で特定のキーを抽出する条件を追加します。
usersローカル変数から、nicknameの値が「〇〇ちゃん」のものだけをループさせる場合、
下記のように書き、terraform plan
を実行すればsynergy.saburoさんのリソースだけが返ってきます。
なお、対象のキーがなくてもリソースが作られないだけでエラーになることはありません。
resource "aws_identitystore_user" "example" { identity_store_id = tolist(data.aws_ssoadmin_instances.example.identity_store_ids)[0] for_each = { for i in local.users : i.user_name => i if can(regex("ちゃん$", i.nickname)) } display_name = each.value.display_name user_name = each.key nickname = each.value.nickname name { family_name = replace(each.key, "/\\..+$/", "") given_name = replace(each.key, "/^.+\\./", "") } emails { primary = true type = "work" value = each.value.email } }
if can(regex("ちゃん$", i.nickname))
の部分をもう少し詳しく説明すると、まずregex関数でnicknameが「〇〇ちゃん」に一致するものを抽出します。
regex関数は、パターンが一致する場合は一致する文字列を返し、一致しない場合はエラーを返します。なので、regex関数の結果をcan関数に渡し、エラーなしで実行できたかを評価させます。
そしてcan関数でtrueを返したものだけをif
でフィルターしてfor_each
に渡しています。
下記のようにconsoleコマンドで見るとregex関数とcan関数の動きを確認できると思います。
$ terraform console > regex("ちゃん$", "さぶちゃん") "ちゃん" > regex("ちゃん$", "いっさん") ╷ │ Error: Error in function call │ │ on <console-input> line 1: │ (source code not available) │ │ Call to function "regex" failed: pattern did not match any part of the given │ string. ╵ > { for i in local.users : i.nickname => can(regex("ちゃん$", i.nickname)) } { "いっさん" = false "さぶちゃん" = true "じろきち" = false } > { for i in local.users : i.user_name => i if can(regex("ちゃん$", i.nickname)) } { "synergy.saburo" = { "display_name" = "シナジー 三郎" "email" = "synergy.saburo@synergy101.jp" "nickname" = "さぶちゃん" "user_name" = "synergy.saburo" } }
最後に
aws_identitystore_userリソースを例にfor_each
を使ってコードをスッキリさせる方法をいくつか紹介しました。
これらを応用するとその他のリソースでも使える場面はあると思います。
これを機にfor_each
を使ってコードをスッキリさせてはいかがでしょうか?