Terraform Dynamic Blocks: Generate Nested Blocks from Variables

· 1 min read · Terraform & OpenTofu Tutorials

Repeat nested blocks with a dynamic block #

Some resources have nested blocks that can be repeated, such as ingress in aws_security_group, setting in Elastic Beanstalk or statement in an IAM policy document. for_each and count work on the resource, not on its nested blocks. A dynamic block generates those blocks from a collection.

Without a dynamic block #

static.tf
resource "aws_security_group" "web" {
  name   = "web"
  vpc_id = aws_vpc.main.id

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

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

With a dynamic block #

dynamic.tf
variable "ingress_ports" {
  type    = list(number)
  default = [80, 443]
}

resource "aws_security_group" "web" {
  name   = "web"
  vpc_id = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_ports

    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}
  • The label ("ingress") is the name of the nested block to generate.
  • for_each is the collection. Each element creates one block.
  • content defines the body of each generated block.
  • Inside, ingress.value and ingress.key refer to the current element. The variable is named after the block label.

Rename the iterator #

Use iterator to give the current element a different name, which helps with nested dynamic blocks:

iterator.tf
dynamic "ingress" {
  for_each = var.rules
  iterator = rule

  content {
    description = rule.value.description
    from_port   = rule.value.port
    to_port     = rule.value.port
    protocol    = rule.value.protocol
    cidr_blocks = rule.value.cidrs
  }
}

with a variable of objects:

variables.tf
variable "rules" {
  type = list(object({
    description = string
    port        = number
    protocol    = string
    cidrs       = list(string)
  }))
}

Optional blocks #

An empty collection generates no block, so a dynamic block is also a way to make a nested block optional:

optional.tf
dynamic "versioning_configuration" {
  for_each = var.enable_versioning ? [1] : []

  content {
    status = "Enabled"
  }
}

When not to use it #

Dynamic blocks make the code harder to read. Use them when the number of blocks really depends on input. If you always have the same two rules, write them explicitly.

#Terraform #OpenTofu #Hcl #AWS