Files
nexus/sreweekly/markdown/487/06-choosing-between-count-and-for-each.md
2026-09-12 17:23:01 +08:00

217 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Choosing Between Count and For-Each
- **期号**: SRE Weekly Issue #487(2025-07-27)
- **作者**: Ned Bellavance
- **链接**: https://nedinthecloud.com/2022/01/27/choosing-between-count-and-for-each/
## 简介
Here’s a great explanation of why it’s often better to use for_each instead of count in Terraform.
## 正文
9 min read
Terraform has two looping mechanisms for creating multiple resources, `count` and `for_each`. The `count` meta-argument has been around for a long time, but `for_each` is a relative newcomer (introduced in version 0.12). Each meta-argument allows you to create more than one resource or module with a single configuration block.
A common question is when to use `count` versus `for_each`. I would make the argument that `for_each` is almost always preferred, and in this post I hope to show you why.
Before I explain why `for_each` is generally superior, it would be useful to understand what is actually happening when you add a looping meta-argument to a Terraform configuration. Let’s start with a simple example.
```
resource "local_file" "count_int_loop" {
count = 3
content = "This is file number ${count.index}"
filename = "${path.module}/int-${count.index}.count"
}
```
Running `terraform apply` will generate three files: int-1.count, int-2.count, int-3.count. If we take a look at the state data:
```
$> terraform state list
local_file.count_int_loop[0]
local_file.count_int_loop[1]
local_file.count_int_loop[2]
```
We have three resources created by the `count` meta-argument with an integer based index. The object `local_file.count_int_loop` is an ordered list of `local_file` resource objects.
If we want four files instead of three, all we have to do is increase the count value by one and Terraform will create a fourth file. This is what `count` was meant for, undifferentiated resource creation based on an integer.
But what if instead of an integer, we were dealing with a list of items?
Before the introduction of `for_each`, the `count` argument was all we had. *(And we liked it.)* You could use a list to create mulitple resources by finding the number of elements in the list and using that for the `count` value. The `length()` function does an admirable job of accomplishing this.
Consider the following configuration with a list of toppings defined as a local value.
```
locals {
toppings = ["lettuce","tomatoes","jalapenos"]
}
resource "local_file" "count_loop" {
count = length(local.toppings)
content = "${local.toppings[count.index]}"
filename = "${path.module}/${local.toppings[count.index]}.count"
}
```
Running `terraform apply` will generate three files: lettuce.count, tomatoes.count, and jalapenos.count. If we take a look at the state:
```
$> terraform state list
local_file.count_loop[0]
local_file.count_loop[1]
local_file.count_loop[2]
```
Once again, we have three resources created by the `count` meta-argument with a number based index. Just like before, the object `local_file.count_loop` is an ordered list of `local_file` resources.
So far, so good, right? What’s the point of a `for_each` arguemnt if we can simply use the `count` argument with a `length` function? Let’s try the same thing with `for_each` instead:
```
resource "local_file" "for_each_loop" {
for_each = toset(local.toppings)
content = "${each.value}"
filename = "${path.module}/${each.value}.foreach"
}
```
Looking at our state now:
```
$> terraform state list
local_file.for_each_loop["jalapenos"]
local_file.for_each_loop["lettuce"]
local_file.for_each_loop["tomatoes"]
```
We have three resources created by the `for_each` meta-argument with a key based reference. The object `local_file.for_each_loop` is a **map** (aka hashtable). The keys will be the strings in the set, or if you submit a map it will be the keys of the map. The map values are the `local_file` resources.
From a practical standpoint, we have essential generated three files with the same content. Either argument seems to do the trick, so why would you prefer one over the other? Two reasons: consistency and referencing.
Something that’s not immediately obvious is how much the order of the items used by `count` matters. Let’s say we want to add a topping to our list. How about some onions?
```
locals {
toppings = ["lettuce","tomatoes","onions","jalapenos"]
}
```
What do you think will happen when we run `terraform plan` against the `count` example? Notice the **order** of the toppings. Onions is now in index **2** and jalapenos is in index **3**.
```
$> terraform plan
Terraform will perform the following actions:
# local_file.count_loop[2] must be replaced
-/+ resource "local_file" "count_loop" {
~ content = "jalapenos" -> "onions" # forces replacement
~ filename = "./jalapenos.count" -> "./onions.count" # forces replacement
~ id = "626451b23e9097d6a2c081959703df63424602bf" -> (known after apply)
# (2 unchanged attributes hidden)
}
# local_file.count_loop[3] will be created
+ resource "local_file" "count_loop" {
+ content = "jalapenos"
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./jalapenos.count"
+ id = (known after apply)
}
# local_file.for_each_loop["onions"] will be created
+ resource "local_file" "for_each_loop" {
+ content = "onions"
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./onions.foreach"
+ id = (known after apply)
}
Plan: 3 to add, 0 to change, 1 to destroy.
```
*Terraform is going to destroy our jalapenos!* And that is because when Terraform runs through the `count` loop, it sees the value *onions* in index 2 and that value used to be *jalapenos*. Terraform has to destroy the original `local_file.count_loop[2]` resource and replace it with the new value. Then it will create a new resource called `local_file.count_loop[3]` using the *jalapenos* value.
The `for_each` loop doesn’t have this problem. Since it is using a key based reference, it doesn’t care about order. In fact, the `for_each` argument requires a set or map as input, neither of which are ordered. Terraform simply sees the key *onion* without a corresponding entry in `local_file.for_each_loop` and decides to create one. The jalapenos file is not touched. Thank goodness! I like my jalapenos.🌶️
Count is going to unnecessarily destroy and recreate the jalapenos file, which might not be a problem for a text file. But imagine that’s a Kubernetes cluster running 100+ applications, and you just destroyed it because you added a new cluster in the wrong order. That’s… bad. Possibly a resume generating event.
Of course, that would never happen becuase you run `terraform plan` first, right? Right???
As pointed out by the previous section, using `count` results in an ordered list and `for_each` results in a map. When you need to reference the resources somewhere else in your configuration, you might find that being able to refer to a resource by key instead of index is much easier. Let’s look at a slightly more advanced example where we are trying to create users and groups in Terraform Cloud.
Users are created with the resource type `tfe_organization_membership`. I could create users with a `count` like this:
```
resource "tfe_organization_membership" "org_members" {
count = length(local.users)
organization = local.organization_name
email = local.users[count.index]
}
```
Or with a `for_each` like this:
```
resource "tfe_organization_membership" "org_members" {
for_each = toset(local.users)
organization = local.organization_name
email = each.value
}
```
To add a user to a team on Terraform Cloud, the resource type `tfe_team_organization_member` is used. The two arguments `team_id` and `organization_membership_id` both require a value that is an attribute of the previously generated team or user. It is a value we must look up using a reference. If we’re using the `for_each` loop to create users, the reference for `organization_membership_id` looks like this:
```
organization_membership_id = tfe_organization_membership.org_members[each.value["member_name"]].id
```
We are using the key `member_name` to find the correct instance of `tfe_organization_membership` and returning the `id` attribute of that instance. While the code looks a little confusing at first, trust me it works. The full block is shown below.
```
resource "tfe_team_organization_member" "team_members" {
for_each = { for member in local.team_members : "${member.team_name}_${member.member_name}" => member }
team_id = tfe_team.teams[each.value["team_name"]].id
organization_membership_id = tfe_organization_membership.org_members[each.value["member_name"]].id
}
```
If we had used a `count` argument to create the users, we would need to use a `for` expression with a filter to look up the membership id value. Something along the lines of this:
```
organization_membership_id = ([for user in tfe_organization_membership.org_members : user.id if user.email == each.value["member_name"]])[0]
```
Would it work? Sure. Is it efficient? Nope. The `for` expression has to loop through all the `tfe_organization_membership` resources to find the one that matches. No big deal if we have three users. Pretty big deal if we have three thousand. Reference by key is one of the best things about maps/hashtables/dictionaries, whatever you want to call them.
You can use `count` if you don’t care about uniqueness or references in your configuration. If every item created by a loop is ephemeral and functionally identical, then there’s probably no benefit to using `for_each`. If you don’t need to refer to anything by the key, then using `count` could be fine. On the other hand, it’s about the same amount of work to use either, and `for_each` has some serious benefits.
One use that still seems relevant is using count with a zero value to make the creation of a resource optional. What do I mean? Consider this:
```
resource "local_file" "count_optional" {
count = local.create_file ? 1 : 0
content = "Hello!"
filename = "${path.module}/count-create.txt"
}
```
The creation of a new organization will happen if the variable `create_new_organization` is set to `true` and not if its set to `false`. You’re only ever creating one or zero of an item. Is there a way to replace this with `for_each`? If so, is there any benefit?
To answer the first question. Yes, you can do it.
```
resource "local_file" "for_each_optional" {
for_each = local.create_file ? toset(["any_value"]) : toset([])
content = "Hello!"
filename = "${path.module}/for-each-create.txt"
}
```
Is there any benefit? Not that I can think of. The `count` version feels more intuitive, but I don’t think it would be any more effective than the `for_each` loop.
I started out the looping overview by using an integer with `count`. This is the one time when `count` has a clear advantage. The count argument takes a number and counts up to that number. You can access the current iteration using the `count.index` expression. `Count` makes more sense if I am creating resources based off a number, instead of set, list, map, or other object.
Could you replace it with a `for_each` argument? Would there be any benefit?
To answer the first question, yes you can do it.
```
for_each = toset(range(3))
```
The `range` function creates a list of integers starting with 0 and going to the max, non-inclusive. The `toset()` function turns that list of integers into a set. The `each.value` value will be the integer of the current iteration, so it’s basically the same as `index.count`.
Is there any benefit? I’d have to say no. The syntax is clunky and requires the execution of two functions to get it working. Contrasted to the `count` argument, there is no tangible benefit of using `for_each` in this situation.
To sum up, here’s the general advice. If you are creating multiple resources based off an integer, and the resources are undifferentiated, then `count` works just fine. Any time you are using an input that is more complex than an integer, the proper answer is going to be `for_each`. Conditional resource creation is a bit of a toss up, so just do what feels intuitive to you or aligns with your team’s conventions.
August 29, 2026
May 23, 2026
May 4, 2026