Both count and for_each create multiple instances of a resource. The difference: count uses numeric indexes, for_each uses string keys. This matters more than you’d think — count can destroy your resources when you remove an item from the middle of a list.
The Problem with count
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_subnet" "main" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
}
State addresses:
aws_subnet.main[0] → 10.0.1.0/24
aws_subnet.main[1] → 10.0.2.0/24
aws_subnet.main[2] → 10.0.3.0/24
Now remove the middle subnet (10.0.2.0/24):
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.3.0/24"] # Removed 10.0.2.0/24
}
terraform plan:
~ aws_subnet.main[1] 10.0.2.0/24 → 10.0.3.0/24 (update in-place — FAILS, can't change CIDR)
- aws_subnet.main[2] 10.0.3.0/24 (destroy!)
Count shifted all indexes down. Subnet [1] tries to change from 10.0.2.0 to 10.0.3.0 (which fails), and subnet [2] gets destroyed. This is production-breaking.
for_each Solves This
variable "subnets" {
default = {
"us-east-1a" = "10.0.1.0/24"
"us-east-1b" = "10.0.2.0/24"
"us-east-1c" = "10.0.3.0/24"
}
}
resource "aws_subnet" "main" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
}
State addresses:
aws_subnet.main["us-east-1a"] → 10.0.1.0/24
aws_subnet.main["us-east-1b"] → 10.0.2.0/24
aws_subnet.main["us-east-1c"] → 10.0.3.0/24
Remove the middle subnet:
variable "subnets" {
default = {
"us-east-1a" = "10.0.1.0/24"
"us-east-1c" = "10.0.3.0/24" # Only "us-east-1b" removed
}
}
terraform plan:
- aws_subnet.main["us-east-1b"] (destroy — only this one!)
Only the removed subnet is destroyed. The others are untouched.
Side-by-Side Comparison
| Feature | count | for_each |
|---|---|---|
| Key type | Numeric index [0], [1], [2] | String key [“name”], [“az”] |
| Remove middle item | ❌ Shifts all indexes (destructive) | ✅ Only removes that key |
| Reordering | ❌ Triggers destroy/create | ✅ Keys are stable |
| Input type | Number | Map or set of strings |
| Conditional | ✅ count = var.enabled ? 1 : 0 | ⚠️ More verbose |
| Reference | aws_subnet.main[0] | aws_subnet.main["us-east-1a"] |
| Best for | On/off toggles | Collections of distinct resources |
When to Use count
Toggle a Resource On/Off
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
tags = { Name = "bastion" }
}
# Reference:
output "bastion_ip" {
value = var.create_bastion ? aws_instance.bastion[0].public_ip : null
}
Create N Identical Resources
variable "worker_count" {
default = 3
}
resource "aws_instance" "worker" {
count = var.worker_count
ami = data.aws_ami.al2023.id
instance_type = "t3.medium"
tags = { Name = "worker-${count.index}" }
}
This is safe because the instances are interchangeable — it doesn’t matter if indexes shift.
When to Use for_each
Named Resources That Differ
variable "buckets" {
default = {
logs = {
versioning = true
lifecycle_days = 90
}
artifacts = {
versioning = true
lifecycle_days = 30
}
backups = {
versioning = false
lifecycle_days = 365
}
}
}
resource "aws_s3_bucket" "this" {
for_each = var.buckets
bucket = "${var.project}-${each.key}"
}
resource "aws_s3_bucket_versioning" "this" {
for_each = { for k, v in var.buckets : k => v if v.versioning }
bucket = aws_s3_bucket.this[each.key].id
versioning_configuration {
status = "Enabled"
}
}
Security Group Rules
variable "ingress_rules" {
default = {
http = { port = 80, cidr = "0.0.0.0/0" }
https = { port = 443, cidr = "0.0.0.0/0" }
ssh = { port = 22, cidr = "10.0.0.0/8" }
}
}
resource "aws_security_group_rule" "ingress" {
for_each = var.ingress_rules
type = "ingress"
security_group_id = aws_security_group.web.id
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = [each.value.cidr]
description = each.key
}
IAM Users
variable "developers" {
type = set(string)
default = ["alice", "bob", "carol"]
}
resource "aws_iam_user" "dev" {
for_each = var.developers
name = each.key
}
# Remove "bob" → only bob's user is deleted
# alice and carol are untouched
Converting count to for_each
Use moved blocks to avoid destroying resources:
# Old: count-based
# resource "aws_subnet" "public" {
# count = 3
# ...
# }
# New: for_each-based
moved {
from = aws_subnet.public[0]
to = aws_subnet.public["us-east-1a"]
}
moved {
from = aws_subnet.public[1]
to = aws_subnet.public["us-east-1b"]
}
moved {
from = aws_subnet.public[2]
to = aws_subnet.public["us-east-1c"]
}
resource "aws_subnet" "public" {
for_each = {
"us-east-1a" = "10.0.0.0/24"
"us-east-1b" = "10.0.1.0/24"
"us-east-1c" = "10.0.2.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
}
Common for_each Patterns
Convert List to Map
variable "names" {
default = ["web", "api", "worker"]
}
resource "aws_ecs_service" "this" {
# Convert list to set
for_each = toset(var.names)
name = each.key
}
Filter with for Expression
variable "instances" {
default = {
web = { type = "t3.micro", enabled = true }
api = { type = "t3.small", enabled = true }
legacy = { type = "t3.micro", enabled = false }
}
}
resource "aws_instance" "this" {
# Only create enabled instances
for_each = { for k, v in var.instances : k => v if v.enabled }
instance_type = each.value.type
ami = data.aws_ami.al2023.id
tags = { Name = each.key }
}
Rule of Thumb
Need to toggle on/off? → count (0 or 1)
N identical, interchangeable? → count (but for_each is still safer)
Named, distinct resources? → for_each (always)
Resources you might remove one? → for_each (always)
Hands-On Courses
- Terraform for Beginners on CopyPasteLearn
- Terraform By Example — practical code examples
Conclusion
Use count for simple on/off toggles (count = var.enabled ? 1 : 0). Use for_each for everything else — collections of resources, named items, anything where you might add or remove items. The index reordering problem with count has caused more production incidents than any other Terraform gotcha. When in doubt, use for_each.