
TerraformのDynamic Blocksを使ってみた
はじめに
今回はTerraformでネストされた複数のリソースを動的に作成できるDynamic Blocksを紹介します。
Dynamic Blocksのドキュメントには以下の記載があります。
You can dynamically construct repeatable nested blocks like setting using a special dynamic block type
翻訳すると、Dynamicブロックは、繰り返し可能なネストされたブロックを動的に構築することができます。と書かれています。
なんとなく繰り返し処理ができるんだな〜とイメージは湧きますが、実際にどのような場面で、どのような記述をすればよいのかイメージが湧きづらかったので試してみました。
また、繰り返し処理としてよく使用されるfor_eachとは何が違うのかについても考えてみました。
やってみる
では、実際にDynamicブロックを使ってリソースを作ってみます。
今回はセキュリティグループに複数のインバウンドルールを設定するコードを書きます。
#################################################
# 変数定義
#################################################
variable "ingress_rules" {
type = list(object({
port = number
description = string
cidr_blocks = list(string)
}))
default = [
{
port = 80
description = "HTTP"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 443
description = "HTTPS"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
#################################################
# セキュリティーグループ作成
#################################################
resource "aws_security_group" "main" {
name = "main-sg"
vpc_id = "<vpc-id>"
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
コードの解説
上記のコードでは以下の要素でDynamicブロックが構成されています。
for_each: 反復処理する値を定義します。今回の場合だと変数で定義したvar.ingress_rules
です。
content: 生成するリソースの内容を定義します。
iterator: 反復変数のカスタム命名で、明示的に指定しなかった場合はデフォルトはdynamic blockの名前になります。今回の場合だとingress
がiteratorです。
このように変数で定義した値をDynamicブロックで定義することで、セキュリティグループ内のネストされたルールに対して反復処理を実行することができます。
では、実際にリソースを作成してみます。
terraform planを実行します。
$ terraform plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_security_group.main will be created
+ resource "aws_security_group" "main" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = "HTTP"
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 80
},
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = "HTTPS"
+ from_port = 443
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 443
},
]
+ name = "main-sg"
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
variables.tfで定義した変数が挿入されていますね。
ではリソースを作成します。
$ terraform apply
実際に作成されたリソースも確認しておきます。
セキュリティグループと、インバウンドルールが作成されていることがわかります。
Dynamicブロックを使えばネストされたリソースが繰り返し処理により簡単に作成できました。
めでたしめでたし。
あれ、、、ちょっと待てよ?
これってfor_eachでもできるんじゃない?と思いませんか?
私は思いました。
for_eachとの違い
そうなんです、for_eachでも同じことができます。
代表的な繰り返し処理といえばfor_eachが思い浮かびますが何が違うのでしょうか?
両方ともTerraformでの繰り返し処理に使われますが、それぞれ目的が異なります。
Dynamicブロック:1つのリソース内でネストされたブロックを複数生成する
for_each:複数のリソースやモジュールを生成する
例えばセキュリティグループを例にすると、以下のような使い方が向いていると思われます。
Dynamicブロック:(セキュリティグループにネストされた)セキュリティグループのルールを複数作成
for_each:セキュリティグループを複数作成
つまり、両者は目的が異なるためDynamicブロックは単体で使うことも考えられますが、for_eachと組み合わせて利用することも考えられます。
「Dynamicブロック + for_each」と「for_eachのみ」のコードを比較すると、Dynamicブロックの良さが伝わるのではと思い、今回は複数のセキュリティグループとルールをまとめて作成するコードで比較してみたいと思います。
Dynamicブロック + for_each
まずはDynamicブロック + for_eachでセキュリティグループを作成してみます。
#################################################
# 変数定義
#################################################
variable "security_groups" {
type = map(object({
name = string
description = string
ingress = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}))
default = {
sg1 = {
name = "sg1"
description = "Security Group 1"
ingress = [
{ from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
]
},
sg2 = {
name = "sg2"
description = "Security Group 2"
ingress = [
{ from_port = 22, to_port = 22, protocol = "tcp", cidr_blocks = ["10.0.0.0/16"] }
]
}
}
}
#################################################
# セキュリティーグループ作成
#################################################
resource "aws_security_group" "main" {
for_each = var.security_groups
name = each.value.name
description = each.value.description
vpc_id = "<vpc-id>"
dynamic "ingress" {
for_each = each.value.ingress
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
resourceブロックの概要は以下のとおりです。
- for_eachでセキュリティグループ自体を作成
- さらにネストしたインバウンドルールをDynamicブロックで作成
for_eachのみ
次に、少し無理やりですが同じセキュリティグループをfor_eachのみで作成するコードを書きます。
#################################################
# 変数定義
#################################################
variable "security_groups" {
type = map(object({
name = string
description = string
ingress_rules = map(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}))
default = {
"sg1" = {
name = "sg1"
description = "Security Group 1"
ingress_rules = {
"http" = {
description = "http"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
"https" = {
description = "https"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
},
"sg2" = {
name = "sg2"
description = "Security Group 2"
ingress_rules = {
"mysql" = {
description = "ssh"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
}
}
}
}
#################################################
# セキュリティーグループ作成
#################################################
resource "aws_security_group" "this" {
for_each = var.security_groups
name = each.value.name
description = each.value.description
vpc_id = "<vpc-id>"
}
# インバウンドルールの作成
resource "aws_security_group_rule" "ingress" {
for_each = {
for idx, sg in flatten([
for sg_key, sg in var.security_groups : [
for rule_key, rule in sg.ingress_rules : {
sg_key = sg_key
rule_key = rule_key
rule = rule
}
]
]) : "${sg.sg_key}_${sg.rule_key}" => sg
}
type = "ingress"
security_group_id = aws_security_group.this[each.value.sg_key].id
description = each.value.rule.description
from_port = each.value.rule.from_port
to_port = each.value.rule.to_port
protocol = each.value.rule.protocol
cidr_blocks = each.value.rule.cidr_blocks
}
Dynamicブロックに比べて複雑なコードになっていることが分かります。
ネストされたリソースをfor_eachで作成することはできますが、Dynamicブロックを使用した方が簡単に実装できることが分かりました。
Dynamicブロックの注意点
Dynamicブロックを使うと冗長的なコードを簡略化できる一方で、作成されるリソースがコードからは読み取りづらくなり、コードの可読性や保守性が下がることも考慮する必要があります。
再利用可能なモジュールで、コードの作成者以外の人が詳細を知る必要がない場合などに限りDynamicブロックを利用することが推奨です。
今回はDynamicブロック検証のために無理やりセキュリティグループを一括で作成しましたが、個々のaws_security_group_ruleリソースを分けることで、コードから作成されるリソースを把握しやすくすることも検討が必要かと思います。
まとめ
Dynamicブロックを使用すると、for_eachのみでは複雑になってしまうネストしたブロックを簡略化することが可能です。
一方でネストされたリソースを変数で定義するのは、作成されるリソースがコードからは判断しづらく、Dynamicブロックを多用するのはリスクがあることも考慮する必要があることが分かりました。