Terraform

Terraform Count vs. For Each Meta-Argument – When to Use It

Terraform count

In this article, I will explain the Terraform count and for_each meta-arguments. I will provide examples of how to use each one, when to use them, and when not to use them! Let’s jump in!

What is count in Terraform?

When you define a resource block in Terraform, by default, this specifies one resource that will be created. To manage several of the same resources, you can use either count or for_each, which removes the need to write a separate block of code for each one. Using these options reduces overhead and makes your code neater.

count is what is known as a ‘meta-argument’ defined by the Terraform language. Meta-arguments help achieve certain requirements within the resource block.

For example, instead of defining 3 virtual machines all with their own separate resource blocks, you could define one and add the count = 3 ‘meta-argument’ into the resource block.

For completeness, There are actually five meta-arguments that can be used within resource blocks at the time of writing:

  • depends_on
  • count
  • for_each
  • provider
  • lifecycle

More on for_each later!

Meta-arguments can also be used within modules, which differ slightly from the resource meta-arguments:

  • depends_on
  • count
  • for_each
  • providers

Note count, for_each, depends_on are the same between resource blocks and modules, lifecycle is omitted and provider becomes providers.

How to use Terraform Count

The count meta-argument accepts a whole number and creates the number of instances of the resource specified.

When each instance is created, it has its own distinct infrastructure object associated with it, so each can be managed separately. When the configuration is applied, each object can be created, destroyed, or updated as appropriate.

Deploying Multiple Resources with Count

Let’s look at an example.

The code below sets up the remote backend for the Terraform state file in Azure Storage, and then creates a single group in Azure AD called Group1.

provider "azurerm" {
  features {}
}

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=2.95.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = ">=2.17.0"
    }
  }
  backend "azurerm" {
    resource_group_name  = "tf-rg"
    storage_account_name = "jacktfstatesa"
    container_name       = "terraform"
    key                  = "adgroups.tfstate"
  }
}

resource "azuread_group" "ad_group" {
  display_name = "Group1"
  security_enabled = false
  mail_enabled = false
}
Deploying multiple resources with count

Consider we now want to add 3 Groups.

We could simply duplicate the azuread_group 3 times, like so:

resource "azuread_group" "ad_group" {
  display_name = "Group1"
  security_enabled = false
  mail_enabled = false
}

resource "azuread_group" "ad_group" {
  display_name = "Group2"
  security_enabled = false
  mail_enabled = false
}

resource "azuread_group" "ad_group" {
  display_name = "Group3"
  security_enabled = false
  mail_enabled = false
}

Consider we now need to add another 50 Groups! This creates unnecessary code and duplicates resource blocks. Instead, we can use count.

As is best practice, we will split the code up into 3 files, main.tf, variables.tf and terraform.tfvars.

split the code up

main.tf

provider "azurerm" {
  features {}
}

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=2.95.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = ">=2.17.0"
    }
  }
  backend "azurerm" {
    resource_group_name  = "tf-rg"
    storage_account_name = "jacktfstatesa"
    container_name       = "terraform"
    key                  = "adgroups.tfstate"
  }
}

resource "azuread_group" "ad_group" {
  count = 3
  display_name = var.ad_group_names[count.index]
  security_enabled = false
  mail_enabled = false
}

variables.tf

variable ad_group_names {
    type = list(string)
    description = "List of all the AD Group names"
}
ad_group_names = ["Group1", "Group2", "Group3"]

On running this code, 3 groups will be created, with the names Group1, Group2 and Group3. This code will neatly create as many groups as required with a single resource block, more can be added by increasing the count value and adding a group name to the terraform.tfvars file.

On line 26 on the main.tf file, the count.index variable is used. This represents the index number of the current count loop. The count index starts at 0 and increments by 1 for each resource. You include the count index variable in strings using interpolation. If you don’t want to start the count at 0 you can start at another value by incrementing the value, e.g. to Increment the count by 3:

count.index + 3

This is also handy to add numbers to the names of resources, for example when you are creating multiple VMs using count, the count.index variable can be appended to the name to make it unique.

But we can make the code better…

How to use expressions in count?

The count meta-argument also accepts numeric expressions. The values of these expressions must be known at runtime, before Terraform performs any remote resource actions. Terraform must know the values of the expression and can’t refer to any resources that values are known for only after the configuration is applied.

For example, consider we have a simple module to create a group in Azure Active Directory. This consists of 3 files, main.tf, output.tf, and variables.tf contained in a folder.

group in Azure Active Directory

main.tf

resource "azuread_group" "ad_group" {
  count                   = length(var.ad_group_names)
  display_name            = var.ad_group_names[count.index]
  owners                  = var.group_owners_list
  prevent_duplicate_names = true
  security_enabled        = true
}
output "ad_group_names" {
  value = azuread_group.ad_group.*.display_name
}

output "ad_group_id" {
  value = azuread_group.ad_group.*.id
variable "ad_group_names" {
  type        = list(string)
  description = "list of all the AD groups that were created"
}

variable "group_owners_list" {
  type        = list(string)
  description = "The name of the of the owner to be added to the groups"
}

On line 2 main.tf uses the ‘collection function’ length to inspect how many items are being passed in the variable var.ad_group_names , and creates a new Azure Active Directory group for each one. length determines the length of a given list, map, or string.

To call the module, we need the main.tf, variables.tf and terraform.tfvars files.

module "azure_ad_groups" {
  source          = "../../modules/azure_ad_group"
  ad_group_names  = var.groups_list
  group_owners_list = var.group_owners_list
}
variable "group_owners_list" {
  type        = list(string)
  description = "The name of the of the owner to be added to the groups"
}

variable "groups_list" {
  type        = list(string)
  description = "The name of the of the groups"
}
group_owners_list = ["jack.roper@madeup.com"]
groups_list = ["Group1", "Group2", "Group3"]

On running terraform apply, this configuration would again result in 3 Azure AD groups being created, as we have specified 3 in the groups_list variable in the terraform.tfvars file (Group1, Group2 and Group3). The benefit of this from the previous example is that we no longer have to manually update the count value, as this is calculated depending on how many group names are specified in the terraform.tfvars file.

Azure groups

Deploying Multiple Resources with For Each

Since this post focuses on using the count meta-argument, it is also good to know when and how to use for_each.

Like the count argument, the for_each meta-argument creates multiple instances of a module or resource block. However, instead of specifying the number of resources, the for_each meta-argument accepts a map or a set of strings. This is useful when multiple resources are required that have different values. Consider our Active directory groups example, with each group requiring a different owner.

A map would be defined like this:

for_each = {
    "Group1" = "jack.roper@madeup.com",
    "Group2" = "bob@madeup.com",
    "Group3" = "john@madeup.com"
}

A set of strings would be defined like this (used with the toset function to convert a list of strings to a set):

for_each = toset( ["Group1", "jack.roper@madeup.com"] ["Group2", "bob@madeup.com"] ["Group3", "john@madeup.com"])

When providing a set, you must use an expression that explicitly returns a set value, like the toset function.

To reference these definitions, the each object is used.

If a map is defined, each.key will correspond to the map key, and each.value will correspond to the map value.

In the example above, each.key would show the group name (Group1, Group2, or Group3), and each.value would show the emails (“jack.roper@madeup.com”, “bob@madeup.com”, or “john@madeup.com”).

If a set of strings is defined, each.key or each.value can be used and will correspond to the same thing.

When to use for_each Instead of count

If the resources you are provisioning are identical or nearly identical, then count is a safe bet. However if elements of the resources change between the different instances, then for_each is the way to go.

Summary

count and for_each are powerful meta-arguments that can be used to make your code simpler, more efficient, saving you valuable time and effort!

Check out the official Hashicorp docs for more details on count, for_each, length and toset.

Cheers!

Discover better way to manage Terraform

Spacelift helps manage Terraform state, build more complex workflows, supports policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.

Start free trial