Terraformの型とループ処理 for_each = { for } について理解する
本記事の目的
- Terraformの型について整理する
- Terraformのループ処理
for_each = { for }
を理解する - Terraformのループ処理の使い所、使い分けについて理解する
Terraformの型について
ループ処理の理解のためには、型の理解が必要なので、ざっくりと説明します。
もちろん、知っている方は読み飛ばしてもらって結構です。
Lists/Tuples/Sets
すべて、[ ]
で囲まれた一連の値であり、下記の違いがあります。
要素の型 | 要素の順序 | 例 | |
---|---|---|---|
list |
同一 | あり | ["foo", "foo", "baz"] |
tuple |
異なる型を許容 | あり | ["foo", "foo", "baz", 1, true] |
set |
同一 | なし(重複が許されない) | ["foo", "bar", "baz"] |
Maps/Objects
ともに、{ }
で囲まれた一連のkey=valueであり、下記の違いがあります。
-
map
: 1つの型のvalueでのみ構成可能- ただし、型を
map(string)
としておけば、暗黙的に型変換されるnumber
やbool
も含めることが可能になります
- ただし、型を
例:
variable "map" {
type = map(string)
description = "サンプルマップ"
default = { a = "hoge", b = 1, c = true }
}
-
object
: 複数の型のvalueで構成可能- keyの名前とvalueの型を定義する必要があります
例:
variable "object" {
type = object({
a = list(string)
b = number
c = bool
})
description = "サンプルオブジェクト"
default = { a = ["hoge", "fuga"], b = 1, c = true }
}
any についての補足
map(any)
ならobject
のように扱えるのでは?と思う方もいるかもしれません。
実際に試してもらったら分かりますが、うまくいきません。
なぜなら、実はany
は単一の型でのみ機能するワイルドカードだからです。
…で、実際に複数のデータ型が入った場合は、やはりstringに変換されるため、map(any)
はmap(string)
と同じと思って差し支えないと思います。
ただし、実際に色々な型が入ると想定しているのであればmap(any)
、文字列だけならmap(string)
と正しく定義しましょう。モジュールの利用者や他の開発者に正しい情報を伝えることが大切だからです。
参考
ループ処理
Terraformの型について整理できたところで、本題のループ処理を見ていきます。
for
for
は式であり、入出力があります。
- 入力として、
list
,set
,tuple
,map
,object
を受け付けます - 出力として、
[ for ]
はtuple
を、{ for }
はobject
を返します-
[ ]
と{ }
は前章で型について整理した通りですね。
-
具体例として、下記のようなlistがあった場合、
locals {
list = ["foo", "bar", "baz"]
}
tuple
の生成
echo '[for s in local.list : upper(s)]' | terraform console
[
"FOO",
"BAR",
"BAZ",
]
object
の生成
echo '{ for s in local.list : s => upper(s) }' | terraform console
{
"bar" = "BAR"
"baz" = "BAZ"
"foo" = "FOO"
}
のようになります。
:
の右辺が出力で、keyとvalueを出力する場合は、<OUTPUT_KEY> => <OUTPUT_VALUE>
の形になります。
正直なところ、for
単体ではあまり出番がなく、次に説明するfor_each
との組み合わせでよく使います。
参考
for_each
for_each
はメタ引数と呼ばれるもので、resource
やmodule
ブロック内で定義できます。
定義することで、resource
やmodule
の動作を変更できます。
具体的には、for_each
を使うことで、1つのresource
ブロックで、複数のリソース定義が可能となります。
for_each
が受け取れる型はset(string)
かmap
です。
set(string)を入力とした場合
例:IAMユーザを2つ作成
locals {
iam_user_names = toset(["hoge", "fuga"])
}
resource "aws_iam_user" "example" {
for_each = local.iam_user_names
name = each.value
}
local valueの配列は、重複がなくてもtuple
扱いになるため、toset()
で変換しています。
なぜ、set(string)
でないといけないのでしょうか。
terraform plan
を実行してみましょう。
# aws_iam_user.example["fuga"] will be created
+ resource "aws_iam_user" "example" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "fuga"
+ path = "/"
+ tags_all = (known after apply)
+ unique_id = (known after apply)
}
# aws_iam_user.example["hoge"] will be created
+ resource "aws_iam_user" "example" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "hoge"
+ path = "/"
+ tags_all = (known after apply)
+ unique_id = (known after apply)
}
注目すべき箇所はリソースIDです。
aws_iam_user.example["fuga"]
aws_iam_user.example["hoge"]
配列の値がそのままTerraformのリソースIDになるため、重複が許されないset(string)
しか受け付けないということです。
set(string)
以外を渡すとどうなるでしょう。
例:パブリックサブネットを2つ作成
locals {
vpc_cidr = "10.0.0.0/16"
vpc_name = "example"
public_subnets = toset([
{
az = "ap-northeast-1a"
name = "public"
cidr = "10.0.1.0/24"
},
{
az = "ap-northeast-1c"
name = "public"
cidr = "10.0.2.0/24"
}
])
}
resource "aws_vpc" "main" {
cidr_block = local.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = local.vpc_name
}
}
resource "aws_subnet" "public" {
for_each = local.public_subnets
availability_zone = each.value.az
cidr_block = each.value.cidr
vpc_id = aws_vpc.main.id
map_public_ip_on_launch = true
tags = {
Name = "${local.vpc_name}-${each.value.name}-${substr(each.value.az, -2, 0)}"
}
}
for_each = local.public_subnets
で、set(object)
型のlocal.public_subnets
を渡しています。terraform plan
を実行してみましょう。
Error: Invalid for_each set argument
The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type object.
当然、set(object)
はダメだよと怒られます。
set(string)
だけだと何かと不便そうですね。そこで、map
の場合を見てみましょう。
mapを入力とした場合
先程の例のpublic_subnets
をmap
にしてみましょう。
locals {
public_subnets = {
0 = {
az = "ap-northeast-1a"
name = "public"
cidr = "10.0.1.0/24"
}
1 = {
az = "ap-northeast-1c"
name = "public"
cidr = "10.0.2.0/24"
}
}
}
terraform plan
を実行すると、期待する結果が得られたと思います。
しかし、マップのkeyを直書きするよりも、できれば、下記のように直感的に書きたいものです。
locals {
public_subnets = [
{
az = "ap-northeast-1a"
name = "public"
cidr = "10.0.1.0/24"
},
{
az = "ap-northeast-1c"
name = "public"
cidr = "10.0.2.0/24"
}
]
}
そこで、{ for }
の出番です。{ for }
は object
を返し、object
は暗黙的にmap
に変換されます。つまり、下記のように書けるわけです。
locals {
vpc_cidr = "10.0.0.0/16"
vpc_name = "example"
public_subnets = [
{
az = "ap-northeast-1a"
name = "public"
cidr = "10.0.1.0/24"
},
{
az = "ap-northeast-1c"
name = "public"
cidr = "10.0.2.0/24"
}
]
}
resource "aws_vpc" "main" {
cidr_block = local.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = local.vpc_name
}
}
resource "aws_subnet" "public" {
+ for_each = { for i in local.public_subnets : i.az => i }
- for_each = local.public_subnets
availability_zone = each.value.az
cidr_block = each.value.cidr
vpc_id = aws_vpc.main.id
map_public_ip_on_launch = true
tags = {
Name = "${local.vpc_name}-${each.value.name}-${substr(each.value.az, -2, 0)}"
}
}
再度説明すると、i.az => i
の部分はlocal.public_subnets
のaz
をkeyにして、object
のvalueと紐付けているのでしたね。確認してみましょう。
echo '{ for i in local.public_subnets : i.az => i }' | terraform console
{
"ap-northeast-1a" = {
"az" = "ap-northeast-1a"
"cidr" = "10.0.1.0/24"
"name" = "public"
}
"ap-northeast-1c" = {
"az" = "ap-northeast-1c"
"cidr" = "10.0.2.0/24"
"name" = "public"
}
}
そして、for_each
で作成したリソースの値をoutputしたい場合は、[ for ]
を使います。
output "public_subnet_ids" {
description = "Public Subnet IDs"
value = [for value in aws_subnet.public : value.id]
}
このfor_each = { for }
の形はTerraformのソースで頻繁に現れるので、ぜひ仕組みを理解しておきたいですね。
応用
下記のようにif
で条件を追加できます。
三項演算子と組み合わせることも可能です。
また、for_each
はdynamic
との組み合わせでも頻出です。機会があれば、dynamic
の使い方についてもまとめようかと思います。
参考
count
最後にcount
ですが、count
はループ処理での利用は非推奨です。for_each
を使いましょう。
非推奨な理由は、下記の記事が具体例もあって、簡潔で分かりやすいです。
count
の使い所はほぼ一択で、リソース作成のON・OFFで使用します。
まとめ
今回はTerraformをDRYに書くために必須であるループ処理を紐解いてみました。
もし、役に立った部分があれば、♡をポチッとお願いします。
また、間違い等あればご指摘お願いします。
以上です。
Discussion