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:
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.
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:
- 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.
- 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.
- 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
}
- 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"
},
]
}
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
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.
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
.
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.
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"
...
}
# 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.
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 thebackend_server
block - Resource instance:
<resource_type>.<resource_name>[<index>]
e.g.aws_subnet.backend_server[0]
refers to the particular instance of thebackend_server
.
Modules
- Module block:
module.<module_name>
e.g.module.server
refers to theserver
module - Module instance:
module.<module_name>[<index>]
module.server[0]
refers to the specific instance of theserver
module.
While the Terraform 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.
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.
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.
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.
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.
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.