# terraform / iteration / meta-arguments

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.

for_each = toset([...])STABLE
each.key"prod-web-01"
addressaws_instance.server["prod-web-01"]
drift safetyZero Index Shifting
count
Numeric & Conditionals
for_each
Named Sets & Maps
dynamic
Nested Blocks
# comparison

Choosing the Right Iteration Method

MethodIterates OverResource Address ExampleBest 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)
# count

1. The count Meta-Argument & Conditionals

main.tfCOUNT MULTIPLIER
1 2 3 4 5 6 7 8 9 10
# 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}"
  }
}
bastion.tfCONDITIONAL TOGGLE
1 2 3 4 5 6 7 8 9
# 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"
}
# pitfall

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!

production danger

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

2. The for_each Meta-Argument (The Safe Pattern)

iam.tfITERATING OVER A SET
1 2 3 4 5 6 7 8 9 10 11
variable "usernames" {
  type    = list(string)
  default = ["alice", "bob", "charlie"]
}

resource "aws_iam_user" "team" {
  for_each = toset(var.usernames)
  name     = each.value
}
subnets.tfITERATING OVER A MAP
1 2 3 4 5 6 7 8 9 10 11 12 13
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
}
# transformations

3. for Comprehensions & Splat Expressions

outputs.tfLIST & MAP COMPREHENSIONS
1 2 3 4 5 6 7 8 9 10 11
# 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.tfSPLAT OPERATOR [*]
1 2 3 4 5 6
# Splat [*] extracts an attribute across all count items
output "all_server_ips" {
  value = aws_instance.web[*].public_ip
}
# dynamic

4. Dynamic Blocks for Nested Resource Configurations

security_group.tfDYNAMIC INGRESS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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"]
    }
  }
}
clean code

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.