count and for_each — Index or Key
In one sentence
The difference between count and for_each is not "how many you create" but what label you attach to each instance. And that label is written into the state file as is.
Why this was needed
You use count = 3 to create three servers. It works well. A few months later the list becomes a variable and it turns into count = length(var.servers). Even that is fine. The incident happens when you delete an item in the middle.
The addresses of instances made with count are [0], [1], and [2]. If you delete the second item from the list, the third is pulled into the second slot. In the world the tool sees, this becomes "the contents of [1] changed and [2] disappeared." So the plan shows the recreation of two servers, not the one you meant to delete. On a real cloud, a server that was running fine would be destroyed and created again.
for_each solves this problem with names. The address becomes a meaningful key like ["web-a"] or ["web-b"], so when you remove one from the list, only that one key disappears. The rest do not budge.
How it works
| Item | count |
for_each |
|---|---|---|
| Value accepted | A number | A set or map |
| Instance address | res[0] |
res["kr"] |
| Reference used inside | count.index |
each.key, each.value |
| Deleting a middle item | The later ones shift, so they are recreated | Only that key is deleted |
| Where it fits well | When only the count matters, on/off switches | When each item has an identity |
The practical criterion is simple. If each instance can have a distinct name, use for_each, and if only the count is meaningful, use count. A typical place where you may keep using count is a feature switch. count = var.enable_debug ? 1 : 0 is an idiom for expressing "present or absent," and since there is only one instance, there is no shifting problem.
You cannot pass a list to for_each as is. A list has order and for_each requires unordered keys, so you must convert it with toset(...). In that case the key becomes the element value itself. If you pass a map, the key is the map's key and each.value is its value — this is used more in practice because it is easier to attach several attributes to each item.
A for expression is a different thing. If for_each is a meta-argument that creates multiple resources, for is an expression that turns one value into another shape. It is used to fold a map into a map again, as in { for k, v in var.services : k => v.port }. You will almost always use it when tidying up outputs or locals.
What it looks like in the field
First, always migrate with moved. If you change a resource already applied with count to for_each, the tool judges that the old addresses have vanished and new addresses have appeared. That is, it destroys everything and recreates everything. If you tell it with a moved block that "res[0] has become res["n0"]," only the state addresses are moved without destruction. If even one destruction appears in the plan, it is a signal that recreation, not migration, is taking place.
Second, record the current addresses before moving. The first step of a migration is not editing code but recording. You must first save the state list result and a copy of the state to files. To be able to say "no resource was missed" after the migration finishes, you must put the list from before the move and the list from after the move side by side and check the counts and the correspondence, and if you did not save the former, there is no way to check at all. Incidents usually happen when 19 of 20 were moved, and the remaining one is silently destroyed by the next apply.
Third, do not compute keys. If the key of for_each is a value that is decided only at apply time, like an attribute of another resource, the instance list cannot be known at the plan stage and an error occurs. The key must be a value fixed at plan time, like a variable or a constant.
Fourth, key names are hard to change. Once decided, a key effectively becomes the resource's identifier, so even fixing az-a to ap-northeast-2a later becomes a migration task. The answer here is also moved. So when you first choose a key, it is cheaper to think once more, "will this name still make sense two years from now?"
When the address changes, the resource is replaced
The real difference between count and for_each is not the syntax but the address written in the state.
If that address changes, Terraform sees it as a different resource and deletes it and recreates it.
count: aws_instance.web[0] aws_instance.web[1] aws_instance.web[2]
for_each: aws_instance.web["a"] aws_instance.web["b"] aws_instance.web["c"]
Say you delete b from the middle of the list. With count, c is pulled into slot 1, so
both slot 1 and slot 2 become replacement targets. With for_each, only ["b"] disappears.
If a database or a disk was in that list, this difference separates an incident from an ordinary deployment.
The key of for_each must be knowable at plan time. If you use a value that is
(known after apply), such as another resource's output, as a key, you get "The for_each value depends on
resource attributes that cannot be determined until apply." The value may be decided later,
but the key must already be decided now.
# 이름을 열쇠로 삼는다 — 만들기 전에 이미 안다
resource "aws_instance" "web" {
for_each = { for s in var.servers : s.name => s }
instance_type = each.value.type
subnet_id = aws_subnet.app[each.value.az].id
}
When moving something already created with count, use moved. Otherwise it
deletes everything and recreates it.
moved {
from = aws_instance.web[0]
to = aws_instance.web["a"]
}
There are places where count is fine. It is when the count is
0 or 1, like an on/off switch that decides whether to create something.
resource "aws_nat_gateway" "this" {
count = var.enable_nat ? 1 : 0
}
In that case the reference becomes aws_nat_gateway.this[0].id, and when it is turned off that reference becomes an error,
so you wrap it with one(aws_nat_gateway.this[*].id) or try.
What to do in the next lab
You create three with count and confirm that the numeric index is written into the state, and save the list of addresses from before the move to a file. Next, you try for_each in two ways, with toset and with a map. You build an output map with a for expression and express a feature switch with a conditional count. At the end, you migrate count to for_each with a moved block and prove with the plan that only the addresses change without destruction.