Terraform count vs for_each: Instance Addresses Are a Contract, Not a Style Choice
Removing a list item with Terraform count can cause a cascade of destroy/recreate actions for all later instances. Using for_each keyed by a stable identifier avoids that by giving each instance a unique address.
04 Apr 2026, 23:05 UTC

You have three app servers defined with count, and someone removes the middle entry from the input list. The plan says Terraform will destroy two servers and create two new ones — even though only one server actually went away. Nothing about your infrastructure changed except position in a list, and that is the whole problem.
The takeaway: count and for_each differ in one thing that matters — how instances are addressed in state. count addresses by numeric index (aws_instance.app[0]), so identity is positional. for_each addresses by map or set key (aws_instance.app[\"web-eu\"]), so identity follows the key. Because those addresses appear in state, plans, outputs, and every downstream reference, choosing between them is a contract with your future refactors, not a stylistic preference.
Why index-based removal cascades
With count, Terraform derives each instance from an element of the driving list by position. Delete the middle element and every later element slides down one slot. Terraform then compares index by index: instance [1] now looks like it should have the configuration of the old [2], and [2] no longer exists. For most resource types, changing the arguments that differ means an in-place update at best and a destroy/recreate at worst — typically the latter when the changed attribute forces replacement, like a name or availability zone.
The result is the classic surprise: you removed one item and the plan touches everything after it. On stateful resources — databases, disks, anything with data you care about — that plan is not just noisy, it is dangerous if applied carelessly.
Keyed maps make identity explicit
for_each accepts a map or a set of strings, and each instance's key becomes part of its state address. Removing a key destroys exactly that instance and nothing else. Adding a key creates exactly one new instance. Reordering the input changes nothing, because maps have no order.
The common pattern is to transform a list of objects into a map keyed by a stable identifier with a for expression:
variable \"servers\" {
type = list(object({
name = string
size = string
}))
}
locals {
servers_by_name = { for s in var.servers : s.name => s }
}
resource \"aws_instance\" \"app\" {
for_each = local.servers_by_name
instance_type = each.value.size
tags = { Name = each.key }
}
Run this in any root module with the AWS provider configured; no special permissions beyond a normal terraform plan. Now removing the web-eu entry from the variable plans a single destroy of aws_instance.app[\"web-eu\"]. Verify with terraform state list after applying: you will see keyed addresses instead of numeric ones.
Where count is still the right tool
count = 0 or count = 1 remains the idiomatic way to conditionally create a single resource — feature flags, per-environment toggles like var.enable_monitoring ? 1 : 0. There is exactly one instance, so positional identity cannot hurt you. A codebase that prefers for_each for replication and keeps count for on/off switches is being consistent, not confused.
Some teams also use for_each = toset(var.names) for simple unique-string inputs. Be careful: toset() silently deduplicates. A duplicated name in the input becomes one missing instance rather than an error, which can mask a configuration bug. If duplicates would indicate a mistake, prefer the map form — colliding keys in a for expression produce an explicit error.
The trade-offs you are signing up for
- Keys are part of the address. Renaming a key — even cosmetically — plans a destroy of the old instance and a create of the new one. Keys must be stable identifiers (name, region, environment), never mutable labels like dates or ticket numbers. Keys are also case-sensitive.
- Moving between the two rewrites every address. Switching an existing resource from
counttofor_eachwould normally plan a full destroy/recreate. Terraform 1.1+movedblocks let you remap addresses explicitly, e.g. fromaws_instance.app[0]toaws_instance.app[\"web-us\"], so the plan shows a move instead. Each mapping must be declared individually, so budget the toil for large instance counts. - Plan output gets longer to read. Keyed addresses are more meaningful but more verbose; reviewers need to actually read keys rather than scan indices.
A decision rule you can apply today
Ask one question: will items ever be removed or reordered from this collection? If yes, use for_each keyed by a stable identifier. If the resource is a singleton gated by a boolean, use count. If you are inheriting a count-based resource that now needs surgical removals, plan the migration with moved blocks and verify with terraform plan that it shows moves, not replacements, before applying.
Behavior described here reflects Terraform 0.12+ for the meta-arguments and 1.1+ for moved blocks; confirm syntax against the docs for your pinned version, since error messages and edge cases differ. The fastest way to build intuition is a scratch directory with a cheap test resource: apply three instances with count, delete the middle element, and read the plan. Then repeat with for_each and compare. The diff between those two plans is the entire argument.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.