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.
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
- Terraform Security Best Practices
- How to Migrate Terraform State Between Different Backends
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 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
.
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.
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.
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 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
.
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.
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:
- 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'scount
might not be the most suitable option. - 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 Terraformfor_each
. - 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]
}
}
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
}
}
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 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 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.
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.
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.
You can check it for free, by creating a trial account.
Written by Omkar Birade