For and For_Each loop in Terraform

In this article, we are going to discuss on For and For_Each loop in terraform. The count meta-argument at the terraform helps us to create multiple resources of the same type. It is the simplest construct to have the functionality of the loop in terraform, but it has two limitations as mentioned below.

  1. Count inside the inline-block does not support: Consider the following to create multiple resources groups in the subscription.
resource "azurerm_resource_group" "rg" {
    count = 3
    name  = "rg-${count.index}"
    location = "eastus"
    tags = {
      Owner = "Admin"
      Prject = "Demo"
    }
}

The count.index holds the iteration variable and adds the integer number to the names during each iteration. i.e. rg-0, rg-1, rg-2 etc. If you want iterate over the tags, the count will not work as it does not support the inline-blocks within the resources.

2. Resources created using the count are of ordered collection type: When we create the resources using count arguments, terraform stores them as ordered collection i.e. managed through the indexes. The following is the code block to create the resource groups using the count.

variable "rgs" {
    type = list
    default = ["rg-0","rg-1","rg-2"]
}

provider "azurerm" {
    features {}
}

resource "azurerm_resource_group" "dev_sub1_rg" {
    count = length(var.rgs)
    name = "${var.rgs[count.index]}"
    location = "eastus"
    tags = {
        Owner = "Admin"
    }
}

output "rgs_output" {
    value = azurerm_resource_group.dev_sub1_rg
}
azurerm_resource_group.dev_sub1_rg[2]: Refreshing state... [id=/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-2]
azurerm_resource_group.dev_sub1_rg[0]: Refreshing state... [id=/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-0]
azurerm_resource_group.dev_sub1_rg[1]: Refreshing state... [id=/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-1]
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

rgs_output = [
  {
    "id" = "/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-0"
    "location" = "eastus"
    "name" = "rg-0"
    "tags" = {
      "Owner" = "Admin"
    }
  },
  {
    "id" = "/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-1"
    "location" = "eastus"
    "name" = "rg-1"
    "tags" = {
      "Owner" = "Admin"
    }
  },
  {
    "id" = "/subscriptions/xxxxxxxxxxxxxxxx/resourceGroups/rg-2"
    "location" = "eastus"
    "name" = "rg-2"
    "tags" = {
      "Owner" = "Admin"
    }
  },
]

As in the above output, the resource group names are bound with index numbers i.e. Index-0 -> rg-0, Index-1 -> rg-1, etc not with the names. Let’s change the variable rgs by removing the second resource group.

variable "rgs" {
    type = list
    default = ["rg-0","rg-2"]
}

In this case, the rg-1 should be deleted without changing the other resource groups. But when we use count, the terraform will try to re-order the resources which means, it deletes the rg-1, and rg-2 and re-creates the rg-2 in-place of rg-1. the below is the terraform plan.

# azurerm_resource_group.dev_sub1_rg[1] must be replaced
-/+ resource "azurerm_resource_group" "dev_sub1_rg" {
      ~ id       = "/subscriptions/xxxxxxxxx/resourceGroups/rg-1" -> (known after apply)
        location = "eastus"
      ~ name     = "rg-1" -> "rg-2" # forces replacement
        tags     = {
            "Owner" = "Admin"
        }
    }

  # azurerm_resource_group.dev_sub1_rg[2] will be destroyed
  - resource "azurerm_resource_group" "dev_sub1_rg" {
      - id       = "/subscriptions/xxxxxxxxx/resourceGroups/rg-2" -> null
      - location = "eastus" -> null
      - name     = "rg-2" -> null
      - tags     = {
          - "Owner" = "Admin"
        } -> null
    }

To overcome these limitations, terraform has introduced For and For_Each loops in version 0.12. Now let’s make use of the same resources group resource block but with tags using inline-blocks.

variable "custom_tags" {
    Owner = "Admin"
    Project = "Demo"
}
resource "azurerm_resource_group" "example" {
  # (...)
  dynamic "tag" {
    for_each = var.custom_tags
    content {
      key                 = tag.key
      value               = tag.value
    }
  }
}

Similarly, if you want to create multiple resource groups without indexes with count meta-argument.

variable "rgs" {
    type = map
    default = {
        rg1 = {
            name = "rg-1"
        },
        rg2 = {
            name = "rg-2"
        }
    }
}
provider "azurerm" {
    features {}
}
resource "azurerm_resource_group" "dev_sub1_rg" {
    for_each = var.rgs
    name = each.value.name
    location = "eastus"
    tags = {
        Owner = "Admin"
    }
}

output "rgs_output" {
    value = azurerm_resource_group.dev_sub1_rg
}

The output would be like which is a type of Object that can you further access the properties.

rgs_output = {
  "rg1" = {
    "id" = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-1"
    "location" = "eastus"
    "name" = "rg-1"
    "tags" = {
      "Owner" = "Admin"
    }
  }
  "rg2" = {
    "id" = "/subscriptios/xxxxxxxxxx/resourceGroups/rg-2"
    "location" = "eastus"
    "name" = "rg-2"
    "tags" = {
      "Owner" = "Admin"
    }
  }
}

The expression For can be used when we want to iterate over the properties of the resources. For example, to get the Id of the resource groups above we can use the for expression as below:

resourcegroup_ids = [ for rg in azurerm_resource_group.rgs: rg.id ]

This is how we can make use of For and For_Each loop expressions.

Share your love

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *