Introduction
This article explains the difference between using the count and for_each meta-arguments when creating multiple instances of a resource in Terraform. Using count with a list can lead to unexpected resource modifications or destruction when the list is changed. Using for_each provides more predictable behavior by tracking resources by a unique key instead of a numeric index.
Scenario
When you create multiple resource instances from a list using the count meta-argument, removing an element from the middle of that list can cause unintended changes to other resources. This occurs because count tracks resources in state by their index number. When an item is removed, the indices of subsequent items shift, causing Terraform to see them as changed resources.
Recommendation
To avoid unintended resource changes, use the for_each meta-argument when creating resources from a collection whose members may change over time. The for_each meta-argument tracks resources by a consistent key, ensuring that changes to one element do not affect others.
Approach 1: Using count (Illustrates the Problem)
The following configuration uses the count meta-argument to create resources from a list.
variable "my_list" {
type = list(string)
default = ["zero", "one", "to_delete", "two", "three"]
## To test, comment the line above and uncomment the line below.
## default = ["zero", "one", "two", "three"]
}
resource "terraform_data" "my_count" {
count = length(var.my_list)
input = "tf_data-${var.my_list[count.index]}"
}If you remove the element to_delete from the list, Terraform produces the following plan.
$ terraform plan
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.The plan shows that because the resources are tracked by index, removing to_delete at index 2 causes Terraform to propose updating the resources at the original indices 2 and 3 and destroying the resource at index 4.
Approach 2: Using for_each (Recommended Solution)
The following configuration uses the for_each meta-argument. Note that for_each requires a map or a set of strings, so the list is converted to a set using toset().
variable "my_list" {
type = list(string)
default = ["one", "two", "to_delete", "three", "four"]
## To test, comment the line above and uncomment the line below.
## default = ["one", "two", "three", "four"]
}
resource "terraform_data" "my_for_each" {
for_each = toset(var.my_list)
input = "tf_data-${each.value}"
}If you remove the element to_delete from this list, Terraform produces a more precise plan.
$ terraform plan
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.The plan shows that because the resources are tracked by their string value (e.g., "to_delete"), Terraform correctly identifies that only the resource associated with that key should be destroyed, leaving all other resources untouched.