Quick Answer
Use for_each when resources have meaningful names/keys (stable addressing). Use count for simple “create N copies” or conditional creation (0 or 1). for_each is safer because removing an item doesn’t reindex all other resources.
The Reindexing Problem with count
# count creates resources by index
resource "aws_subnet" "private" {
count = 3
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
}
# aws_subnet.private[0], aws_subnet.private[1], aws_subnet.private[2]
# If you remove the middle subnet, indexes shift:
# [0] stays, [1] becomes [0]'s old [2] — Terraform destroys and recreates!
# for_each uses keys — no reindexing
resource "aws_subnet" "private" {
for_each = toset(["us-east-1a", "us-east-1b", "us-east-1c"])
cidr_block = cidrsubnet(var.vpc_cidr, 8, index(tolist(toset(["us-east-1a", "us-east-1b", "us-east-1c"])), each.key))
availability_zone = each.key
}
# aws_subnet.private["us-east-1a"], aws_subnet.private["us-east-1b"], ...
# Removing "us-east-1b" only destroys that one subnet — others untouched
When to Use count
Conditional Resource Creation
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
# Reference: aws_instance.bastion[0].public_ip (when created)
Simple N Copies
resource "aws_instance" "worker" {
count = var.worker_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
tags = { Name = "worker-${count.index}" }
}
When to Use for_each
Named Resources from a Map
variable "subnets" {
type = map(object({
cidr = string
az = string
tier = string
}))
default = {
"private-a" = { cidr = "10.0.1.0/24", az = "us-east-1a", tier = "private" }
"private-b" = { cidr = "10.0.2.0/24", az = "us-east-1b", tier = "private" }
"public-a" = { cidr = "10.0.3.0/24", az = "us-east-1a", tier = "public" }
}
}
resource "aws_subnet" "main" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = { Name = each.key, Tier = each.value.tier }
}
Resources from a Set
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.value
}
Comparison Table
| Feature | count | for_each |
|---|---|---|
| Address format | resource[0], resource[1] | resource["key"] |
| On item removal | Reindexes — may destroy/recreate | Only removes that item |
| Input type | Number | Set or map |
| Conditional creation | ✅ count = 0 or 1 | ⚠️ Possible but verbose |
| Known at plan time | Must know number | Must know keys |
| Reference syntax | resource[0].id | resource["key"].id |
Converting count to for_each
# Before (count)
variable "az_list" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "private" {
count = length(var.az_list)
availability_zone = var.az_list[count.index]
}
# After (for_each)
resource "aws_subnet" "private" {
for_each = toset(var.az_list)
availability_zone = each.key
}
# Migrate state:
# terraform state mv 'aws_subnet.private[0]' 'aws_subnet.private["us-east-1a"]'
# terraform state mv 'aws_subnet.private[1]' 'aws_subnet.private["us-east-1b"]'
Related Articles
Conclusion
Default to for_each for most use cases — it creates stable, key-based resource addresses that don’t shift when items are added or removed. Use count for simple conditional creation (count = var.enabled ? 1 : 0) and fixed-number copies where order doesn’t matter.




