Terraform Dynamic Blocks, Count & For Each
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.
Related Concepts
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