Terraform count vs for_each (and the Errors)
A practical look at when to reach for count, when for_each saves you, and the specific errors each meta-argument tends to throw.
Key takeaways
- Use for_each for any collection of distinct resources, because it addresses each instance by a stable key, so removing one item touches only that item.
- Use count for on/off toggles and truly identical copies.
- Both need values known at plan time, and switching from count to for_each needs moved blocks to avoid recreating everything.
On this page
Use for_each for collections of distinct things, because it addresses each instance by a stable key and removing one item touches only that item. Use count for a zero-or-one toggle or truly identical copies, since it addresses by index and every later index shifts when the list changes. Migrate between them with moved blocks, not a rebuild.
Verified against
Terraform v1.17.x (the latest docs version), moved blocks requiring v1.1 or later, against the developer.hashicorp.com pages for the count and for_each meta-arguments and refactoring. Checked October 2026.
Both count and for_each let you stamp out multiple copies of a resource from one block. They look interchangeable in a demo. They are not, and the difference shows up the first time you edit the middle of a list and watch Terraform propose to rewrite or destroy resources you never touched.
The core difference: index vs key
count gives you a numbered list. You ask for N copies and Terraform addresses them by position: aws_instance.web[0], aws_instance.web[1], and so on. The identity of each resource is its index, nothing more.
for_each gives you a map or a set. Each resource is addressed by a stable key you choose: aws_instance.web["api"], aws_instance.web["worker"]. The identity is the key, and the key does not move when its neighbors change.
That single distinction, position versus key, is the whole story behind most of the pain people hit.
The classic count problem
Say you manage a set of IAM users with count.
# main.tf
variable "users" {
default = ["alice", "bob", "carol", "dave"]
}
resource "aws_iam_user" "team" {
count = length(var.users)
name = var.users[count.index]
}
This applies cleanly. team[0] is alice, team[1] is bob, and so on down the list. Now Bob leaves and you remove him from the middle:
# main.tf (bob removed)
variable "users" {
default = ["alice", "carol", "dave"]
}
Watch what the plan does:
# aws_iam_user.team[1] will be updated in-place
~ name = "bob" -> "carol"
# aws_iam_user.team[2] will be updated in-place
~ name = "carol" -> "dave"
# aws_iam_user.team[3] will be destroyed
Plan: 0 to add, 2 to change, 1 to destroy.
You deleted one user and Terraform wants to touch three. Because every index after the removed element shifts down by one, team[1] is no longer bob but carol, team[2] becomes dave, and the tail of the list gets destroyed. For IAM users that is an annoyance, and often a failed apply, because renaming bob to carol collides with the carol who still exists. For resources where the changed attribute forces replacement, it is worse. For stateful resources like databases or EBS volumes, an index shift is a data-loss incident.
Why for_each avoids it
Rewrite the same thing keyed by name:
# main.tf
variable "users" {
default = ["alice", "bob", "carol", "dave"]
}
resource "aws_iam_user" "team" {
for_each = toset(var.users)
name = each.key
}
Now the resources are team["alice"], team["bob"], team["carol"], team["dave"]. Remove bob and the plan is exactly what you meant:
# aws_iam_user.team["bob"] will be destroyed
Plan: 0 to add, 0 to change, 1 to destroy.
Carol and dave do not move, because their keys never changed. HashiCorp's own guidance draws the same line: count for nearly identical instances, for_each when instances need distinct values that cannot be derived from an integer.
Side by side
count | for_each | |
|---|---|---|
| Instance address | Index: team[0] | Key: team["alice"] |
| Remove a middle item | Shifts every later index | Touches only that key |
| Input type | Whole number | Map or set of strings |
| Must be known at plan | The number | The keys (values can be unknown) |
| Reference inside the block | count.index | each.key, each.value |
| Best fit | On/off toggle, identical copies | Named, distinct resources |
The errors each one causes
count depends on apply-time values: The most common count failure is asking Terraform to size a list from something it cannot know until apply.
# main.tf
resource "aws_instance" "web" {
count = length(data.aws_availability_zones.available.names)
# ...
}
If that data source or an upstream resource attribute is not resolvable during plan, you get:
Error: Invalid count argument
The "count" value depends on resource attributes that cannot be
determined until apply, so Terraform cannot predict how many instances
will be created. To work around this, use the -target argument to first
apply only the resources that the count depends on.
Terraform has to know the number of instances before it can build the plan graph. If the count is computed downstream, it cannot.
for_each with unknown or duplicate keys: for_each has a stricter cousin of the same problem. The keys must be known at plan time, even if the values are not.
Error: Invalid for_each argument
The "for_each" set includes values derived from resource attributes that
cannot be determined until apply.
The fix is usually to key off something static (a variable, a local literal) and keep the unknown, apply-time data in the value. Two more traps. toset() silently collapses duplicate list entries into one key, so a list with the same name twice yields one resource, not two; and a for expression that builds a map with duplicate keys is a hard error unless you use the ... grouping operator. The docs also forbid sensitive values in for_each and keys built from impure functions like uuid() or timestamp().
Converting count to for_each without recreating everything
The catch: switching a block from count to for_each changes every resource address. team[0] becomes team["alice"]. To Terraform those are different resources, so a naive switch plans a full destroy-and-recreate. You migrate the state instead.
The clean, reviewable way is a moved block committed alongside the change:
# moved.tf
moved {
from = aws_iam_user.team[0]
to = aws_iam_user.team["alice"]
}
moved {
from = aws_iam_user.team[1]
to = aws_iam_user.team["bob"]
}
Run terraform plan and the moves show up as address changes with zero replacements. The imperative equivalent, if you prefer to do it out of band, is:
$ terraform state mv 'aws_iam_user.team[0]' 'aws_iam_user.team["alice"]'
$ terraform state mv 'aws_iam_user.team[1]' 'aws_iam_user.team["bob"]'
Prefer moved blocks (Terraform v1.1 and later). They live in code, survive review, and every teammate who applies the change gets the migration for free instead of running one-off CLI commands against shared state.
When count is still the right call
count is not a mistake. It is a different tool. The place it genuinely wins is a simple on/off toggle, where you are not iterating a collection at all.
# dashboard.tf
resource "aws_cloudwatch_dashboard" "main" {
count = var.enable_dashboard ? 1 : 0
# ...
}
Here there is no list to reorder and no key that means anything. You want zero or one, and count = var.enabled ? 1 : 0 reads exactly as intended. Reach for it when the quantity is the whole point and the elements have no distinct identity worth preserving. Everything else, the sets and maps of named things, belongs in for_each.
For the wider catalog of plan-time and apply-time failures, see the Terraform error troubleshooting guide. The same key-stability rule applies one level up: a module called with for_each gets keyed addresses too, which is one more reason shared module versioning should treat input keys as part of the interface.
Questions people ask
When should I use count instead of for_each in Terraform?
Use count when the number is the whole point: a var.enabled ? 1 : 0 toggle, or several copies that are genuinely identical and never removed from the middle. Use for_each whenever each instance has its own identity, such as named users, buckets, or subnets, because stable keys keep unrelated instances out of the plan when the collection changes.
How do I fix "Invalid for_each argument" in Terraform?
The keys depend on something Terraform cannot know until apply, such as an ID a resource creates. Build the keys from static inputs (variables, locals, literal names) and move apply-time attributes into the values. If a sensitive value is involved, derive a non-sensitive set of keys with a for expression. As a last resort, -target the dependency first.
How do I change count to for_each without destroying resources?
Add one moved block per instance mapping each old index to its new key, for example from = aws_iam_user.team[0] and to = aws_iam_user.team["alice"], then run terraform plan. A correct migration shows only address moves and zero replacements. On Terraform older than v1.1, use terraform state mv with the same pairs instead.
The call we'd make
Default to for_each for any collection of distinct resources, and treat stable keys as a first-class design decision rather than an afterthought. Reserve count for boolean toggles where a single index carries no meaning. When you migrate an existing count block, always ship a moved block so the change is a rename in state, not a rebuild in production. Do that and the two errors above stop being surprises and become things you designed around.
Sources
Cite this article
Kiril Urbonas (2026). Terraform count vs for_each (and the Errors). DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/terraform-count-vs-for-each
[Terraform count vs for_each (and the Errors)](https://www.devopsness.com/blog/terraform-count-vs-for-each) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from DevOps
Explore more articles in this category
15 SRE Interview Questions With Answers, From SLOs to On-Call
SRE interviews test numbers and judgment. Fifteen questions on SLIs, error budgets, incident command, toil, capacity, on-call and postmortems, plus a systems-design scenario, each with the answer that scores and the one that loses the room.
Sign Container Images With Cosign: Keyless GitHub Actions Workflow, SBOM and In-Cluster Checks
One workflow that builds, signs keylessly with GitHub OIDC, attaches an SBOM and SLSA provenance, plus the Kyverno policy that refuses anything unsigned. Copy it, change three names, ship it.
Fix Prometheus High Cardinality: 6 Commands to Find and Drop Bad Series
When Prometheus runs out of memory, one label is almost always to blame. Six commands to find it, the relabel rules that drop it, and the scrape limits that stop it coming back.
You might have missed
Evergreen posts worth revisiting.