Chapter 9: Terraform Loop Battle: count vs for_each
In Terraform, it is inevitable that you will need to create multiple instances of the same resource or module. Terraform provides two meta-argumets to create loops under resource and module blocks:
count:
it is used to create multiple copies of a resource in Terraform. It takes an integer value that specifies the number of copies to createfor_each:
it is used to iterate over a map in Terraform
Let's create three resource groups using count
and for_each
loop as follows:
# Define a list of resource groups to create
variable "resource_groups" {
type = list(string)
default = ["rg-example-001", "rg-example-002", "rg-example-003"]
}
# Create resource groups with count
resource "azurerm_resource_group" "count_example" {
count = length(var.resource_groups)
name = "${var.resource_groups[count.index]}-count"
location = "North Europe"
}
# Create resource groups with for_each
resource "azurerm_resource_group" "for_each_example" {
for_each = toset(var.resource_groups)
name = "${each.key}-for_each"
location = "North Europe"
}
# Print all resource group names
output "resource_group_names" {
value = concat(
["${azurerm_resource_group.count_example.*.name}"],
["${values(azurerm_resource_group.for_each_example)[*].name}"]
)
}
If you run the code, you will see:
> terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.count_example[0] will be created
+ resource "azurerm_resource_group" "count_example" {
# output is omitted for brevity
+ name = "rg-example-001-count"
}
# azurerm_resource_group.count_example[1] will be created
+ resource "azurerm_resource_group" "count_example" {
# output is omitted for brevity
+ name = "rg-example-002-count"
}
# azurerm_resource_group.count_example[2] will be created
+ resource "azurerm_resource_group" "count_example" {
# output is omitted for brevity
+ name = "rg-example-003-count"
}
# azurerm_resource_group.for_each_example["rg-example-001"] will be created
+ resource "azurerm_resource_group" "for_each_example" {
# output is omitted for brevity
+ name = "rg-example-001-for_each"
}
# azurerm_resource_group.for_each_example["rg-example-002"] will be created
+ resource "azurerm_resource_group" "for_each_example" {
# output is omitted for brevity
+ name = "rg-example-002-for_each"
}
# azurerm_resource_group.for_each_example["rg-example-003"] will be created
+ resource "azurerm_resource_group" "for_each_example" {
# output is omitted for brevity
+ name = "rg-example-003-for_each"
}
Plan: 6 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ resource_group_names = [
+ [
+ "rg-example-001-count",
+ "rg-example-002-count",
+ "rg-example-003-count",
],
+ [
+ "rg-example-001-for_each",
+ "rg-example-002-for_each",
+ "rg-example-003-for_each",
],
]
If you carefully examine the output, you will notice that the resource group created in the count loop is created as a list: azurerm_resource_group.count_example[0]
, azurerm_resource_group.count_example[1]
, and azurerm_resource_group.count_example[2]
. On the other hand, the resource groups created in the for_each
loop are created as a map: azurerm_resource_group.for_each_example["rg-example-001"]
, azurerm_resource_group.for_each_example["rg-example-002"]
, and azurerm_resource_group.for_each_example["rg-example-003"]
.
This difference is significant because using count can make it more challenging to manage resources if you later need to adjust the number of resources. For instance, adding or removing a resource can affect other resources since the ordering of the list passed to the count
loop will be modified.
The for_each
expression allows you to create a variable number of resources based on the contents of a map. This can be useful if you need to create a dynamic number of resources or if you want to use a map to store resource configurations. To illustrate this, try removing "rg-example-002
" from the default values and running the code again:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
-/+ destroy and then create replacement
Terraform will perform the following actions:
# azurerm_resource_group.count_example[1] must be replaced
-/+ resource "azurerm_resource_group" "count_example" {
~ name = "rg-example-002-count" -> "rg-example-003-count" # forces replacement
# output is omitted for brevity
}
# azurerm_resource_group.count_example[2] will be destroyed
# (because index [2] is out of range for count)
- resource "azurerm_resource_group" "count_example" {
- name = "rg-example-003-count" -> null
# output is omitted for brevity
}
# azurerm_resource_group.for_each_example["rg-example-002"] will be destroyed
# (because key ["rg-example-002"] is not in for_each map)
- resource "azurerm_resource_group" "for_each_example" {
- name = "rg-example-002-for_each" -> null
# output is omitted for brevity
}
Plan: 1 to add, 0 to change, 3 to destroy.
As you can see, Terraform compares the state file and notices that the resource_groups
variable now only has two elements. The second element is not the resource it was expecting, so it destroys the existing resource group and creates a new one with the same name, just to store it in the state file as the second element. On the other hand, the for_each
loop notices that a map element has been removed and simply tries to remove the corresponding element.
Last updated