Terraform のコーディングにおいて、似た構成の複製をどのように表現するかは結構重要な課題です。放っておくと汚いコピペだらけになっていくからです。
色々な目的とやり方があると思いますので、その表現を実現するためのパーツにでもなればと思い、学習用教材的に書いてみるやつでございます。
目次
説明はそんなに多くないですが、コードのせいで縦長になったので目次を置いておきます。Terraform バージョンは v1.5.7 で動作確認しています。単体の複製
あるリソースに対して、単体の場合は count や for_each を使うことで、list や map の値を使いつつ複数のリソースを作成することができます。count
直に数値を指定したり、list の数だけ作成できます。
1 2 3 4 5 6 7 8 9 10 11 |
locals { names = ["test01", "test02"] } resource "aws_eip" "default" { count = length(local.names) tags = { Name = local.names[count.index] } } |
リソースは 0 から始まる数値で管理されるので、同じ数だけ作る関連リソースでは、数値を合わせるように参照します。
1 2 3 4 5 6 |
resource "aws_nat_gateway" "default" { count = length(local.names) allocation_id = aws_eip.default[count.index].id ... } |
for_each
map を指定して、その key の数ぶんを作成でき、each で key / value を利用可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
locals { eips = { test01 = { role = "web" } test02 = { role = "test" } } } resource "aws_eip" "default" { for_each = local.eips tags = { Name = each.key role = each.value["role"] } } |
リソースはキー名で管理されるため、参照する場合もキーを指定します。
1 2 3 4 5 6 |
resource "aws_nat_gateway" "default" { for_each = local.eips allocation_id = aws_eip.default[each.key].id ... } |
for_each はリソース内のアイテムを複数指定するのにも使えますが、今回は別の話なのでオマケの紹介です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
locals { tags = { Name = "test01" role = "web" } } resource "aws_autoscaling_group" "default" { ... dynamic "tag" { for_each = local.tags content { key = tag.key value = tag.value propagate_at_launch = true } } } |
セットの複製
複数のリソースでセットを作る場合、2つ目以降を作るのにコードを丸っとコピペしていると煩雑になっていくため、module で一括りに利用できるようにします。module
module 用のディレクトリとコードを作成し、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
variable "name" { } variable "subnet_id" { } resource "aws_eip" "default" { tags = { Name = var.name } } resource "aws_nat_gateway" "default" { allocation_id = aws_eip.default.id subnet_id = var.subnet_id } |
source と variable を指定して利用できます。新しい module 利用の前には、terraform init を実行する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module "test01" { source = "./modules/nat" name = "test01" subnet_id = "subnet-abcdefg" } module "test02" { source = "./modules/nat" name = "test02" subnet_id = "subnet-bcdefgh" } |
count + module
module を利用する際にも count や for_each で複数セットを作ることが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 |
locals { names = ["test01", "test02"] } module "test" { count = length(local.names) source = "./modules/nat" name = local.names[count.index] subnet_id = "subnet-abcdefg" } |
条件分岐
三項演算子があるので、複製構造と合わせて使うことで、より柔軟な構成をすることが可能になります。三項演算子
フラグを仕込んでコード丸ごと削除やコメントアウトしないでよくしたり、
1 2 3 4 5 6 7 |
locals { flag = true } resource "aws_eip" "default" { count = local.flag ? 1 : 0 } |
実行環境ごとに作るか作らないかを制御できます。
1 2 3 4 5 6 7 |
locals { workspace = terraform.workspace } resource "aws_eip" "default" { count = local.workspace == "production" ? 1 : 0 } |
入れ子
入れ子構造にすることも可能です。( ) は無くても動きます。
1 2 3 4 5 6 7 8 9 |
locals { first = false second = true result = local.first ? "A" : (local.second ? "B" : "C") } $ terraform console > local.result "B" |
ループ構造
list や map のループ処理として for があります。ただし、処理中に何かを作成することはできなく、list や map として変数を作り直すための機能です。for
list を処理したり、
1 2 3 4 5 6 7 8 9 10 11 12 |
locals { data = ["test01", "test02"] result = [for v in local.data: "prefix-${v}"] } $ terraform console > local.result [ "prefix-test01", "prefix-test02", ] |
map を list で返したり、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
locals { data = { key01 = "value01" key02 = "value02" } result = [for k,v in local.data: "${k}-${v}"] } $ terraform console > local.result [ "key01-value01", "key02-value02", ] |
map を編集した map で返したり、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
locals { data = { key01 = "value01" key02 = "value02" } result = {for k,v in local.data: v => k} } $ terraform console > local.result { "value01" = "key01" "value02" = "key02" } |
ちょっと使いづらいけど、入れ子も可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
locals { data = { key01 = { key01_01 = "value01" key01_02 = "value02" } key02 = { key02_01 = "value03" key02_02 = "value04" } } result = [for k,v in local.data: [for k2,v2 in v: v2]] } $ terraform console > local.result [ [ "value01", "value02", ], [ "value03", "value04", ], ] |
ワンライナーじゃなく、改行で整えることも可能。
1 2 3 4 5 6 7 |
locals { result = [ for k,v in local.data: [ for k2,v2 in v: v2 ] ] } |
入れ子の反対方向の出し子も可能で、型の扱いが少し異なります。詐欺みたいですが使い道はあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
locals { result = [for k2,v2 in [for k,v in local.data: v]: v2] } $ terraform console > local.result [ { "key01_01" = "value01" "key01_02" = "value02" }, { "key02_01" = "value03" "key02_02" = "value04" }, ] |
複製の方法
基本的には以上の方法を駆使して表現していくわけですが、リソース作成を実現するだけなら、どのような方法でも可能です。単にリソースの作成/削除をするにも、フラグで制御するのか、コメントアウトするのか、コードや .tf ファイル丸ごと消すのか、といった選択肢があります。
複数のリソース管理では、count, for_each, module, コードや .tf ファイルの丸ごとコピペ といった選択肢があります。
コーディングの基本として、同じ構成の処理は極力書かない、というものがありますので、少なくともコピペは避けたいところですが、module だらけにするのもコーディングしづらいものがあり、リソース間の細かい違いを吸収しつつコードを重複させないバランス感覚はなかなか難しかったりします。
Terraform 自体がコーディングしやすいよう進化し続けているのもあり、私も構成としては既に三代目に突入しており、for と for_each があることでようやくマシになってきた感触があります。
それにより運用自体は設定の編集だけで済むようになりましたが、代わりに resource の記述は黒魔術気味になりました。続けて、その黒魔術部分を分解して理解を深めておきたいと思います。
階層構造
設定値でリソース作成を済ますということは、設定が複雑になっても大丈夫ということで、階層構造をいかに実現するかという話になります。Pythonの入れ子ループ
階層構造の設定を以下のように作成した場合、2段のループを行うことで3回の結果を得ることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
config = { "dev01": { "role01": { "name": "example01", "user": "test01", }, "role02": { "name": "example02", "user": "test02", }, }, "dev02": { "role01": { "name": "example03", "user": "test03", }, } } for k,v in list(config.items()): for k2,v2 in list(v.items()): print(k, k2, v2["name"], v2["user"]) |
結果はこうで、簡単すぎてアクビがでちゃいます。
1 2 3 |
dev01 role01 example01 test01 dev01 role02 example02 test02 dev02 role01 example03 test03 |
こんな初歩中の初歩をあえて書いたのは、terraform では急に難しくなるからです。
Terraform の入れ子ループ
同じように Terraform で設定を書くとこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
locals { config = { dev01 = { role01 = { name = "example01" user = "test01" } role02 = { name = "example02" user = "test02" } } dev02 = { role01 = { name = "example03" user = "test03" } } } } |
ゴールとしては、for_each に map を渡すことになりますが、素直な方法では設定内容の値を余すことなく使うことはできません。
それは for_each の中で for_each を使うような入れ子構造にできないことが大きな理由です。for_each には最下層の3個がキーとなる map を渡さなくてはなりません。
1 2 3 4 5 6 7 8 |
resource "aws_eip" "default" { for_each = local.config # <-- グループ2個分しかない tags = { group = each.key role = each.value # <-- role がキーになった map なので、どうしようもない } } |
色々試しましたが、まず最下層から無理やり3個の list として取り出すしかありません。こうしてから……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
locals { result = [for k,v in local.config: [for k2,v2 in v: v2]] } $ terraform console > local.result [ [ { "name" = "example01" "user" = "test01" }, { "name" = "example02" "user" = "test02" }, ], [ { "name" = "example03" "user" = "test03" }, ], ] |
こうじゃ!見事に3個になりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
locals { result = flatten([for k,v in local.config: [for k2,v2 in v: v2]]) } $ terraform console > local.result [ { "name" = "example01" "user" = "test01" }, { "name" = "example02" "user" = "test02" }, { "name" = "example03" "user" = "test03" }, ] |
でもこれだと、肝心のキーが2階層分、消失してしまうので無理やり継続利用できるように埋め込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
locals { result = flatten([ for k,v in local.config: [ for k2,v2 in v: merge(v2,{group=k,role=k2}) ] ]) } $ terraform console > local.result [ { "group" = "dev01" "name" = "example01" "role" = "role01" "user" = "test01" }, { "group" = "dev01" "name" = "example02" "role" = "role02" "user" = "test02" }, { "group" = "dev02" "name" = "example03" "role" = "role01" "user" = "test03" }, ] |
だいぶ雰囲気が出てきましたが、まだ map になっていないので、キーをユニークにした map に編集してあげます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
locals { result = { for x in flatten([ for k,v in local.config: [ for k2,v2 in v: merge(v2,{group=k,role=k2}) ] ]): "${x["group"]}-${x["role"]}" => x } } $ terraform console > local.result { "dev01-role01" = { "group" = "dev01" "name" = "example01" "role" = "role01" "user" = "test01" } "dev01-role02" = { "group" = "dev01" "name" = "example02" "role" = "role02" "user" = "test02" } "dev02-role01" = { "group" = "dev02" "name" = "example03" "role" = "role01" "user" = "test03" } } |
これで resource の for_each に渡してあげれば、キーが管理名になり、each.key や each.value[“name”] で値を思う存分に使うことができます。ね、簡単でしょう?
続・階層構造
ここまででも、わりと気持ち悪い感じのコードになりましたが、さらなる発展例で泥沼に潜り込みます。最下層の設定に、schedules を追加しました。Autoscaling 用のイメージで、各Roleごとに複数のスケジュールを登録できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
locals { config = { dev01 = { role01 = { name = "example01" schedules = { morning = { min_size = 1 } evening = { min_size = 3 } } } role02 = { name = "example02" } } dev02 = { role01 = { name = "example03" schedules = { midnight = { min_size = 0 } } } } } result = { for x in flatten([ for k,v in local.config: [ for k2,v2 in v: merge(v2,{group=k,role=k2}) ] ]): "${x["group"]}-${x["role"]}" => x } } $ terraform console > local.result { "dev01-role01" = { "group" = "dev01" "name" = "example01" "role" = "role01" "schedules" = { "evening" = { "min_size" = 3 } "morning" = { "min_size" = 1 } } } "dev01-role02" = { "group" = "dev01" "name" = "example02" "role" = "role02" } "dev02-role01" = { "group" = "dev02" "name" = "example03" "role" = "role01" "schedules" = { "midnight" = { "min_size" = 0 } } } } |
さきほどのままの map だと、スケジュール個数分の map になっていないので、ここからさらに絞り上げる必要があります。
schedules が存在する設定に絞り込み、かつキー情報を引き継いでいって、最終的にユニークなキーの map に仕上げます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
locals { result = { for z in flatten([ for x in flatten([ for k,v in local.config: [ for k2,v2 in v: merge(v2,{target="${k}-${k2}"}) if contains(keys(v2), "schedules") ] ]): [ for y2,x2 in x["schedules"]: merge(x2,{target=x["target"],name="${x["target"]}-${y2}"}) ] ]): z["name"] => z } } $ terraform console > local.result { "dev01-role01-evening" = { "min_size" = 3 "name" = "dev01-role01-evening" "target" = "dev01-role01" } "dev01-role01-morning" = { "min_size" = 1 "name" = "dev01-role01-morning" "target" = "dev01-role01" } "dev02-role01-midnight" = { "min_size" = 0 "name" = "dev02-role01-midnight" "target" = "dev02-role01" } } |
name はこのスケジュール・リソース用の名前で、target はそれを登録する Autoscaling リソースの名前って感じです。これに cron 形式の情報などを追加すれば、あとは for_each に渡していかようにでもヤリクリできます。
複雑なように見えますが、同じことを2回やっているだけです。map を同列に組み直すために flatten() を使い、merge() で情報を引き継ぎ、最後に key / value に仕上げています。設定に項目を書かなくても大丈夫なように if contains() を使ったり、小賢しいこともわりと組み込んでいけるので、不可能なことはそうない気がします。
できるだけ設定値だけでリソース管理をしようとすると、その分は resource 側が複雑になりますし、逆に設定側の手間を増やせば resource 側をシンプルにできるので、それは目的とお好み次第です。
今回はわりと汚らしい例まで踏み込んでみましたが、このくらいやるべきって話ではなく、どのような表現が可能なのかを知っておくことで、無駄のある構成を回避したり、構成を諦めたりしなくて済むようにしておきたいですねって感じです。
キレイにまとまったと思いますが、これでもだいぶ苦労しましたし、これを書きながらも理解を深めて修正もしたので、やはりシステムはただ動けばいいやってモノじゃないってのを実感した回でした:-)