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!
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.
Terraform 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 three 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
.
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.
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
}
Consider we now want to add three Groups.
We could simply duplicate the azuread_group three 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 three files, main.tf, variables.tf and terraform.tfvars.
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"
}
terraform.tfvars
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 three:
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…
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.
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.tf
output "ad_group_names" {
value = azuread_group.ad_group.*.display_name
}
output "ad_group_id" {
value = azuread_group.ad_group.*.id
variables.tf
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.
main.tf
module "azure_ad_groups" {
source = "../../modules/azure_ad_group"
ad_group_names = var.groups_list
group_owners_list = var.group_owners_list
}
variables.tf
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"
}
terraform.tfvars
group_owners_list = ["jack.roper@madeup.com"]
groups_list = ["Group1", "Group2", "Group3"]
On running terraform apply
, this configuration would again result in three Azure AD groups being created, as we have specified three 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.
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.
More on Terraform for_each meta-argument.
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.
For some of our Terraform users, the most convenient solution to configure a stack is to specify its input values in a variable definitions file that is then passed to Terraform executions in plan and apply phases. Spacelift supports this approach but does not provide a separate mechanism, depending instead on a combination of Terraform’s built-in mechanisms and Spacelift-provided primitives like environment variables, mounted files, and before_init
scripts. Learn more about how Spacelift can help you with handling .tfvars and get started on your journey by creating a trial account.
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!
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
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.