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_delete
to 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.