Dynamic Blocks in Terraform

Learn how to use dynamic blocks in Terraform to generate repeated nested blocks from collections, reducing duplication and keeping your code clean. This lesson explains the problem dynamic blocks solve, the core concept and mental model, step-by-step syntax, and a hands-on walkthrough. It compares dynamic blocks with a

Focus: use dynamic blocks for repeated resources

Sponsored

You know the drill: you’re writing Terraform for a set of security group rules, a list of ingress ports, or a handful of tags that need to become nested blocks. You start copy–pasting the same block five times, changing only one value. It works, but the moment the spec changes, you have to edit every copy — and heaven forbid you forget one. That’s the pain dynamic blocks solve: they let you generate repeated nested blocks from a collection, so your code stays DRY, readable, and easy to update in one place.

The problem this lesson solves

Terraform’s resource syntax is rigid: most resources accept nested blocks like ingress, rule, tag, or setting. When you need several of those, the instinct is to write them out manually. That approach breaks down fast:

  • Duplication – You repeat nearly identical blocks, often dozens of times across a configuration.
  • Maintenance hell – A change to port numbers, names, or values means editing every copy.
  • Resource bloat – More code to read, review, and debug means slower, riskier deployments.
  • Inconsistency – It’s easy to miss one block or introduce a typo in a single rule.

The core problem: you need repeated nested blocks, but Terraform doesn’t allow variables directly inside those blocks. Dynamic blocks are the escape hatch.

Core concept / mental model

Think of a dynamic block as a template with a for loop baked in. Instead of writing five identical ingress blocks, you declare one dynamic "ingress" block and give it a list of objects. Terraform expands it into the final resource definition before it talks to the provider.

A mental model that helps: as if you wrote a for_each loop inside the resource itself. The dynamic block has three parts:

  • for_each – the collection (list or map) that drives the loop.
  • iterator – (optional) the name of the loop variable (defaults to the block label).
  • content – the block body, where you use iterator.value to inject data.

Here’s the big insight: dynamic blocks only work inside a resource or data source, not at the top level. They generate nested blocks — they can’t create new resources. That distinction keeps your mental model honest: dynamic blocks are for block repetition, not resource repetition (that’s for_each and count).

How it works step by step

Let’s break the syntax down piece by piece.

1. The dynamic block placeholder

Inside a resource, you write:

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.allowed_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

2. The for_each collection

for_each can be a list or a map. If it’s a list of strings (like port numbers), you use ingress.value to get the current element. If it’s a map, you also get ingress.key.

3. The content block

Inside content, you reference the iterator just like you would in a for_each expression. The iterator’s name defaults to the block label (ingress in the example), but you can override it with iterator = "rule" to avoid confusion when nesting loops.

4. Nested dynamic blocks

You can nest dynamic blocks inside dynamic blocks. Each level gets its own iterator, so name them clearly to keep the code readable.

resource "aws_autoscaling_group" "example" {
  dynamic "tag" {
    for_each = var.tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

5. When Terraform expands the block

Terraform evaluates the for_each collection, iterates over it, and builds a list of nested blocks. It does this during the plan phase, so terraform plan will show you exactly how many blocks will be created.

The full lifecycle: write dynamic block → terraform validate checks syntax → terraform plan expands the loop → provider receives the final resource definition.

Hands-on walkthrough

Let’s build a real example: an AWS security group with a dynamic set of ingress rules.

Step 1 – Define your variables

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

Step 2 – Write the dynamic block

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP, HTTPS, and app port"

  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"]
    }
  }

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

Step 3 – Add a map for richer rules

If you need different protocols per port, switch to a map:

variable "ingress_rules" {
  type = map(object({
    port     = number
    protocol = string
  }))
  default = {
    http  = { port = 80, protocol = "tcp" }
    https = { port = 443, protocol = "tcp" }
  }
}

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

Step 4 – Run it

Save the files, run terraform init, then terraform plan. You’ll see the security group with three (or more) ingress blocks expanded. The plan output shows each rule explicitly — confirm the list matches your intent.

Pro tip: Use terraform console to preview your for_each expression: [for p in var.ingress_ports : p] will show you the exact list the loop will use.

Compare options / when to choose what

Dynamic blocks aren’t the only way to handle repetition. Here’s how they stack up against alternatives:

Approach Best for Pros Cons
Dynamic blocks Repeated nested blocks within a single resource Clean, keeps logic inside the resource Slightly harder to read when deeply nested
for_each on resources Creating multiple entire resources Scales across many resource instances Can’t touch nested blocks inside a resource
Static blocks Fixed, one-off rules Simplest to read Duplicates code when lists grow
Modules Reusable chunks of configuration Encapsulates logic, reduces duplication More overhead for small cases

When to prefer dynamic blocks:

  • The repetition is inside a resource (e.g., security group rules, IAM policies).
  • The list of blocks changes based on variables or data sources.
  • You want to avoid maintaining hundreds of copied blocks.

When NOT to use them: - If you only need 2–3 static blocks, plain blocks are simpler. - If you need to create several resources, use for_each or count on the resource itself. - If the nested blocks have wildly different structures, dynamic blocks with if may become hard to read — consider splitting the resource.

Troubleshooting & edge cases

1. “The given for_each argument value is unsuitable”

This happens when you pass a null or a non-collection value. Make sure your variable is a list or map, and if you use optional, provide a default or handle null:

variable "ports" {
  type    = list(number)
  default = []
}

2. Empty collection results in zero blocks

If for_each = [] or {}, the dynamic block simply disappears. That’s usually what you want, but it can silently remove all rules. Check your inputs.

3. Iterator name clashes

If you nest dynamic blocks and both use the same label, the inner loop shadows the outer. Use iterator to disambiguate:

dynamic "rule" {
  iterator = "outer_rule"
  for_each = var.outer
  content {
    dynamic "sub_rule" {
      iterator = "inner_rule"
      for_each = outer_rule.value.sub_rules
      content {
        # use inner_rule.value
      }
    }
  }
}

4. Using if inside dynamic blocks

You can conditionally include a block with if in the for_each expression:

dynamic "ingress" {
  for_each = var.enable_https ? [443] : []
  content {
    from_port = ingress.value
    to_port   = ingress.value
    protocol  = "tcp"
  }
}

5. Plan looks wrong

Common cause: the variable’s default value changes between runs. Always verify the for_each expression in the plan output. Use terraform state list to see actual created resources.

What you learned & what's next

You now understand how to use dynamic blocks for repeated resources — the core idea of generating nested blocks from collections, the step-by-step syntax, and when to choose it over for_each or static blocks. You practiced with an AWS security group example and saw how to handle edge cases like empty collections and iterator name clashes.

Next in the Terraform foundations track, you’ll tackle variable validation and preconditions, ensuring your inputs are safe before Terraform even plans. That lesson builds directly on the collections you used here — the same values you loop over can be validated upfront, preventing bad configurations before they become infrastructure changes.

Practice recap

Try this mini exercise: define a variable ingress_rules as a map of objects with port and protocol, then write a resource aws_security_group that uses a dynamic block to create an ingress rule for each. Run terraform plan and verify the output expands to the correct number of rules. Then change one value in the map and see how quickly the plan updates — that’s the maintenance win.

Common mistakes

  • Using for_each on a resource when you actually need to repeat nested blocks — they serve different purposes.
  • Forgetting that for_each requires a collection — passing null or a scalar value causes a plan-time error.
  • Overriding the iterator name but still referencing the old label — the default matches the block label, but after you change iterator, you must use the new name.
  • Assuming dynamic blocks create new resources — they only generate nested blocks inside a single resource; use count or for_each on the resource itself for that.

Variations

  1. Use a map instead of a list in for_each to access key and value for richer nested block attributes.
  2. Nest dynamic blocks to handle multi-level structures, using explicit iterator names to keep your code readable.
  3. Combine dynamic blocks with conditional expressions (if) to include or exclude blocks based on variables or data sources.

Real-world use cases

  • Generate firewall rules in an AWS security group from a variable list of ports and protocols.
  • Create IAM policy statements with multiple actions per resource using a dynamic block over a map of permissions.
  • Define autoscaling group tags from a map of key–value pairs, ensuring each tag propagates to launched instances.

Key takeaways

  • Dynamic blocks let you generate repeated nested blocks from a collection, reducing duplication and maintenance burden.
  • The for_each argument must be a list or map; an empty collection produces zero blocks.
  • Use the iterator argument to disambiguate nested loops and avoid shadowing.
  • Dynamic blocks are not a replacement for for_each on resources — they operate only inside a resource or data source.
  • Always inspect terraform plan output to confirm how many blocks were expanded from your dynamic block.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.