How to Use Terraform Count Meta-Argument
Tags: terraform, infrastructureascode
Published: 2024-06-24 11:43:43

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 count *meta-argument* comes into the picture. Before jumping into understanding how to use it, let's quickly understand what meta arguments are.

##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` 

The scope of this article is limited to the count meta-argument.

##What is the Terraform count meta-argument?
The Terraform `count` meta-argument simplifies the creation of multiple resource instances without having to repeat the same resource block multiple times. 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 value representing the number of instances you want to create.

##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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-count-index.png&w=3840&q=75)

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

💡 You might also like:

-   [How to Scale Terraform Deployments with GitHub Actions](https://spacelift.io/blog/github-actions-terraform)
-   [Terraform Security Best Practices](https://spacelift.io/blog/terraform-security)
-   [How to Migrate Terraform State Between Different Backends](https://spacelift.io/blog/terraform-migrate-state)

##Terraform count meta-argument - examples

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

### How to use Terraform count in resource blocks

Every [Terraform resource block](https://spacelift.io/blog/terraform-resources) 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]
 }
}
```

We can, of course, customize more parameters of a resource by referring to a list of objects of configurations 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.

### 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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-output-count.png&w=3840&q=75)

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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-count.png&w=3840&q=75)

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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-count-conditional.png&w=3840&q=75)

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

### 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 ](https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work)[modules](https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work).

```
# 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`.

### 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.

However, 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.

### 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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-data-count.png&w=3840&q=75)

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.

##Terraform count limitations

While the 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 ](https://spacelift.io/blog/terraform-for-each)[`for_each`](https://spacelift.io/blog/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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-count-list.png&w=3840&q=75)

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](https://spacelift.io/_next/image?url=https%3A%2F%2Fspaceliftio.wpcomstaging.com%2Fwp-content%2Fuploads%2F2023%2F12%2Fterraform-count-function.png&w=3840&q=75)

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.

##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 ](https://spacelift.io/blog/terraform-count-for-each)[count and for_each meta-arguments](https://spacelift.io/blog/terraform-count-for-each).

##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 values

Avoid 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.

### 4\. 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](https://spacelift.io/blog/5-ways-to-manage-terraform-at-scale).

##Key points
The `count` meta-argument is a powerful argument to create multiple instances 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.

We encourage you also to explore [how Spacelift makes it easy to work with Terraform](https://docs.spacelift.io/).

If you need any help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this. It supports Git workflows, policy as code, programmatic configuration, context sharing, drift detection, and many more great [features right out of the box](https://spacelift.io/blog/how-specialized-solution-can-improve-your-iac).

You can check it for free, by [creating a trial account](https://spacelift.io/).

_Written by Omkar Birade_