# Terraform Dynamic Blocks: Generate Nested Blocks from Variables

> How to use dynamic blocks in Terraform and OpenTofu to generate repeated nested blocks such as security group rules, with for_each, iterator and AWS examples.

- Source: https://www.itwonderlab.com/terraform-dynamic-blocks/
- Published: 2026-02-10
- Updated: 2026-02-10
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## 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

```hcl title="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

```hcl title="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:

```hcl title="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:

```hcl title="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:

```hcl title="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.

> [!TIP]
> For [AWS security groups](https://www.itwonderlab.com/aws-terraform-tutorial-aws-security-groups/), the separate resources `aws_vpc_security_group_ingress_rule` and `aws_vpc_security_group_egress_rule` with `for_each` are often easier to maintain than inline `ingress` blocks with `dynamic`, because each rule is an independent resource. See [for_each vs count](https://www.itwonderlab.com/terraform-for-each-vs-count/).
