TL;DR:
countaddresses resources by positional index, so removing an item from the middle of the list shifts every later item's address and Terraform reassociates later addresses with different configuration. Whether each update is in place or a replacement depends on the attributes and provider.for_eachaddresses by a stable string key, so removing one item touches only that one.
How to approach it
Frame it as a state addressing problem, not a syntax preference. Then walk a concrete plan. This question rewards showing the actual resource addresses.
A strong answer
Terraform records each instance in state under an address. With count that address is aws_instance.web[0], [1], [2]. The index is the identity. With for_each it is aws_instance.web["api"], ["worker"], and the map key is the identity.
Now delete the middle element of a three-item list under count. The list becomes two items, so [1] is now what used to be [2], and [2] no longer exists. Terraform compares desired to state and concludes that instance 1 must change to a different configuration and instance 2 must be destroyed. A changed Name tag on an EC2 instance is normally an in-place update, as in the fragments below. Changing an immutable attribute can instead force replacement. The unwanted identity reassignment is the issue in either case.
These are alternative addressing fragments, not one runnable module; each EC2 resource also needs a suitable AMI and instance type. Do not declare both with the same address.
# fragile: identity is the position in the list
variable "names" { default = ["api", "worker", "cron"] }
resource "aws_instance" "web" {
count = length(var.names)
tags = { Name = var.names[count.index] }
}
# removing "worker" -> plans changes to 1 and destroys 2
# stable: identity is the key
resource "aws_instance" "web" {
for_each = toset(var.names)
tags = { Name = each.key }
}
# removing "worker" -> destroys exactly aws_instance.web["worker"]
The rule I use: count is for a homogeneous fleet where the members have no identity, and for the conditional-creation idiom count = var.enabled ? 1 : 0. Everything else uses for_each. If the resources have names, roles, or attached state, they have identity, and identity belongs in the key.
The migration matters too. Moving an existing resource from count to for_each is not a no-op, because every state address changes. You either use terraform state mv 'aws_instance.web[0]' 'aws_instance.web["api"]' for each instance, or in newer versions a moved block in the configuration, which is reviewable and survives being run by someone else.
One further trap: for_each keys must be known at plan time. Keying on an attribute of a not-yet-created resource produces the "Invalid for_each argument" error, and the fix is to key on something static such as an input variable rather than a computed value.
What interviewers probe next
"How would you fix this without downtime on a running fleet?" moved blocks or state mv, verified with a plan that shows zero changes. A plan that shows zero changes is the proof; unexpected changes require investigation: they may reflect drift or input changes as well as an incorrect mapping.
"When is count still the right answer?" Conditional creation, and identical members with no individual identity. Say the second part explicitly, because it is the honest limit of the rule.
"What happens if two keys collide in a toset?" The set deduplicates, so you silently get fewer resources than list entries. Worth a validation if the input is user-supplied.
Common mistakes
Answering "use for_each, it is better" with no mechanism. The interviewer is testing whether you know why, because the why is what lets you predict the plan.
Forgetting the migration cost and implying you can swap the meta-argument in place. That answer has caused real outages.
Describing the difference as a style choice. It is a state identity choice, and state identity is what Terraform is.
Sources: Terraform count.