Terraform

Terraform Count Meta-Argument – How to Use It & Examples

Terraform Count Meta-Argument

Resource blocks are the building blocks of the Terraform language. They describe one or more infrastructure objects like virtual machines, gateways, load balancers, etc. A single resource block represents a single infrastructure object. But what if we want to create multiple near-identical infrastructure objects without having to copy-paste the resource block multiple times e.g., a fleet of EC2 instances or multiple users?

This is where the Terraform count meta-argument comes into the picture. Before jumping into understanding how to use it, let’s quickly understand what meta arguments are.

We will cover:

  1. What are meta-arguments in Terraform?
  2. What is the Terraform count meta-argument?
  3. What is the Terraform count index?
  4. How to use Terraform count?
  5. Terraform count meta-argument – examples
  6. Terraform count limitations
  7. count vs. for_each
  8. Terraform count best practices

What are meta-arguments in Terraform?

Terraform defines meta-arguments as arguments that can be used with every resource type to change the resource’s behavior. Terraform supports the following meta-arguments:

  • depends_on 
  • count
  • for_each
  • provider
  • lifecycle
  • provisioner 

In this article, we will focus on the count meta-argument.

What is the Terraform count meta-argument?

count is a Terraform meta-argument that streamlines the process of creating multiple resource instances, eliminating the need to duplicate resource blocks. It can be used with both resource and module blocks. To use the count meta-argument, you need to specify the count argument within a block, which accepts a whole number that indicates the desired number of instances to create.

Terraform count use cases

You can use count in several cases:

  1. Simple provisioning of multiple resources of the same kind
resource "aws_instance" "this" {
   count         = 3
   instance_type = "t2.micro"
   ami           = "my_ami_id"
}

This will create three AWS EC2 instances.

  1. Conditional creation of resources
resource "aws_instance" "this" {
   count         =  var.create_instance ? 1 : 0
   instance_type = "t2.micro"
   ami           = "my_ami_id"
}

variable "create_instance" {
 type    = bool
 default = true
}

This will create an EC2 instance if the create_instance variable is set to true.

  1. Scaling resources dynamically using a variable
resource "aws_instance" "this" {
   count         =  var.instance_number
   instance_type = "t2.micro"
   ami           = "my_ami_id"
}

variable "instance_number" {
 type    = number
 default = 5
}
  1. Iterating over lists and creating multiple resources of the same kind
resource "aws_instance" "this" {
   count         = length(var.instances)
   instance_type = var.instances[count.index].instance_type
   ami           = var.instances[count.index].ami
}

variable "instances" {
 type    = list(object({
   instance_type = string
   ami           = string
 }))
 default = [{
   ami           = "ami1"
   instance_type = "t2.micro"
 },
 {
   ami           = "ami2"
   instance_type = "t3.micro"
 },
 ]
}

What is the Terraform count index?

The Terraform count.index is exposed when you are using count on your resources and acts as an iterator.

Let’s take a look at a simple example:

resource "aws_instance" "this" {
   count         = length(var.instances)
   instance_type = "t2.micro"
   ami           = "amiid"
   tags = {
       Name = var.instances[count.index]
   }
}

variable "instances" {
 type    = list(string)
 default = ["instance1", "instance2", "instance3"]
}

In the above example, we are setting the count to the length of our variable, which will be three, because we have three elements inside of it. In the tags, we are setting the name as var.instances[count.index], to get the name of our instances from the list.

Our list has three elements, so there are three indexes in it. Because we are dealing with a list, if you are familiar with other programming languages, you should know that the first element in any list has the index 0. So, for our example, we have the following indexes: 0 which corresponds to instance, 1 which corresponds to instance 2, and 2 which corresponds to instance 3.

Count.index will be 0 in the first iteration, 1 in the second iteration, and 2 in the last iteration. So var.intances[count.index] will translate to: 

  • var.instances[0] (which is instance1) in the first iteration
  • var.instances[1] (which is instance2) in the second iteration
  • var.instances[2] (which is instance3) in the third iteration

How to use Terraform count?

Let’s see the count meta-argument in action. 

Note: Please note that all examples provided are simplified to illustrate the functionality of the count argument and may not always adhere to the best practice.

The following code snippet demonstrates a resource block responsible for generating a single EC2 instance.

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 tags = {
   Name = "backend-server"
 }
}

Running the terraform plan command shows a plan to create a single instance of the aws_instance.backend_server resource.

# aws_instance.backend_server will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
     + availability_zone                    = (known after apply)
...

Let’s see how the plan changes with the introduction of the count meta-argument. We will set the count argument’s value to 3 to create three instances of the backend_server.

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = 3
 tags          = {
   Name = "backend-server"
 }
}

Let’s run the terraform plan command again to observe the changes.

# aws_instance.backend_server[0] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
     + availability_zone                    = (known after apply)
...
# aws_instance.backend_server[1] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
     + availability_zone                    = (known after apply)
...
# aws_instance.backend_server[2] will be created
 + resource "aws_instance" "backend-server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
     + availability_zone                    = (known after apply)

We can see that Terraform plans to create the expected three instances of the backend-server. Furthermore, it appends indices to the instance names to ensure unique identification.

However, after applying these changes using the terraform apply command, we notice that all the servers share the same name, “backend-server“. This occurs because all instances were created with identical configurations.

terraform count index

What if we want to change the configurations between instances? Let’s see how we can do that with the count object.

Terraform count meta-argument - examples

We’ll now go through some of the use case examples for the Terraform count meta-argument.

Example 1: How to use Terraform count in resource blocks?

Every Terraform resource block using the count meta-argument has the count object available in expressions.

The count object has a single attribute named index. As the name suggests, index is a sequential number for each instance starting from 0. We can use the index attribute as a part of the name to make them uniquely identifiable.

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = 3
 tags          = {
   Name = "backend-server-${count.index}"
 }
}

Let’s check the terraform plan to see if the server names reflect the changes.

# aws_instance.backend_server[0] will be updated in-place
 ~ resource "aws_instance" "backend_server" {
       id                                   = "i-06a600d1cc7cb2015"
     ~ tags                                 = {
         ~ "Name" = "backend-server" -> "backend-server-0"
       }
...

 # aws_instance.backend_server[1] will be updated in-place
 ~ resource "aws_instance" "backend_server" {
       id                                   = "i-0b24597cd1d1b0cb1"
     ~ tags                                 = {
         ~ "Name" = "backend-server" -> "backend-server-1"
       }
...

 # aws_instance.backend_server[2] will be updated in-place
 ~ resource "aws_instance" "backend_server" {
       id                                   = "i-0226ea49c4220256a"
     ~ tags                                 = {
         ~ "Name" = "backend-server" -> "backend-server-2"
...
Plan: 0 to add, 3 to change, 0 to destroy.

Great! The server names are now distinct.

Using the index attribute on its own may seem limited in its usability. However, it becomes incredibly powerful when we utilize it to reference external configurations.

Let’s explore how we can refer to an external configuration with the index attribute.

Referring to an external configuration with an index

The index attribute can also be used to refer to a list of configurations defined as variables. As a simple example, we will define and refer to unique server names.

locals {
 server_names=["backend-service-a", "backend-service-b", "backend-service-c"]
}

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = 3
 tags          = {
   Name = local.server_names[count.index]
 }
}

Of course, we can customize more parameters of a resource by referring to a list of configuration objects instead of using simple strings.

Referring to configurations outside is good. However, we are still hardcoding the value for the count.

Hardcoding values make the Terraform code less flexible and less maintainable. If you need to change the number of instances, you must manually modify the count value each time, increasing the likelihood of errors and making it harder to scale your infrastructure. Moreover, if the count values are scattered throughout the code, it becomes harder to track and manage changes. This can lead to difficulties in understanding and maintaining the configuration over time, especially as your infrastructure evolves.

Example 2: How to use Terraform count with conditional expressions?

The count argument supports using numeric expressions. For instance, we can change the resource block to derive the number of instances from the length of the list of configurations count = length(local.server_names).

locals {
 server_names=["backend-service-a", "backend-service-b", "backend-service-c"]
}

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = length(local.server_names)
 tags          = {
   Name = local.server_names[count.index]
 }
}

Running the terraform plan shows no changes since the length of the list is the same as the hard-coded value for count.

terraform output count

Is it possible to change the value of the count conditionally?

Being able to use numeric expressions opens up possibilities to play around with the number of instances conditionally. For instance, we can add an expression that returns a different value based on the instance_type.

locals {
  server_names = ["backend-service-a", "backend-service-b", "backend-service-c"]
}

variable "instance_type" {
  type	= string
  default = "t2.micro"
}

resource "aws_instance" "backend_server" {
  ami       	= "ami-07355fe79b493752d"
  instance_type = var.instance_type
  count     	= var.instance_type == "t2.micro" ? 3 : 1
  tags = {
	Name = local.server_names[count.index]
  }
}

When the default value of the instance_type variable is set to t2.micro, the terraform plan remains the same.

terraform count

But when we change the value of instance_type to t2.medium, the terraform plan shows a new plan to reduce the number of instances as expected.

terraform count conditional

Example 3: Conditional expressions with Terraform count=0

You can conditionally create resources in Terraform by leveraging count (for_each too). For any resource, if a certain condition is met, we will add a count of one, and if it is not met, we will add a count of zero, which means that the resource is not created.

resource "any_resource" "any_name" {
   count      = my_condition ? 1 : 0
   parameter1 = "this"
   ...
}

In the next section, we will learn how to use the count argument with modules.

Example 4: How to use Terraform count in module blocks?

Just the way we used the count argument with resource blocks, we can use it the same with Terraform modules.

# main.tf
module "server" {
 source = "./modules/server"
 count  = 2
}

# modules/server/main.tf
resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 tags = {
   Name = "server"
 }
}

Running the terraform plan shows the plan below.

# module.compute_servers[0].aws_instance.backend_server will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
...
# module.compute_servers[1].aws_instance.backend_server will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply
...
Plan: 2 to add, 0 to change, 0 to destroy.

An interesting observation is that the index is now positioned after the module name rather than directly following the resource name. This shift in positioning is because Terraform creates instances of the entire module instead of individual resources. Essentially, this is equivalent to re-writing the resource block multiple times.

In previous examples, referring to an instance of a resource involved adding the index at the end of the resource name, such as module.aws_instance.backend_server[0]. However, with modules, to refer to a specific instance of a resource, we must first reference the module instance followed by the resource name, like module.compute_servers[0].aws_instance.backend_server. We will learn more about this later in the article.

We will now cover dynamic values and how to refer to other resources in a block using count.

Example 5: How to use Terraform count with dynamic block?

It is important to note that the value for the count argument must be known before Terraform executes any remote resource actions. The count value cannot reference any resource attributes that are only known after the configuration is applied. For example, count can’t use a unique ID generated by the remote API when an object is created.

You cannot use count within a dynamic block, as dynamic blocks leverage only for_each. The only thing you can do (which we don’t recommend) is to create a resource using count, that leverages a dynamic block inside of it. For example, you can define something like this:

resource "aws_security_group" "this" {
 count       = length(var.security_groups)
 name        = var.security_groups[count.index].name

 dynamic "ingress" {
   for_each = var.security_groups[count.index].ingress_rules
   content {
     from_port   = ingress.value.from_port
     to_port     = ingress.value.to_port
     protocol    = ingress.value.protocol
     cidr_blocks = ingress.value.cidr_blocks
   }
 }
}

This will create security groups based on a list(object) variable, and will define ingress rules with a dynamic block that takes the ingress_rules from that variable using count.index.

It is still possible to refer to other resource blocks and data resources within the count argument. In the upcoming section, we will explore how we can achieve this.

Example 6: How to use Terraform count in data blocks and other resource blocks?

Blocks using the count meta-argument can refer to other data and resource blocks to set the value of count the same way as referring to external configurations we saw earlier.

Referring to a data resource

In Terraform, data resources are utilized to read information from existing infrastructure and can be referenced within the count argument. For example, to create an EC2 instance in each subnet of an existing VPC, we can use the data resource aws_subnets and refer to it within the aws_instance block in the count meta argument.

locals {
  vpc_id = "vpc-0742ea90775a96859"
}

data "aws_subnets" "subnets" {
 filter {
   name   = "vpc-id"
   values = [local.vpc_id]
 }
}

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = length(data.aws_subnets.subnets.ids)
 subnet_id     = data.aws_subnets.subnets.ids[count.index]
 tags          = {
   Name = "backend-server-${count.index}"
 }
}

output "subnets" {
 value = data.aws_subnets.subnets.ids
}

Here, the aws_subnets data block returns a list of subnets matching the vpc-id filter and the count meta-argument refers to derive its value: length(data.aws_subnets.subnets.ids)

The terraform plan reflects that there are four subnets in the provided vpc.

terraform data count

Since we have four subnets, Terraform will automatically create a total of four EC2 machines, one per subnet.

# aws_instance.backend_server[0] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
     + availability_zone                    = (known after apply)
...
# aws_instance.backend_server[1] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
...
# aws_instance.backend_server[2] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)
...
# aws_instance.backend_server[3] will be created
 + resource "aws_instance" "backend_server" {
     + ami                                  = "ami-07355fe79b493752d"
     + arn                                  = (known after apply)
     + associate_public_ip_address          = (known after apply)

Referring to a resource block

The count argument can just as easily refer to other resource blocks. For example, rather than referring to already existing subnets, we can create new subnets, each with an EC2 machine in it.

resource "aws_vpc" "demo_vpc" {
 cidr_block = "12.0.0.0/16"
 tags = {
   Name       = "demo-vpc"
 }
}

locals {
 cidr_blocks = [ "12.0.0.0/20", "12.0.16.0/20" ]
}

resource "aws_subnet" "demo_subnets" {
 vpc_id      = aws_vpc.demo_vpc.id
 count       = length(local.cidr_blocks)
 cidr_block  = local.cidr_blocks[count.index]
}

resource "aws_instance" "backend_server" {
 ami           = "ami-07355fe79b493752d"
 instance_type = "t2.micro"
 count         = length(aws_subnet.demo_subnets)
 subnet_id     = aws_subnet.demo_subnets[count.index].id
 tags          = {
   Name = "backend-server-${count.index}"
 }
}

An interesting observation to make here is that both aws_subnet and aws_instance are using the count argument. The aws_subnet resource refers to the cidr_blocks variable while the aws_instance resource refers to aws_subnet.

Pay attention to how we reference the id attribute of the demo_subnets instances: subnet_id = aws_subnet.demo_subnets[count.index].id. Notice that we refer to the instance instead of the resource block. We will learn about the differences between these two in the next section.

Resource block vs resource instances

We observed earlier that in the absence of the count argument Terraform uses the regular resource name to refer to the infrastructure object. However, with the count argument, Terraform uses indices to refer to specific instances. This is because when the count meta argument is used, Terraform distinguishes between the resource block and the instances of the resource.

Let’s see how they can be referred.

Resources

  • Resource block:
    <resource_type>.<resource_name>
    e.g. aws_instance.backend_server refers to the backend_server block
  • Resource instance:
    <resource_type>.<resource_name>[<index>]
    e.g. aws_subnet.backend_server[0] refers to the particular instance of the backend_server.

Modules

  • Module block:
    module.<module_name>
    e.g. module.server refers to the server module
  • Module instance:
    module.<module_name>[<index>]
    module.server[0] refers to the specific instance of the server module.

Terraform count limitations

While the Terraform count meta argument is a powerful feature, there are some limitations and considerations:

  1. Limited dynamic scaling: The count argument is evaluated during the planning phase, and the resources are provisioned based on that count. If you need dynamic scaling (e.g., adjusting the count based on runtime conditions), Terraform’s count might not be the most suitable option.
  2. Limited Logic: The count feature primarily relies on simple numeric values. If you need more complex logic or conditional creation of resources, you might need to consider other features like Terraform for_each.
  3. Unintended changes based on ordering: When using count, the resource instances are identified by an index. Modifying an element anywhere in between the list causes unintended changes for all subsequent elements.

Let’s explore how using count meta-argument can introduce unintended changes based on ordering.

In the example we discussed earlier, let’s try adding a new server somewhere in the middle of the server_names list. The expectation would be that the terraform plan reflects a plan to add a single new resource.

locals {
  server_names = ["backend-service-a", "backend-service-a1", "backend-service-b", "backend-service-c"]
}

resource "aws_instance" "backend_server" {
  ami       	= "ami-07355fe79b493752d"
  instance_type = "t2.micro"
  count     	= length(local.server_names)
  tags = {
	Name = local.server_names[count.index]
  }
}
terraform count list

We can see that instead of just adding a single new resource, Terraform plans to additionally change two existing resources. This behavior occurs because instances are identified by their index. If an element is modified anywhere in the list, it triggers changes for all subsequent elements, which is unintended.

In such cases, using the for_each meta argument is more suitable. By utilizing for_each, we can define a map or set of key-value pairs to uniquely identify each instance. This allows us to modify individual elements without affecting the others. 

Rewriting the same example using for_each, reflects the expected behavior without unintended changes to other resources.

locals {
  server_names = ["backend-service-a", "backend-service-a1", "backend-service-b", "backend-service-c"]
}

resource "aws_instance" "backend_server" {
  ami       	= "ami-07355fe79b493752d"
  instance_type = "t2.micro"
  for_each  	= toset(local.server_names)
  tags = {
	Name = each.value
  }
}
terraform count function

With the utilization of for_each, every instance is uniquely identified through a key, and this identification is independent of the order. In the case of a list, the key and value are the same.

Can you use the count argument for Terraform output values?

You cannot create multiple output values using the count argument. What you can do, is reference resources that are created with count in an output by using the splat operator (‘*’).

For example, if you have the following code that creates EC2 instances:

resource "aws_instance" "this" {
   count         = 3
   instance_type = "t2.micro"
   ami           = "my_ami_id"
}

You can define an output like this:

output "aws_intance_arns" {
   value = aws_instance.this.*.resource_arn
}

This will create a list of the arns of the instances.

for_each vs count

When should you opt for for_each instead of count? If your resource instances aren’t identical, choosing the for_each meta-argument is preferable, as it grants greater control over how objects change.

Learn more about the differences between count and for_each meta-arguments.

Terraform count best practices

When using the count meta-argument, it’s essential to follow best practices to ensure that your infrastructure code is maintainable, scalable, and avoids potential pitfalls. Here are some best practices for using the count meta-argument:

1. Avoid hardcoding valuesAvoid hardcoding values related to count whenever possible. Instead, use variables to make your configurations more flexible and adaptable to changes.

2. Use input variables for dynamic configuration Leverage input variables to dynamically configure the count value. This allows for more flexible and parameterized configurations, making it easier to adapt to different environments.

3. Consider using for_each for non-identical instances If you are dealing with non-identical instances, consider using the for_each meta-argument instead of count. This provides more control and flexibility in managing resources individually.

4. Understand the dependencies Understand the dependencies between resources when using the count meta-argument to avoid unexpected issues during resource creation or deletion.

5. Review terraform plans Before applying changes, always review the terraform plan to understand the impact of count-related modifications. This helps catch potential issues before they affect your infrastructure.

By following these best practices, you can ensure that your usage of the count meta-argument aligns with Terraform’s best practices, resulting in maintainable and scalable infrastructure.

How does Spacelift simplify working with Terraform?

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, the kind of resources you can create, and the 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 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, you can 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. The documentation provides more information on configuring private workers.

To learn more about Spacelift, create a free account today or book a demo with one of our engineers.

Key points

The count meta-argument is a powerful argument for managing multiple resources without having to repeat any code. It brings efficiency and scalability to Terraform configurations. However, the count meta-argument is not a one-size-fits-all solution. While it excels in scenarios where identical instances are needed, it may fall short in situations requiring more nuanced control over individual resources. Overall, when used judiciously, the count argument enhances the flexibility and maintainability of IaC.

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.

Automate Terraform Deployments with Spacelift

Automate your infrastructure provisioning, and build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and many more.

Start free trial

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