Terraform Dynamic Blocks with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
kreuzwerker/docker 3.9.0
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform and Docker — Terraform lab environment on Ubuntu
Privilege Normal user (Docker socket access required)
Scope Terraform dynamic block syntax, for_each, iterator, content, conditional nested blocks, docker_container labels and ports lab, one nested dynamic example, comparison with resource for_each and for expressions, readability guidance, limitations, and common errors. Does not cover count or for_each on resources in depth, full module design, or cloud-only schemas.
Related guides Terraform count vs for_each
Terraform expressions
Terraform resources
Terraform variables
Terraform Associate certification course

Some provider schemas expect the same nested block repeated many times — container labels, security group rules, listener ports. Copy-pasting ten identical labels blocks works until the list grows or comes from a variable.

A dynamic block generates those repetitions from a for_each collection. The result is still one docker_container (or one aws_security_group, or one azurerm_* resource) with nested blocks expanded before Terraform talks to the provider.

This guide walks through real docker_container examples on Terraform 1.15.8 — static blocks first, then dynamic refactors you can plan, apply, and verify with docker inspect.

NOTE
Work in ~/terraform-labs/terraform-dynamic-block/ on the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before plan or apply. Examples use the Docker provider and docker_container nested labels, ports, and mounts blocks validated against provider schema 3.9.0. The lab image is nginx:alpine, which listens on container port 80 by default. If Docker Hub rate-limits pulls, use the official mirror public.ecr.aws/docker/library/nginx:alpine in docker_image.nginx.

What is a Terraform dynamic block?

A dynamic block is language sugar for repeatable nested blocks. Terraform evaluates the for_each collection, then materializes one nested block per element inside the parent block.

text
resource for_each  →  multiple resource instances (docker_container.web["a"], docker_container.web["b"])
dynamic block      →  multiple nested blocks inside one instance (many labels blocks inside docker_container.web)

Dynamic blocks do not create arbitrary resource blocks. You cannot write dynamic "aws_instance" and spawn instances from a loop — that remains the job of count and for_each on resources.

They apply wherever the parent schema allows nested blocks: resource, data, provider, and provisioner configurations that declare repeatable block types.


Dynamic block syntax

Every dynamic block shares the same shape:

hcl
dynamic "<NESTED_BLOCK_TYPE>" {
  for_each = <collection>
  iterator   = <name>   # optional; defaults to the block type name
  labels     = [...]    # only when the nested block type uses block labels

  content {
    # arguments for one nested block instance
  }
}
Piece Role
"<NESTED_BLOCK_TYPE>" Must match a nested block type in the provider schema (labels, ports, mounts, …)
for_each Collection or structural value with one element per nested block to generate
iterator Optional alias for the current element; default is the block type name (labels, ports, …)
labels Required only for nested block types that use block labels in the schema
content Body of one generated nested block; references use iterator.key and iterator.value

Inside content, the default iterator exposes .key and .value. For a collection of objects, .value is the current object. For lists and tuples, .key is the element index; for maps it is the map key. For sets, key is identical to value, so HashiCorp recommends not relying on it.

Unlike resource-level for_each, a dynamic block is not creating independently addressed resource instances. Its for_each can therefore iterate over collection or structural values appropriate to the nested-block generation — a broader set than the map-or-set-of-strings rule that governs resource for_each.

count is not valid on a dynamic block. To make a nested block optional, drive for_each with a conditional collection instead.


Basic dynamic block example

The main lab at ~/terraform-labs/terraform-dynamic-block/main/ starts with repeated static labels blocks, then refactors them to dynamic "labels".

Static version (commented in the lab file for comparison):

hcl
labels {
  label = "managed_by"
  value = "terraform"
}

labels {
  label = "lab"
  value = "dynamic-block"
}

The same labels driven by a list variable:

hcl
dynamic "labels" {
  for_each = var.container_labels
  content {
    label = labels.value.label
    value = labels.value.value
  }
}

var.container_labels defaults to two objects (managed_by, lab). Terraform generates one labels nested block per list element.

Initialize the lab directory:

bash
cd ~/terraform-labs/terraform-dynamic-block/main && terraform init

Apply the configuration so the dynamic labels and conditional port publish to a real container:

bash
cd ~/terraform-labs/terraform-dynamic-block/main && terraform apply -auto-approve -input=false -no-color

The apply creates a network, pulls nginx:alpine, and starts tf-dynamic-block-main with four dynamic label blocks and one ports block.

Confirm Docker published the host port mapping:

bash
docker ps --filter name=tf-dynamic-block-main --format '{{.Names}} {{.Ports}}'
output
tf-dynamic-block-main 0.0.0.0:18080->80/tcp

The mapping points host port 18080 at container port 80, where nginx:alpine listens by default. Verify the dynamic ports block actually forwards traffic:

bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080
output
200

An HTTP 200 confirms the generated nested ports block works end to end, not only that Docker recorded a mapping.

Inspect the Terraform-managed labels:

bash
docker inspect tf-dynamic-block-main --format '{{json .Config.Labels}}'
output
{"env":"lab","lab":"dynamic-block","managed_by":"terraform","project":"terraform-dynamic-block"}

Four keys appear because the lab runs two dynamic "labels" blocks — one over the list variable and one over a map — which is equivalent to writing four static labels blocks.


Iterator and conditional dynamic blocks

dynamic with for_each on a map

The second dynamic "labels" block in the lab iterates var.tags with a custom iterator:

hcl
dynamic "labels" {
  for_each = var.tags
  iterator = tag

  content {
    label = tag.key
    value = tag.value
  }
}

When iterator is omitted, Terraform names the iterator after the block type. With iterator = tag, references inside content must use tag.key and tag.value, not labels.key.

Conditional nested blocks

To include a ports block only when publishing is enabled:

hcl
dynamic "ports" {
  for_each = var.expose_port ? [{ internal = 80, external = var.host_port }] : []
  content {
    internal = ports.value.internal
    external = ports.value.external
  }
}

When var.expose_port is false, for_each is an empty list and Terraform generates zero ports blocks. This replaces patterns like count = var.enabled ? 1 : 0 that work on resources but are unavailable inside dynamic.

Set expose_port to false in a tfvars file or variable override and run plan — the ports nested block disappears from the diff while the container resource remains.


Nested dynamic blocks

You can nest dynamic inside content when the schema has nested block types inside nested block types. Keep depth shallow; one practical example is enough for most modules.

The nested/ lab mounts a volume with labels on volume_options:

hcl
dynamic "mounts" {
  for_each = var.mount_specs
  content {
    type   = "volume"
    source = docker_volume.data.name
    target = mounts.value.target

    dynamic "volume_options" {
      for_each = length(mounts.value.labels) > 0 ? [mounts.value.labels] : []
      content {
        dynamic "labels" {
          for_each = volume_options.value
          iterator = vol_label

          content {
            label = vol_label.key
            value = vol_label.value
          }
        }
      }
    }
  }
}

Each iterator name (mounts, volume_options, vol_label) scopes references in its content block. Reusing the wrong name is a common source of validate errors.

Initialize and apply the nested example:

bash
cd ~/terraform-labs/terraform-dynamic-block/nested && terraform init

Apply so Terraform creates the volume mount with nested dynamic labels:

bash
cd ~/terraform-labs/terraform-dynamic-block/nested && terraform apply -auto-approve -input=false -no-color

Plan and apply succeed with three resources (docker_volume, docker_image, docker_container). The plan shows mounts with nested volume_options and labels blocks generated from var.mount_specs.


Dynamic blocks vs resource for_each and for expressions

These three constructs look similar but solve different problems:

Mechanism What it produces Example address or result
Resource for_each Multiple resource instances docker_container.web["api"]
dynamic block Repeated nested blocks in one instance Four labels blocks inside docker_container.web
for expression A list, map, or object value [for x in var.names : upper(x)]
text
resource for_each  →  N separate resources
dynamic            →  N nested blocks inside 1 resource
for expression     →  1 value (collection or structure)

Use resource for_each when each collection element should be its own managed object with a distinct state address. Use dynamic when the provider expects repeated nested configuration inside a single object. Use a for expression when you need to transform data, not emit nested blocks.

Full meta-argument workflows live in Terraform count vs for_each. Terraform expressions covers for expression syntax in depth.


When not to use dynamic blocks and limitations

Dynamic blocks are not automatically clearer than static blocks. If a resource needs one or two nested blocks, write them explicitly. Reach for dynamic when repetition is real and driven by variables.

Signs you may have gone too far:

  • Nearly every nested block in the resource is dynamic, and the file is harder to read than a flat list
  • You are generating nested blocks to work around a missing resource for_each — split into multiple resources instead
  • Iterator names are reused carelessly across three or more nesting levels

Schema and meta-argument limits

Dynamic blocks can only generate nested block types the provider schema defines for that parent block. They cannot invent block types (metadata on docker_container fails validate) and cannot generate resource-level meta-argument blocks such as lifecycle or provider.

Terraform evaluates a dynamic block and generates ordinary nested blocks before passing the resulting resource configuration to the provider. The provider still sees static nested blocks — dynamic is Terraform language behavior, similar to a for expression that produces blocks rather than a single value.

for_each on a dynamic block must be a collection or structural value Terraform can iterate — not a bare string or number. That acceptance is broader than resource-level for_each, which is limited to maps and sets of strings for instance addressing.

Functions such as file() inside a dynamic content block follow the same rules as static nested blocks. They do not join the dependency graph differently.


Common Terraform dynamic block errors

The errors/ tree under ~/terraform-labs/terraform-dynamic-block/ reproduces failures you are likely to see in review.

Symptom Likely cause Fix
Invalid dynamic for_each value for_each is not iterable Pass a collection or structural value, not a bare string or number
Reference to undeclared resource on labels.key Custom iterator set but default name used in content Reference tag.key when iterator = tag
Unsupported block type Nested block name not in schema Check provider docs; use only declared block types
Plan shows zero nested blocks for_each is empty Confirm variable values; conditional may be false
Duplicate nested block keys Set-style nesting deduplicates Ensure label/port keys are unique per schema rules

Invalid for_each type — errors/invalid-for-each/:

bash
cd ~/terraform-labs/terraform-dynamic-block/errors/invalid-for-each && terraform init && terraform validate -no-color
output
Error: Invalid dynamic for_each value

  on main.tf line 10, in resource "docker_container" "web":
  10:     for_each = "not-a-collection"

Cannot use a string value in for_each. An iterable collection is required.

Wrong iterator reference — errors/wrong-iterator/:

bash
cd ~/terraform-labs/terraform-dynamic-block/errors/wrong-iterator && terraform init && terraform validate -no-color
output
Error: Reference to undeclared resource

  on main.tf line 14, in resource "docker_container" "web":
  14:       label = labels.key

A managed resource "labels" "key" has not been declared in the root module.

Unsupported nested block — errors/unsupported-block/:

bash
cd ~/terraform-labs/terraform-dynamic-block/errors/unsupported-block && terraform init && terraform validate -no-color
output
Error: Unsupported block type

  on main.tf line 9, in resource "docker_container" "web":
   9:   dynamic "metadata" {

Blocks of type "metadata" are not expected here.

Official References


Summary

You started from repeated static nested blocks on docker_container, then refactored them to dynamic "labels" and dynamic "ports" driven by variables. The for_each collection controls how many nested blocks Terraform generates; the optional iterator argument renames the loop variable inside content.

Conditional nested blocks use for_each = condition ? [one_element] : [] because count is not available on dynamic. That pattern publishes container port 80 on host port 18080 when var.expose_port is true — matching the default nginx:alpine listener. Nested dynamic blocks work when the schema supports deeper block types — the lab mounts example chains mounts, volume_options, and labels with distinct iterator names.

Dynamic blocks repeat nested configuration inside one resource. Resource for_each creates multiple instances; for expressions transform values. Do not use dynamic to spawn resources or to replace readable static configuration. When labels or rules are few and fixed, explicit blocks are often the better choice. Next, compare instance repetition in Terraform count vs for_each or wire dynamic labels into Terraform variables from tfvars.


Frequently Asked Questions

1. What is a Terraform dynamic block?

A dynamic block generates zero or more nested configuration blocks inside a resource, data, provider, or provisioner block. Terraform expands each dynamic block into ordinary nested blocks at plan time based on a for_each collection.

2. Can dynamic blocks create Terraform resources?

No. Dynamic blocks only repeat nested blocks the surrounding schema already supports. To create multiple resource instances, use count or for_each on the resource block itself.

3. What is the difference between dynamic and for_each on a resource?

Resource for_each creates multiple instances of the same resource type, each with its own address such as docker_container.web["api"]. A dynamic block repeats nested blocks inside one resource instance, such as multiple labels blocks inside a single docker_container.

4. Why is count not available inside a dynamic block?

Dynamic blocks use for_each only. To include or omit a nested block conditionally, set for_each to a single-element list or map when the condition is true, or to an empty collection when it is false.

5. When should I use a dynamic block?

Use a dynamic block when a provider schema expects repeated nested blocks and the number of repetitions comes from a variable list or map. If nearly the entire resource body becomes dynamic, explicit static blocks or separate resources are often easier to read.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)