Skip to content

Terraform Dynamic Blocks, Count & For Each

DodaTech 5 min read

Terraform dynamic blocks, the count meta-argument, and for_each expressions enable you to create multiple resources, iterate over collections, and generate nested configurations dynamically from variables.

What You'll Learn

In this tutorial, you will learn how to use Terraform count and for_each for resource creation, dynamic blocks for nested configuration generation, and when to choose each approach for different use cases.

Why It Matters

Hardcoding multiple resource blocks is repetitive and error-prone. Count, for_each, and dynamic blocks let you manage variable numbers of resources from a single definition, reducing code duplication and enabling data-driven infrastructure.

Real-World Use

DodaTech uses for_each to create per-service IAM roles from a map variable. Durga Antivirus Pro's networking team uses dynamic blocks to generate ingress rules from a list of port configurations, adding or removing rules by editing a single variable.

Count

The count meta-argument creates a specified number of identical resources:

variable "subnet_cidrs" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

resource "aws_subnet" "public" {
  count             = length(var.subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "public-subnet-${count.index + 1}"
  }
}

Expected output: Creates three subnets, one per CIDR. Resources are indexed as aws_subnet.public[0], aws_subnet.public[1], aws_subnet.public[2].

Count with Conditional

resource "aws_instance" "bastion" {
  count         = var.create_bastion ? 1 : 0
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

Expected output: When create_bastion is true, one bastion instance is created. When false, the resource block produces zero instances.

For Each

For each creates resources from a map or set of strings, keying each resource by a unique identifier:

variable "security_groups" {
  type = map(object({
    description = string
    ingress_ports = list(number)
  }))

  default = {
    web = {
      description   = "Web server security group"
      ingress_ports = [80, 443]
    }
    ssh = {
      description   = "SSH access security group"
      ingress_ports = [22]
    }
    api = {
      description   = "API server security group"
      ingress_ports = [8080, 8443]
    }
  }
}

resource "aws_security_group" "this" {
  for_each = var.security_groups

  name        = "sg-${each.key}"
  description = each.value.description
  vpc_id      = aws_vpc.main.id
}

Expected output: Creates three security groups named sg-web, sg-ssh, and sg-api. Resources are referenced as aws_security_group.this["web"].

For Each with Set of Strings

variable "bucket_names" {
  type    = set(string)
  default = ["logs", "backups", "artifacts"]
}

resource "aws_s3_bucket" "data" {
  for_each = var.bucket_names

  bucket = "dodatech-${each.key}-${var.environment}"
  tags = {
    Name = each.key
  }
}

Expected output: Creates three S3 buckets with names like dodatech-logs-production.

Dynamic Blocks

Dynamic blocks generate nested configuration blocks within a resource:

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"] },
    { from_port = 22, to_port = 22, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"] },
  ]
}

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

Expected output: Generates three ingress blocks -- HTTP, HTTPS open to all, and SSH restricted to the internal network. Additional rules are added by extending the variable list.

Count vs For Each

Aspect Count For Each
Input type Number Map or set of strings
Resource key Integer index String key from map
Stable addressing No (index shifts) Yes (key-based)
Conditional creation count = condition ? 1 : 0 Not directly supported
Best for Identical resources Resources with distinct configurations

Common Mistakes

1. Count Index Shifting

Removing an item from the middle of a list shifts all indices. Terraform destroys and recreates resources. Use for_each with a map for stable addressing.

2. Using Count with Sets of Strings

Count expects a number, not a set. Use count = length(var.list) and access by index, or use for_each directly.

3. Dynamic Block on Non-Repeatable Blocks

Not all nested blocks support dynamic generation. Check provider documentation for which blocks accept dynamic.

4. Empty For Each

Passing an empty map or set to for_each creates zero resources. This is valid but verify your variable has at least one element.

5. Mixing Count and For Each on the Same Resource

A resource cannot use both count and for_each. Choose one approach per resource block.

Practice Questions

1. What is the difference between count and for_each? Count creates N identical resources indexed by integer. For each creates resources from a map or set, keyed by string.

2. Why does count cause resource recreation when the list changes? Removing an item shifts all subsequent indices. Terraform sees index changes as delete-and-create operations.

3. What is a dynamic block used for? Dynamic blocks generate nested configuration blocks within a resource from a collection, avoiding repetitive block definitions.

4. How do you conditionally create a resource with for_each? Use a conditional map: for_each = var.create ? { key = value } : {}. An empty map produces zero resources.

5. Challenge: Write a configuration that uses for_each to create S3 buckets from a map variable, count to create subnets from a CIDR list, and dynamic blocks to generate security group rules.

Mini Project: Dynamic Infrastructure Library

Create a reusable module that accepts a map of security group configurations (name, description, ingress rules) and uses dynamic blocks to generate the full security group resources. Test with three different configurations.

Remote Backends
Built-in Functions

What's Next

Master Terraform dynamic blocks and iteration, then explore Built-in Functions for string manipulation, type conversion, and file operations.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro