DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Terraform Tutorial

Introduction to Terraform
Installing Terraform
The Terraform Core Workflow
Terraform Variables and Outputs
Terraform State and Backends
Terraform Modules
Terraform Data Sources
Terraform Count and for_each
Terraform Expressions and Functions
Terraform Dynamic Blocks
Terraform Workspaces
Terraform with AWS
Terraform Provisioners
Terraform CI/CD Pipeline
Terraform Security

Terraform Dynamic Blocks

PreviousPrev
Next

Learn how Terraform dynamic blocks generate nested configuration, where they fit well, how to use them safely, and when they make code too complex.

What Are Dynamic Blocks?

Terraform dynamic blocks let you generate repeated nested blocks inside a resource, data source, provider, or provisioner configuration. They are useful when the block you want to repeat is not a top-level resource but a nested configuration block such as ingress, egress, statement, or another provider-defined structure.

A normal resource block with count or for_each repeats the entire resource. A dynamic block repeats just one nested portion inside the resource.

That is the key idea.

For example, an AWS security group can contain multiple ingress blocks. If the number of ingress rules varies by environment or is driven by input data, hardcoding every ingress block can become repetitive. A dynamic block lets Terraform generate those nested blocks from a collection.

Why Dynamic Blocks Exist

Many provider schemas contain nested blocks rather than flat arguments. Examples include:

  • security group ingress and egress rules
  • IAM policy statements or conditions in some patterns
  • load balancer listener actions
  • route definitions
  • container definitions in some structured providers

If the number of nested blocks is fixed and small, writing them directly is often best. But when the structure is variable, duplicated, or based on input data, dynamic blocks reduce repetition.

The goal is not to make Terraform feel like a general-purpose programming language. The goal is to keep configuration declarative while still allowing repeated nested structures.

Dynamic Block Syntax

Here is the general form:

dynamic "ingress" {
  for_each = var.ingress_rules

  content {
    from_port   = ingress.value.from_port
    to_port     = ingress.value.to_port
    protocol    = ingress.value.protocol
    cidr_blocks = ingress.value.cidr_blocks
  }
}

Break it down:

  • dynamic "ingress" says Terraform will generate one or more ingress blocks
  • for_each decides how many blocks to create
  • content { ... } defines what each generated block contains
  • the iteration object is named after the block label, so ingress.value refers to the current item

This syntax can feel unusual at first because it is not the same each.key/each.value pattern you use on resources. Inside a dynamic block, the iterator defaults to the block label unless you explicitly change it.

A Basic Security Group Example

Suppose you want to create a security group with several ingress rules based on a variable.

variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))

  default = [
    {
      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"]
    }
  ]
}

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

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

Terraform will generate two ingress blocks, one for port 80 and one for port 443.

This is the most common beginner use case for dynamic blocks.

Using the Current Item Inside content

The current item is usually accessed through <block_label>.value.

In the previous example:

  • ingress.value.from_port
  • ingress.value.to_port
  • ingress.value.protocol
  • ingress.value.cidr_blocks

If the collection is a map, you can also use the key.

Terraform also supports an explicit iterator argument if you want a different name:

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule

  content {
    from_port = rule.value.from_port
    to_port   = rule.value.to_port
  }
}

This can improve readability, especially when nested dynamic blocks are involved.

Dynamic Blocks in Security Groups

Security groups are a great teaching example because AWS security groups often need several nearly identical nested blocks.

Hardcoded approach

resource "aws_security_group" "web" {
  name   = "web-sg"
  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"]
  }
}

This is perfectly fine when the rule set is small and fixed.

Dynamic approach

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

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }
}

This is better when:

  • rules differ across environments
  • a module caller should define the rules
  • the number of rules is not fixed
  • you want to avoid repeating the same nested structure many times

Dynamic Blocks in IAM Policies

Dynamic blocks can also help when nested policy fragments vary by input.

For example, imagine a module builds multiple statements from a collection.

variable "policy_statements" {
  type = list(object({
    sid       = string
    actions   = list(string)
    resources = list(string)
  }))
}

data "aws_iam_policy_document" "app" {
  dynamic "statement" {
    for_each = var.policy_statements

    content {
      sid       = statement.value.sid
      actions   = statement.value.actions
      resources = statement.value.resources
    }
  }
}

This pattern is helpful when different callers need different policy statements but the overall document structure remains the same.

Why this matters

Instead of hardcoding five statement blocks and editing them manually for each project, you let input data describe the policy shape.

That makes modules more reusable while still keeping policy logic explicit.

Nesting Dynamic Blocks

Terraform also supports nested dynamic blocks when the provider schema contains repeated nested blocks inside repeated nested blocks.

Example pattern:

dynamic "statement" {
  for_each = var.policy_statements

  content {
    sid = statement.value.sid

    dynamic "condition" {
      for_each = statement.value.conditions

      content {
        test     = condition.value.test
        variable = condition.value.variable
        values   = condition.value.values
      }
    }
  }
}

This is powerful, but it can become difficult to read quickly.

When nested dynamic blocks are worth it

Use them when:

  • the provider schema is genuinely nested
  • the data model is structured and meaningful
  • the repetition would otherwise be large and error-prone

When nested dynamic blocks are risky

Avoid them when:

  • the configuration becomes hard to understand
  • a few explicit blocks would be clearer
  • the abstraction hides important security or networking intent

Dynamic Block vs Multiple Hardcoded Blocks

This is one of the most important design decisions.

ApproachBest whenTradeoff
Hardcoded nested blocksSmall, fixed configurationMore repetition
Dynamic blocksVariable or input-driven nested configurationMore abstraction

Hardcoded blocks are often clearer

If you only have two ingress rules and they will never change, writing them directly is often the best answer. Anyone reading the resource sees exactly what is allowed.

Dynamic blocks are often better for reusable modules

If a module should accept a caller-defined set of rules, a dynamic block usually gives the right balance of flexibility and structure.

When NOT to Use Dynamic Blocks

Dynamic blocks are useful, but they are easy to overuse.

Do not use them just because you can. They are not automatically better than explicit blocks.

Avoid dynamic blocks when the config is tiny and fixed

This is too abstract for little gain:

dynamic "ingress" {
  for_each = [80, 443]

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

It works, but two explicit ingress blocks may be easier for a beginner to read.

Avoid dynamic blocks when they hide important intent

Security policies and firewall rules are high-impact infrastructure. If abstraction makes them harder to audit, it may not be worth it.

Avoid dynamic blocks when input complexity becomes the real problem

Sometimes a large, deeply nested variable structure is harder to maintain than a few repeated blocks. In those cases, simplify the interface instead of adding more dynamic logic.

A Complete Practical Example

Here is a realistic security group module pattern.

variable "name" {
  type = string
}

variable "vpc_id" {
  type = string
}

variable "ingress_rules" {
  type = list(object({
    description = string
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
}

resource "aws_security_group" "this" {
  name   = var.name
  vpc_id = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    iterator = rule

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

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

This keeps the security group reusable while leaving the default egress rule explicit and easy to audit.

Best Practices for Dynamic Blocks

1. Use them for repeated nested blocks, not for everything

If the entire resource is repeated, count or for_each is usually the right tool. Dynamic blocks solve a different problem.

2. Keep input data well-typed

List or map objects with clear fields make dynamic blocks easier to read and validate.

3. Consider an explicit iterator

If the default label-based iterator is confusing, rename it to something clearer.

4. Prefer explicit blocks when the structure is small

Clarity is often more valuable than removing a few lines.

5. Be extra careful with security-related abstractions

Anything involving ports, CIDRs, principals, or policy statements should remain easy to inspect.

Common Beginner Mistakes

Mistake 1: Using dynamic blocks to repeat whole resources

Dynamic blocks repeat nested blocks, not top-level resources.

Mistake 2: Making simple configuration harder to read

If the dynamic version feels more complicated than the hardcoded version, it probably is.

Mistake 3: Confusing iterators

Inside dynamic "ingress", the current item is ingress.value unless you define another iterator name.

Mistake 4: Over-nesting everything

Nested dynamic blocks can be valid, but they should serve a clear purpose.

Final Thoughts

Terraform dynamic blocks are a focused tool for a specific job: generating repeated nested configuration. They are especially useful in reusable modules where nested blocks such as ingress rules or IAM statements vary based on input data.

The most important ideas are:

  • dynamic blocks repeat nested blocks, not whole resources
  • for_each decides how many nested blocks are generated
  • content defines the body of each generated block
  • readability matters just as much as reducing repetition

If explicit blocks are clearer, use explicit blocks. If nested configuration truly needs to be driven by data, dynamic blocks can make Terraform modules much more flexible without losing structure.


Knowledge Check

Exercise

Question 1: Purpose

What problem do Terraform dynamic blocks primarily solve?

Exercise

Question 2: Syntax

Inside dynamic "ingress" { ... }, which expression normally accesses the current item's value?

Exercise

Question 3: Overuse

When should you usually avoid dynamic blocks?

PreviousPrev
Next

Continue Learning

Terraform Expressions and Functions

Get a quick overview of Terraform expressions and built-in functions before diving into string operations and collection transformations.

5 min read·Easy

Terraform String Functions

Learn Terraform string interpolation, formatting, replacements, regex helpers, templatefile, conditionals, and other string-oriented expressions used in real infrastructure code.

10 min read·Easy

Terraform Collection Functions

Learn Terraform list, map, set, and type-conversion functions, plus for expressions and transformation patterns for dynamic infrastructure code.

10 min read·Easy

Explore Related Topics

AW

AWS Tutorials

Provision AWS infrastructure with Terraform

CI

CI/CD Tutorials

Run terraform plan/apply in CI pipelines

Try the Tool

YAML Validator

Validate Terraform and Kubernetes YAML configs before applying.

On This Page

What Are Dynamic Blocks?Why Dynamic Blocks ExistDynamic Block SyntaxA Basic Security Group ExampleUsing the Current Item Inside `content`Dynamic Blocks in Security GroupsHardcoded approachDynamic approachDynamic Blocks in IAM PoliciesWhy this mattersNesting Dynamic BlocksWhen nested dynamic blocks are worth itWhen nested dynamic blocks are riskyDynamic Block vs Multiple Hardcoded BlocksHardcoded blocks are often clearerDynamic blocks are often better for reusable modulesWhen NOT to Use Dynamic BlocksAvoid dynamic blocks when the config is tiny and fixedAvoid dynamic blocks when they hide important intentAvoid dynamic blocks when input complexity becomes the real problemA Complete Practical ExampleBest Practices for Dynamic Blocks1. Use them for repeated nested blocks, not for everything2. Keep input data well-typed3. Consider an explicit `iterator`4. Prefer explicit blocks when the structure is small5. Be extra careful with security-related abstractionsCommon Beginner MistakesMistake 1: Using dynamic blocks to repeat whole resourcesMistake 2: Making simple configuration harder to readMistake 3: Confusing iteratorsMistake 4: Over-nesting everythingFinal ThoughtsKnowledge Check