Going to AWS re:Invent 2024?

➡️ Book a meeting with Spacelift

Terraform

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

Terraform count

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

What we will cover:

  1. Meta-arguments in Terraform
  2. What is count in Terraform?
  3. Terraform count example: Deploying multiple resources with count
  4. How to use expressions in count?
  5. What is for_each in Terraform?
  6. Terraform for_each example: Deploying multiple resources with for_each
  7. When to use for_each instead of count?

Meta-arguments in Terraform

By default, defining a resource block in Terraform specifies one resource that will be created. To manage several of the same resources, you can use either count or for_each, so you don’t have to write a separate block of code for each one. Using these options reduces overhead and makes your code neater.

Five meta-arguments can be used within resource blocks::

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.

What is count in Terraform?

Terraform count is 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.

How to use Terraform count

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

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

Terraform count example: Deploying multiple resources

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

Let’s now 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 a best practice, we will split the code into three 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"]

Three groups — Group1, Group2, and Group3 — are created when you run this code. This code neatly creates 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.

The count.index variable is used on line 26 on the main.tf file. 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 approach is also handy for adding 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 three 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 two 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 create three Azure AD groups, as we have specified three in the groups_list variable in the terraform.tfvars file (Group1, Group2 and Group3). 

As we saw in the previous example, the benefit of this is that we don’t have to update the count value manually, as this is calculated depending on how many group names are specified in the terraform.tfvars file.

Azure groups

What is for_each in Terraform?

for_each is another loop in Terraform that lets you create multiple resources of the same kind. To use it, you will need to either leverage a map or a set expression. These expressions can be defined either as a variable, a local, or even directly as an expression at the resource level inside the for_each parameter.

for_each exposes one attribute called each. This attribute contains a key and value (if you are using a map), and a value (if you are using a set). These can be used with each.key and each.value.

How to use Terraform for_each

Let’s look at an example that uses a map(object) local expression:

locals {
 instances = {
   dev1 = {
     instance_type = "t2.micro"
     ami_id        = "dev1ami"
   }
   dev2 = {
     instance_type = "t3.micro"
     ami_id        = "dev2ami"
   }
   dev3 = {
     instance_type = "t3.micro"
     ami_id        = "dev3ami"
   }
 }
}

resource "aws_instance" "this" {
 for_each = local.instances

 instance_type = each.value.instance_type
 ami           = each.value.ami_id

 tags = {
   Name = each.key
 }
}

In this example, we will create three instances:

  • The first one will have a “t2.micro” instance type, a “dev1ami” ami, and a “dev1” Name tag.
  • The second one will have a “t3.micro” instance type, a “dev2ami” ami, and a “dev2” Name tag.
  • The third one will have a “t3.micro” instance type, a “dev3ami” ami, and a “dev3” Name tag.

Terraform for_each example: Deploying multiple resources

Because 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 with different values are required. Consider our Active directory groups example, with each group requiring a different owner.

A map is 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 will show the group name (Group1, Group2, or Group3), and each.value will 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, for_each is the way to go.

Let’s look at an example:

locals {
   instances = [
       {
           instance_type = "t2.micro"
           ami_id        = "dev1ami"
       },
       {
           instance_type = "t3.micro"
           ami_id        = "dev2ami"
       },
       {
           instance_type = "t3.micro"
           ami_id        = "dev3ami"
       }
   ]
}

resource "aws_instance" "this" {
   count         = length(local.instances)
   instance_type = local.instances[count.index].instance_type
   ami           = local.instances[count.index].ami_id
}

In this example, if you create these instances and want to remove the second one and reapply the code, this will recreate your third instance because of the index change — the third instance initially has an index 2, but after the second instance is removed, the third instance will have an index 1. 

Imagine having 50 instances in your configuration and needing to remove one from the beginning of the list — almost all the instances will be recreated.

This is where for_each shines because each element will have a name instead of an index, and removing one element from the map or set will not affect the other elements.

Also, count can be used to conditionally create resources easier than for_each:

locals {
  enable_instance = true
}
resource "aws_instance" "this" {
   count         = local.enable_instance ? 1 : 0 
}

In the above example, an instance will be created only if local.enable_instance is true.

Terraform count vs. for_each table summary

Below, you can find the table summary for Terraform count and for_each meta arguments.

Type Description Use case
count Meta-argument Based on a count value Resources you are provisioning are identical
for_each Meta-argument Based on a set of input values Resources change between the different instances

Deploying Terraform resources with Spacelift

Terraform is really powerful, but to achieve an end-to-end secure Gitops approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:

  • Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
  • Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation,  create dependencies among them, and share outputs
  • Build self-service infrastructure – You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
  • Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to integrate security tools in your workflows using Custom Inputs.

Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. For more information on configuring private workers, refer to the documentation.

You can check it for free by creating a trial account or booking a demo with one of our engineers.

Summing up

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

Terraform count is used when you want to create a specific number of resources based on a fixed or conditional count, producing a single resource list indexed numerically. You use for_each to create resources based on a map or set of strings, enabling more granular control by associating each resource with a unique key from the collection.

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.

Learn more

The Practitioner’s Guide to Scaling Infrastructure as Code

Transform your IaC management to scale

securely, efficiently, and productively

into the future.

ebook global banner
Share your data and download the guide