dynamicブロック — 繰り返しブロックを動的に生成するメタ引数

1. 概要

  • dynamicブロックの基本構文とcontentブロックの書き方
  • for_eachとの組み合わせ方
  • セキュリティグループルールへの適用(最頻出パターン)
  • ネストしたdynamicブロック
  • dynamicを使うべき場面・使わないべき場面

dynamicブロックは、リソース内の繰り返しブロックを動的に生成する機能です。セキュリティグループのインバウンドルールや、ECSコンテナのポートマッピングなど、同じ構造のブロックを複数記述するときに使います。


2. 基本構文

dynamic "<ブロック名>" {
  for_each = <コレクション>
  content {
    # ブロックの中身(<ブロック名>.valueを使って各要素を参照)
    # デフォルトのイテレータ名 = dynamicブロックのラベル名
    <属性> = <ブロック名>.value.<フィールド>
    # ...
  }
}
  • for_eachにlist/set/mapを渡すと、要素の数だけブロックが生成される
  • contentブロックに繰り返すべき設定を書く
  • イテレータ変数のデフォルト名はdynamicブロックのラベル名(例:dynamic "ingress" なら ingress.key / ingress.value
  • iterator引数で任意の別名に変更できる(ネスト時に区別しやすくなる)

3. セキュリティグループルールへの適用

最も多い使用例です。複数のポート・CIDRをリストで定義し、dynamicで展開します。

terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "environment" {
  description = "環境名"
  type        = string
  default     = "dev"
}

locals {
  # インバウンドルールをリストで定義
  ingress_rules = [
    { port = 443, protocol = "tcp", cidr = "0.0.0.0/0", description = "HTTPS" },
    { port = 80,  protocol = "tcp", cidr = "0.0.0.0/0", description = "HTTP redirect" },
    { port = 8080, protocol = "tcp", cidr = "10.0.0.0/8", description = "Internal API" },
  ]
}

resource "aws_security_group" "web" {
  name        = "${var.environment}-web-sg"
  description = "Web server security group"

  # ✅ dynamicブロックでルールを動的に生成
  dynamic "ingress" {
    for_each = local.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = [ingress.value.cidr]
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "All outbound"
  }

  tags = {
    Name        = "${var.environment}-web-sg"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

dynamicなしで書いた場合(繰り返しが発生):

# ❌ dynamicなしだと同じ構造を3回書く必要がある
resource "aws_security_group" "web" {
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }
}

4. mapを使ったパターン

mapで渡すと<ブロック名>.keyでキーを参照できます。

locals {
  db_parameters = {
    "max_connections"       = "100"
    "shared_buffers"        = "256MB"
    "effective_cache_size"  = "768MB"
    "log_min_duration_statement" = "1000"
  }
}

resource "aws_db_parameter_group" "postgres" {
  family = "postgres15"
  name   = "${var.environment}-postgres15"

  dynamic "parameter" {
    for_each = local.db_parameters
    content {
      name  = parameter.key    # mapのキーがパラメータ名
      value = parameter.value  # mapの値がパラメータ値
    }
  }

  tags = {
    Name        = "${var.environment}-postgres15"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

5. 条件付きブロック(0/1制御)

for_eachに空コレクションを渡すと、そのブロックは生成されません。条件によってブロックを有効/無効にできます。

variable "enable_monitoring" {
  description = "CloudWatchモニタリングを有効にするか"
  type        = bool
  default     = false
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = "t3.micro"

  # enable_monitoringがtrueのときだけmetadata_optionsブロックを生成
  dynamic "metadata_options" {
    for_each = var.enable_monitoring ? [1] : []
    content {
      http_endpoint               = "enabled"
      http_tokens                 = "required"
      instance_metadata_tags      = "enabled"
    }
  }

  tags = {
    Name        = "app"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

6. dynamicブロックのiterator(別名)

デフォルトのイテレータ名はdynamicブロックのラベル名(例:ingress.value)ですが、iterator引数で任意の別名に変更できます。ネストしたdynamicで外側と内側のイテレータを区別するときに便利です。

dynamic "ingress" {
  for_each = local.ingress_rules
  iterator = rule  # ingress.value の代わりに rule.value を使う
  content {
    from_port   = rule.value.port
    to_port     = rule.value.port
    protocol    = rule.value.protocol
    cidr_blocks = [rule.value.cidr]
  }
}

7. dynamicを使わないべき場面・制約

ネストは1段のみ有効

dynamicブロックは1段のネストには対応していますが、contentブロック内でさらにdynamicをネストする場合はそれぞれ独立したブロックとして記述します。iteratorを使って外側と内側のeachを区別することで多重ネストに対応できます(セクション6参照)。ただし3段以上のネストは可読性が著しく低下するため、localsでデータを事前に整形することを強く推奨します。

# ❌ ブロックが1つだけのときはdynamicは不要
dynamic "egress" {
  for_each = [1]
  content {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# ✅ 1つだけなら通常のブロックで書く
egress {
  from_port   = 0
  to_port     = 0
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"]
}

8. 関連記事


9. まとめ

  • dynamicブロックはリソース内の繰り返しブロックを動的に生成する
  • for_eachにlist/set/mapを渡し、contentブロックに設定を書く
  • セキュリティグループのルールやDBパラメータグループが最頻出の用途
  • for_each = var.flag ? [1] : []で条件付きブロック(有効/無効)を実現できる
  • ブロックが1つだけの場合は通常のブロックを使う(dynamicは不要)
  • dynamicのネストはiteratorで外側と内側を区別する。3段以上のネストは可読性が悪化するのでlocalsで事前整形する

動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 公式ドキュメント: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks