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:
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::
depends_on
count
for_each
provider
lifecycle
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
.
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.
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
}
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
}
count
.As a best practice, we will split the code 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"]
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…
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.
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 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.
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 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.
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.
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.
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 |
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.
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.