Terraform for_each: Replacing Count's Fragile Indexing with Stable Keys
Terraform's for_each meta-argument solves count's index-shift problem by addressing resources with stable keys. See a worked IAM example, targeting commands, and the plan-time trade-offs to watch.
16 Mar 2026, 05:07 UTC

The index-shift problem
If you have used count to create a handful of similar resources, you have probably seen this: you remove the first item from a list, run terraform apply, and Terraform destroys the last resource and recreates the middle one. The cause is addressing. count identifies instances by numeric position—aws_iam_user.svc[0], aws_iam_user.svc[1]—so any insertion or deletion shifts every subsequent index.
The for_each meta-argument fixes this by addressing instances with stable keys instead of positions. For maps and sets, each instance gets a synthetic address built from each.key, so adding or removing an entry leaves the remaining resources untouched.
How for_each addresses resources
for_each accepts a map or a set of strings. Terraform creates one resource instance per key (for maps) or per element (for sets). Inside the resource block, each.key gives you the map key or set element, and each.value gives you the corresponding map value.
This changes the resource address from an index to a label. Instead of aws_iam_user.svc[0], you get aws_iam_user.svc["billing-api"]. That label is stable as long as the key does not change, which makes plans predictable and lets you target a single instance with terraform plan -target.
A worked example: service accounts
Suppose you manage IAM users for internal services. With count, adding a service in the middle of a list shifts every user after it. With for_each, you define a map keyed by service name.
variable "service_accounts" {
type = map(object({
environment = string
owner = string
}))
default = {
"billing-api" = { environment = "prod", owner = "finance" }
"search-api" = { environment = "staging", owner = "platform" }
}
}
resource "aws_iam_user" "service" {
for_each = var.service_accounts
name = each.key
path = "/${each.value.environment}/"
tags = {
Owner = each.value.owner
Env = each.value.environment
}
}
Run terraform plan in the directory containing this configuration. In the plan output, look for the Resource actions section: you should see two instances to be created, addressed as aws_iam_user.service["billing-api"] and aws_iam_user.service["search-api"]. If you see numeric indices instead, the block is still using count.
After applying, verify the state with:
terraform state list | grep aws_iam_user.service
You should see both keyed instances listed. If one is missing, the map did not contain the expected key at apply time.
Targeting a single instance
Because the address is deterministic, you can operate on one service account without touching the others:
terraform plan -target='aws_iam_user.service["billing-api"]'
Run this from the same working directory. The -target flag limits the plan to that resource instance and its dependencies. It is useful for quick checks, but relying on it for routine applies can hide drift elsewhere in the configuration.
Trade-offs and sharp edges
for_each is not a drop-in replacement for every count use case. Four limitations matter in practice.
Keys must be known and static at plan time. The map or set is evaluated during planning. If you build it from a remote data source or a complex expression, the plan waits for that data and can fail if the source is unavailable. More importantly, if a key changes—say you rename "billing-api" to "billing-api-v2"—Terraform sees the old key disappear and the new key appear. For resources that cannot be renamed in place, that means a destroy followed by a create.
Map keys must be strings. If your expression produces a map with number or boolean keys, the plan fails with a type error. Convert keys with tostring() or restructure the data so keys are strings before passing them to for_each.
Not every resource supports it. Check the specific resource documentation in the provider arguments reference. If for_each is not listed, the resource only supports count or singular declaration.
Sensitive map values need explicit protection. If your map contains secrets, mark the variable as sensitive = true so that each.value does not appear in plan output or logs.
Checking your work
Before you commit a for_each refactor, run these checks:
terraform planand confirm the Resource actions show the expected number of instances with keyed addresses, not indices.terraform state list | grep <resource_type>to audit that every intended key exists in state and no orphaned indices remain.terraform graphif you suspect implicit dependencies; state can contain edges fromfor_eachexpressions that are not obvious from the configuration alone.
If the plan shows a destroy for a key you did not intend to remove, stop and compare the old and new key sets. The fix is almost always a key naming problem, not a provider bug.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.