Every Terraform engineer eventually has the same incident: they remove one element from the middle of a list, run terraform plan, and watch in horror as Terraform proposes to destroy and recreate half their fleet. The root cause is almost always count, and the fix is almost always for_each.
This is also why the Terraform Associate exam returns to this comparison so reliably — it’s the cleanest test of whether you understand how Terraform tracks resources. Here’s the full picture: how each meta-argument works, where each one breaks, and the decision rule that survives both the exam and production.
The Problem They Both Solve
Without meta-arguments, one resource block manages exactly one object. Need three subnets? You’d copy-paste three blocks. count and for_each let a single block manage multiple instances of a resource — they differ entirely in how those instances are identified in state.
That identification detail is everything. It decides what happens when your input changes.
How count Works
count takes a whole number and creates that many instances, identified by position:
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 are numeric:
aws_subnet.main[0]
aws_subnet.main[1]
aws_subnet.main[2]
Where count breaks: index shifting
Delete "10.0.2.0/24" — the middle element — and re-plan. The list is now two elements long, so Terraform wants:
aws_subnet.main[1]to change from10.0.2.0/24to10.0.3.0/24(a destroy-and-recreate, since CIDR forces replacement)aws_subnet.main[2]to be destroyed
You removed one subnet; Terraform churns two. Every instance after the removed element shifts down an index, and Terraform sees each shift as a change to that resource. With stateful resources — databases, volumes, NAT gateways — this is an outage generator.
How for_each Works
for_each takes a map or a set of strings and creates one instance per element, identified by key:
variable "subnets" {
default = {
web = "10.0.1.0/24"
app = "10.0.2.0/24"
data = "10.0.3.0/24"
}
}
resource "aws_subnet" "main" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value
tags = { Name = each.key }
}
State addresses are named:
aws_subnet.main["web"]
aws_subnet.main["app"]
aws_subnet.main["data"]
Delete the app entry and re-plan: Terraform destroys exactly aws_subnet.main["app"] and touches nothing else. The identity of web and data never depended on app existing. This is the entire argument for for_each.
Inside the block you get each.key and each.value (for a set of strings, they’re the same value).
The map/set requirement
for_each does not accept a plain list — this is a favorite exam distractor. Hand it a list and you’ll get an error; the fix is toset():
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.value
}
Two more constraints worth knowing for the exam:
- The keys of a
for_eachmap must be known at plan time — you can’t key on an attribute that doesn’t exist until apply. - Sensitive values can’t be used in
for_eachkeys, because keys appear in state addresses and plan output.
The One Place count Still Wins
count remains the idiom for conditionally creating a resource:
resource "aws_cloudwatch_dashboard" "ops" {
count = var.enable_dashboard ? 1 : 0
# ...
}
Zero or one instance, no identity problem possible. You’ll also reach for count when you genuinely need N identical, interchangeable copies of something — stateless workers where instance #2 means nothing.
The Decision Rule
| Situation | Use |
|---|---|
| Instances are distinct things with their own identity (per-env, per-team, per-AZ) | for_each |
| The input collection will grow and shrink over time | for_each |
| Resources are stateful (databases, volumes, DNS records) | for_each |
| Conditional create: 0 or 1 instances | count |
| N truly identical, disposable copies | count |
If you remember one sentence: count tracks position, for_each tracks identity — and removing an element only behaves the way you expect when identity is what’s tracked.
Bonus: for_each on dynamic Blocks and Modules
The same mechanism extends beyond resources, and the exam expects you to know both:
Dynamic blocks generate repeated nested blocks inside one resource:
resource "aws_security_group" "web" {
name = "web"
dynamic "ingress" {
for_each = var.ingress_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Modules accept count and for_each directly — one module block can instantiate a full stack per environment:
module "network" {
source = "./modules/network"
for_each = var.environments
cidr_block = each.value.cidr
}
How the Exam Phrases It
Having reviewed how this topic appears across full-length practice exams, the question shapes are consistent:
- “A list element was removed — what does Terraform plan?” Tests whether you know
countshifts indexes (mass churn) whilefor_eachremoves one instance. - “Which types does
for_eachaccept?” Map or set of strings. A bare list is the trap answer. - “How do you create a resource only when a variable is true?” The
count = condition ? 1 : 0idiom. - Reading state addresses. Recognizing that
[0]meanscountand["web"]meansfor_each. each.key/each.valuesemantics, especially the set case where they’re identical.
A reliable rule of thumb from candidates: if a question shows a plan with a surprising number of destroyed resources, the answer involves count and index shifting. Drilling these under a real 60-minute timer is what makes the pattern automatic — the Sailor.sh Terraform Associate mock exam bundle includes this scenario family across its full-length mocks, with explanations of why each wrong option fails.
Migrating an Existing Resource from count to for_each
Real projects accumulate count early and regret it later. The migration without downtime:
- Change the resource block to
for_each. - Move each instance’s state address:
terraform state mv 'aws_subnet.main[0]' 'aws_subnet.main["web"]'— or, in modern Terraform, declaremovedblocks and letplando it declaratively. - Run
terraform planand confirm zero changes before applying.
(If state mv is unfamiliar, our Terraform state management guide covers the full subcommand family.)
Frequently Asked Questions
Q: Can I use count and for_each on the same resource?
A: No — they’re mutually exclusive meta-arguments. One per resource block.
Q: Why does for_each reject my list of objects?
A: for_each requires a map or set of strings. Convert with a for expression: for_each = { for s in var.subnets : s.name => s }.
Q: Does for_each work with providers?
A: No. count and for_each apply to resources, data sources, and modules — not provider blocks.
Q: Is one faster than the other? A: No meaningful performance difference. The difference is entirely about state identity and change behavior.
Keep Going
This is one of a handful of topics where one clean mental model wins you several exam questions. Pair it with the full Terraform Associate exam guide, warm up with free interactive TF-004 practice questions, and then validate under exam conditions with full-length timed mocks before you book the real thing.