# Terraform count vs for_each (and the Errors)

URL: https://www.devopsness.com/blog/terraform-count-vs-for-each
Author: Kiril Urbonas
Published: 2026-07-31
Updated: 2026-10-31
Categories: DevOps
Tags: Terraform, Iac, Count, For Each, DevOps

Summary: A practical look at when to reach for count, when for_each saves you, and the specific errors each meta-argument tends to throw.

---
# Terraform count vs for_each (and the Errors)

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]
> 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`.

```hcl
# 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:

```hcl
# main.tf (bob removed)
variable "users" {
  default = ["alice", "carol", "dave"]
}
```

Watch what the plan does:

```text
  # 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:

```hcl
# 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:

```text
  # 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.

```hcl
# 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:

```text
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.

```text
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:

```hcl
# 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.

```hcl
# 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](/blog/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](/blog/terraform-module-versioning-shared-registries) 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

- [Terraform docs: The for_each meta-argument](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each)
- [Terraform docs: The count meta-argument](https://developer.hashicorp.com/terraform/language/meta-arguments/count)
- [Terraform docs: Refactoring with moved blocks](https://developer.hashicorp.com/terraform/language/modules/develop/refactoring)

