Skip to content

Count and For Each

count and for_each both create multiple copies of a resource from a single block of configuration, but they address those copies differently — by numeric index versus by stable key — and that difference decides how safely you can change the underlying collection later.

count takes a whole number and creates that many near-identical copies of a resource, indexed 0 through count - 1. Inside the resource, count.index refers to the current copy’s position; outside the resource, each copy is addressed with that same numeric index, like aws_instance.web[0].

variable "instance_names" {
type = list(string)
default = ["primary", "secondary", "worker"]
}
resource "aws_instance" "web" {
count = length(var.instance_names)
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = var.instance_names[count.index]
}
}

This creates aws_instance.web[0] tagged "primary", aws_instance.web[1] tagged "secondary", and aws_instance.web[2] tagged "worker". That works fine as long as the list never changes shape in the middle.

count’s real weakness: removing a middle item

Section titled “count’s real weakness: removing a middle item”

The problem is specifically about what happens when an item is removed from the middle of var.instance_names. Suppose "secondary" is deleted, leaving ["primary", "worker"]. Conceptually, exactly one thing should happen: destroy the instance that was tagged "secondary". But Terraform does not track “the instance tagged secondary” — it tracks index 0, index 1, index 2. After the list shrinks, index 1 now refers to "worker" instead of "secondary", and index 2 no longer exists at all. Terraform’s plan reflects the index, not the intent:

  • aws_instance.web[1] is destroyed (it was "secondary") and recreated with the new value at that index ("worker")
  • aws_instance.web[2] ("worker", the original) is destroyed, with nothing to replace it

One conceptual removal turns into a cascade of destroy-and-recreate operations for every index after the one that was removed, purely because the addressing scheme is positional. This is the single most common count foot-gun, and it gets worse the larger the list and the earlier the removed item sits.

for_each takes a map or a set of strings and creates one resource instance per key, addressed by that key rather than by position — aws_instance.web["primary"] instead of aws_instance.web[0].

variable "instance_names" {
type = set(string)
default = ["primary", "secondary", "worker"]
}
resource "aws_instance" "web" {
for_each = var.instance_names
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = each.key
}
}

Now remove "secondary" from the set. Only aws_instance.web["secondary"] is planned for destruction. aws_instance.web["primary"] and aws_instance.web["worker"] are untouched — their keys never moved, so Terraform has no reason to touch them. This is why for_each is the better default for almost anything beyond the simplest fixed-count case where order and identity genuinely do not matter: it makes “remove one thing” mean exactly “destroy one thing,” with everything else left alone.

for expressions: transforming a collection, not repeating a resource

Section titled “for expressions: transforming a collection, not repeating a resource”

A for expression is a different concept that happens to share a name with for_each — it is a general-purpose comprehension for transforming one collection into another, and it has nothing to do with creating multiple resources on its own.

locals {
upper_names = [for name in var.instance_names : upper(name)]
name_to_id = { for k, v in aws_instance.web : k => v.id }
}

[for name in var.instance_names : upper(name)] takes a list and produces a new list, one transformed element per input element. {for k, v in aws_instance.web : k => v.id} takes the map of resource instances that for_each created and produces a plain map from name to instance ID. Neither of these lines creates or destroys a single resource — they are pure data transformations you could use inside a local, an output, or a variable’s default. The overlap in the word “for” trips up a lot of newcomers: the for_each meta-argument repeats a resource block once per element of a collection, while a for expression transforms a collection’s contents into a new collection. They are unrelated features that happen to be spelled similarly.

flowchart TB
  subgraph count["count: index-based"]
    c0["index 0 primary"] --> c1["index 1 destroyed then recreated as worker"]
    c1 --> c2["index 2 destroyed, no replacement"]
  end
  subgraph foreach["for_each: key-based"]
    f0["key primary untouched"]
    f1["key secondary destroyed"]
    f2["key worker untouched"]
  end
Removing a middle item shifts every count index, but only removes one for_each key
What happens when you remove an item from the middle of a list used with count on a resource?
Why does for_each avoid the cascading destroy-and-recreate problem that count has?
What is the difference between a for expression and the for_each meta-argument, despite the similar name?
Which meta-argument would you choose for a fixed set of three near-identical logging sidecar resources where order and stable identity do not matter at all?