TerraformのDynamic Blocksを使ってみた

TerraformのDynamic Blocksを使ってみた

Clock Icon2025.03.08

はじめに

今回は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

実際に作成されたリソースも確認しておきます。
CleanShot 2025-03-08 at 10.55.51@2x
セキュリティグループと、インバウンドルールが作成されていることがわかります。

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でセキュリティグループを作成してみます。

dynamic_block
#################################################
# 変数定義
#################################################
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のみで作成するコードを書きます。

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ブロックを多用するのはリスクがあることも考慮する必要があることが分かりました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.