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:

  1. 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 create

  2. for_each: it is used to iterate over a map in Terraform

Let's create three resource groups using countand 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"].

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.

I personally recommend using count only for conditional resource creation. If you need to create a list of resources, you can use the toset() built-in function of Terraform and use a for_each loop. This will protect you from accidentally deleting your resources, as the key of each map must be unique.

Last updated