Scale Infrastructure with Code: count, for_each & Dynamic Blocks
When you need to provision 5 identical worker nodes, 10 IAM users, or 15 security group firewall rules, you don't duplicate code blocks. Terraform provides count, for_each, and dynamic blocks to iterate cleanly across lists, sets, and maps.
Choosing the Right Iteration Method
| Method | Iterates Over | Resource Address Example | Best Used For |
|---|---|---|---|
count | Whole Number (integer) | aws_instance.web[0] | Identical clones or conditional toggle (0 or 1) |
for_each | Set of strings or Map of objects | aws_instance.web["api"] | Distinct resources with unique configurations (IAM, Subnets) |
dynamic | List/Map inside a resource | ingress { ... } | Repeating nested blocks (Security group rules, tags, routes) |
1. The count Meta-Argument & Conditionals
# Launch 3 web servers with index numbering
resource "aws_instance" "web" {
count = 3
ami = "ami-0123456789abcdef0"
instance_type = "t3.micro"
tags = {
Name = "web-server-${count.index + 1}"
}
}# If enable_bastion is true -> count is 1 (created)
# If enable_bastion is false -> count is 0 (skipped)
resource "aws_instance" "bastion" {
count = var.enable_bastion ? 1 : 0
ami = "ami-0123456789abcdef0"
instance_type = "t3.nano"
}The Index Shift Flaw: Why count Breaks on Lists
Suppose you use count = length(var.users) with ["alice", "bob", "charlie"]. If "alice" leaves the company and is removed from index 0, "bob" becomes index 0 and "charlie" becomes index 1. Terraform will destroy and recreate every single user after the deleted item!
Unintended Destruction
On stateful resources (like databases or storage), index shifting can destroy and replace live production data. Always use for_each when iterating over distinct named items!
What Terraform Does with count
- users[0] = "alice" DELETED from code
- →
- users[0] becomes "bob" Terraform updates index 0 from alice to bob
- →
- users[2] destroyed Total count shrank from 3 to 2
2. The for_each Meta-Argument (The Safe Pattern)
variable "usernames" {
type = list(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
for_each = toset(var.usernames)
name = each.value
}locals {
subnets = {
pub-1a = { cidr = "10.0.1.0/24", az = "ap-south-1a" }
pub-1b = { cidr = "10.0.2.0/24", az = "ap-south-1b" }
}
}
resource "aws_subnet" "main" {
for_each = local.subnets
vpc_id = aws_vpc.prod.id
cidr_block = each.value.cidr
availability_zone = each.value.az
}3. for Comprehensions & Splat Expressions
# 1. Transform list of user names to uppercase
output "upper_users" {
value = [for u in var.usernames : upper(u)]
}
# 2. Extract ARN map from for_each resource
output "user_arns" {
value = { for k, user in aws_iam_user.team : k => user.arn }
}# Splat [*] extracts an attribute across all count items
output "all_server_ips" {
value = aws_instance.web[*].public_ip
}4. Dynamic Blocks for Nested Resource Configurations
locals {
ingress_ports = [80, 443, 8080, 22]
}
resource "aws_security_group" "web_sg" {
name = "web-traffic-sg"
description = "Allow HTTP, HTTPS, WebApp, and SSH"
dynamic "ingress" {
for_each = local.ingress_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}Eliminate 40 Lines of Copy-Paste
Instead of writing out 4 separate ingress { ... } blocks with identical protocol and CIDR definitions, a dynamic "ingress" block generates them cleanly from a list variable or local value.