Introduction
This article shows the difference between using the count and for_each meta-arguments when creating multiple instances of the same resource.
The issue arises when you have a resource created based on a list with count and you start manipulating that list.
This might lead to unexpected changes to your resource, for example destruction of an untouched element from the list.
This can be resolved by using the for_each meta-argument.
The difference is that when using the count meta-argument, the resources are stored in state by an index number. When changing the list, the index numbers might get re-calculated, causing subsequent elements to be changed or destroyed unexpectedly.
When using the for_each meta-argument, the resources are stored by a key. The result is that when manipulating the list only the resource with that key is affected.
Please see the count and for_each examples below.
Count
variable "my_list" {
type = list(string)
default = ["zero", "one", "to_delete", "two", "three"]
# default = ["zero", "one", "two", "three"]
}
resource "terraform_data" "my_count" {
count = length(var.my_list)
input = "tf_data-${var.my_list[count.index]}"
}
In the code above we have a variable with 5 elements in a list. This list is used to create the terraform_data resource with a count.
If we now remove the element to_delete from this list and run a terraform
plan, we see the following output:
% terraform plan
terraform_data.my_count[4]: Refreshing state... [id=17805a4b-7066-d7b9-5052-820c1288619c]
terraform_data.my_count[0]: Refreshing state... [id=0e11dc93-c968-d22c-f19a-2a9b818b2a1d]
terraform_data.my_count[1]: Refreshing state... [id=90b52455-95bd-f5d6-483c-48537ecb9d89]
terraform_data.my_count[3]: Refreshing state... [id=f14609db-5494-4a40-68b2-df5e7bd9c8de]
terraform_data.my_count[2]: Refreshing state... [id=99838c58-d8a9-8fe8-dee7-ba6b2b9a23d9]
Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
~ update in-place
- destroy
Terraform will perform the following actions:
# terraform_data.my_count[2] will be updated in-place
~ resource "terraform_data" "my_count" {
id = "99838c58-d8a9-8fe8-dee7-ba6b2b9a23d9"
~ input = "tf_data-to_delete" -> "tf_data-two"
~ output = "tf_data-to_delete" -> (known after apply)
}
# terraform_data.my_count[3] will be updated in-place
~ resource "terraform_data" "my_count" {
id = "f14609db-5494-4a40-68b2-df5e7bd9c8de"
~ input = "tf_data-two" -> "tf_data-three"
~ output = "tf_data-two" -> (known after apply)
}
# terraform_data.my_count[4] will be destroyed
# (because index [4] is out of range for count)
- resource "terraform_data" "my_count" {
- id = "17805a4b-7066-d7b9-5052-820c1288619c" -> null
- input = "tf_data-three" -> null
- output = "tf_data-three" -> null
}
Plan: 0 to add, 2 to change, 1 to destroy.
From the output you can see that the elements are stored by index in the list, for example terraform_data.my_count[2] is the element tf_data-to_delete.
As you can see from the plan, Terraform wants to rename the resource tf_data-to_deleteto tf_data-two, rename tf_data-two to tf_data-three and destroy tf_data-three.
for_each
Now let's have a look at the for_each meta-argument.
variable "my_list" {
type = list(string)
default = ["one", "two", "to_delete", "three", "four"]
#default = ["one", "two", "three", "four"]
}
resource "terraform_data" "my_for_each" {
for_each = toset(var.my_list)
input = "tf_data-${each.value}"
}
In the code above, we have the same variable with 5 elements in a list. This list is used to create the terraform_data resource with a for_each.
If we now remove the element to_delete from this list and run a terraform
plan, we see the following output:
% terraform plan
terraform_data.my_for_each["three"]: Refreshing state... [id=ff0879d4-2d3a-1999-4602-592e929bcf39]
terraform_data.my_for_each["two"]: Refreshing state... [id=e25b4ec1-87d3-d57d-4359-3bd2d040a935]
terraform_data.my_for_each["one"]: Refreshing state... [id=bdb21998-df99-e13d-5b24-56269a09ad19]
terraform_data.my_for_each["to_delete"]: Refreshing state... [id=625b9eb7-956f-a96f-f87a-1d3d20f322ca]
terraform_data.my_for_each["zero"]: Refreshing state... [id=1669ec9f-e347-a225-af4d-ba4c87d088ad]
Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# terraform_data.my_for_each["to_delete"] will be destroyed
# (because key ["to_delete"] is not in for_each map)
- resource "terraform_data" "my_for_each" {
- id = "625b9eb7-956f-a96f-f87a-1d3d20f322ca" -> null
- input = "tf_data-to_delete" -> null
- output = "tf_data-to_delete" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
From the output you can see that the elements are stored by name in the list, for example terraform_data.my_for_each["to_delete"] is the element tf_data-to_delete.
As you can see from the plan, Terraform now only wants to destroy the tf_data-to_delete, leaving the other resources untouched.
As shown with the examples above, using the for_each meta-argument instead of count helps to avoid unintended changes of resources.