DEV Community

Cover image for Terraform Count Meta-Argument 101
Panchanan Panigrahi
Panchanan Panigrahi

Posted on • Edited on

Terraform Count Meta-Argument 101

In Terraform, managing infrastructure typically starts with defining resource blocks, each representing a single infrastructure component, such as an EC2 instance or a load balancer. But what happens when you need to create multiple similar resources, like a fleet of virtual machines or a group of users? Do you need to write out multiple, repetitive resource blocks?

Thankfully, there's a more efficient way! Enter the Terraform "count" meta-argument, a powerful feature that allows you to deploy multiple, nearly identical resources without duplicating code. In this blog, we'll explore how the count meta-argument simplifies scaling your infrastructure and how the count.index attribute provides precise control over individual instances. Let’s dive in!

What is the count meta-argument?

The count meta-argument in Terraform is a powerful feature that allows you to create multiple instances of a resource or module efficiently, without the need to duplicate code. By specifying the count argument within a resource, module, or data block, you provide a whole number that indicates how many instances you want to create or fetch. This helps in scaling infrastructure effortlessly, whether you need multiple EC2 instances or want to retrieve multiple data objects. It's a simple yet effective way to manage repetitive infrastructure tasks with just a single line of configuration.

Common Use Cases for count:

There are two cases where we generally use count:

  1. provisioning of multiple resources of the same kind.
  2. Conditional Resource Provisioning.

Lets understand them with examples.

Example 1: Provisioning Multiple Resources of the Same Type

resource "aws_instance" "ubuntu_instance" {
   count         = 3
   instance_type = "t2.micro"
   ami           = "ami-0a0e5d9c7acc336f1"
   tags = {
    Name = "ubuntu_server"
   }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the count meta-argument is set to 3, which means Terraform will create three identical AWS EC2 instances using the specified ami and instance_type. However, all instances will share the same tag Name = "ubuntu_server", making it difficult to distinguish between them. This could become even more challenging if you decide to scale up to 10 or more instances in the future. To handle such scenarios efficiently, you can use dynamic values with the count.index attribute to ensure each instance has a unique identifier, making management much easier.

Example 2: Using a List Variable for Dynamic Tagging

To efficiently provide unique tags for each instance, we can use a list variable containing distinct names. This approach offers greater flexibility, making it much easier to manage and distinguish multiple instances, especially as you scale your infrastructure.


variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["ubuntu_server_1", "ubuntu_server_2", "ubuntu_server_3"]
}


resource "aws_instance" "ubuntu_instance" {
   count         = length(var.instance_names)
   instance_type = "t2.micro"
   ami           = "ami-0a0e5d9c7acc336f1"
   tags = {
    Name = var.instance_names[count.index]
   }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the count meta-argument is set to the length of var.instance_names, ensuring that the number of instances matches the number of names in the list. The count.index attribute (which is a zero-based index) retrieves the corresponding name from var.instance_names for each instance. This means:

  • During the first iteration (count.index = 0), the instance gets the tag Name = "ubuntu_server_1".
  • In the second iteration (count.index = 1), the tag is Name = "ubuntu_server_2".
  • In the third iteration (count.index = 2), the tag is Name = "ubuntu_server_3".

Referring to the Instance IDs

In Terraform, when using the count meta-argument, multiple instances of a resource are created and stored in a list-like structure. This means each instance can be accessed using an index, just as you would with elements in a list or array. Let’s break down how to refer to individual instances and all instances collectively:

Accessing a Single Instance

Each instance created by Terraform is assigned an index starting from 0. To refer to the first instance, use the syntax aws_instance.ubuntu_instance[0]. This allows you to access the properties of that specific instance, such as its id.

For example, the following output block retrieves the ID of the first instance:

# Output the ID of the first ubuntu_instance
output "ubuntu_server_1_id" {
  value = aws_instance.ubuntu_instance[0].id
}
Enter fullscreen mode Exit fullscreen mode

Here, aws_instance.ubuntu_instance[0].id refers to the id attribute of the first instance in the list.

Accessing All Instances

When you want to access all instances as a whole, you can use the [*] operator, which represents all elements in the list. This method extracts the id attribute from every instance and returns them as a list of IDs.

For example:

# Output the IDs of all ubuntu_instances
output "all_ubuntu_servers_ids" {
  value = aws_instance.ubuntu_instance[*].id
}
Enter fullscreen mode Exit fullscreen mode

In this case, aws_instance.ubuntu_instance[*].id gathers the id of every instance created and stores them in a list. If you have three instances, the output will be something like: ["i-0abcd1234", "i-0abcd5678", "i-0abcd91011"].

How This Works

  • When Terraform creates resources using count, it automatically manages them in an ordered list, with count.index determining their position.
  • You can then use index-based referencing ([0], [1], etc.) to target individual instances or the [*] Splat Expressions to refer to all instances collectively.

Why This Approach Is Effective

This method solves the problem of identical tags and makes it easy to modify or expand your infrastructure. If you need more instances in the future, simply update the instance_names variable with additional names, and Terraform will handle the rest, ensuring each instance is uniquely tagged. This approach maintains clarity, efficiency, and flexibility, allowing for easy scaling and management of your infrastructure.

Example 3: Using the count Meta Argument with Conditional Expressions

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["ubuntu_server_1", "ubuntu_server_2", "ubuntu_server_3"]
}

variable "instance_type" {
  description = "The type of EC2 instance to create"
  type        = string
  default     = "t2.micro"
}

resource "aws_instance" "ubuntu_instance" {
   count         = var.instance_type == "t2.micro" ? length(var.instance_names) : 1
   instance_type = var.instance_type
   ami           = "ami-0a0e5d9c7acc336f1"
   tags = {
    Name = var.instance_names[count.index]
   }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the count meta-argument is controlled by a conditional expression:

  • count = var.instance_type == "t2.micro" ? length(var.instance_names) : 1

This line means:

  • If the instance_type variable is "t2.micro", the count value will be set to length(var.instance_names), which in this case is 3. This results in creating three EC2 instances, each with a unique name from the instance_names list.
  • If instance_type is any other value, the count becomes 1, meaning only one instance will be created.

Example 4: Using the count Meta Argument in Data Blocks

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["ubuntu_server_1", "ubuntu_server_2", "ubuntu_server_3"]
}

data "aws_instances" "ubuntu_instance" {
  count = 2

  filter {
    name   = "tag:Name"
    values = var.instance_names
  }

  filter {
    name   = "instance-state-name"
    values = ["running"]
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the count meta-argument is utilized within a data block to dynamically control how many instances of data are retrieved from AWS.

How count Works in Data Blocks

  • Dynamic Retrieval: By setting count = 2, Terraform retrieves data for two instances that match the specified filters.

  • Filter Criteria:

    • Tag Filter: The first filter looks for instances whose names match the values in var.instance_names.
    • State Filter: The second filter ensures that only instances with the state "running" are considered.

Which Instances Will Be Retrieved?

Assuming you have the following instances in AWS:

  • ubuntu_server_1 (running)
  • ubuntu_server_2 (stopped)
  • ubuntu_server_3 (running)

With the filters applied, the retrieval results will be:

  • ubuntu_server_1: Retrieved (running)
  • ubuntu_server_3: Retrieved (running)
  • ubuntu_server_2: Not retrieved (stopped)

Accessing Retrieved Instances

You can access the retrieved instances using the following syntax:

  • data.aws_instances.ubuntu_instance[0] for ubuntu_server_1.
  • data.aws_instances.ubuntu_instance[1] for ubuntu_server_3.

This structure allows you to effectively manage and interact with the relevant instance data directly within your Terraform configuration, providing a clear and organized way to handle infrastructure resources.

Example 5: Using the count Meta Argument in the Root Module with a Child Module

Child Module: modules/ec2_instance/main.tf

variable "instance_name" {
  description = "Name of the EC2 instance"
  type        = string
}

resource "aws_instance" "ubuntu_instance" {
  ami           = "ami-0a0e5d9c7acc336f1"
  instance_type = "t2.micro"

  tags = {
    Name = var.instance_name
  }
}
Enter fullscreen mode Exit fullscreen mode

Root Module: main.tf

variable "instance_count" {
  description = "Number of EC2 instances to create"
  type        = number
  default     = 3
}

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["ubuntu_server_1", "ubuntu_server_2", "ubuntu_server_3"]
}

module "ec2_instances" {
  source        = "./modules/ec2_instance"
  count         = var.instance_count
  instance_name = var.instance_names[count.index]
}

output "instance_ids" {
  description = "IDs of the created EC2 instances"
  value       = module.ec2_instances[*].id
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the Example

In this example, we explore how to effectively utilize the count meta-argument in a root module to provision multiple EC2 instances through a dedicated child module. This approach enhances modularity and reusability in Terraform configurations.

1. Child Module: Definition and Purpose

The child module, located in modules/ec2_instance/main.tf, encapsulates the logic for creating a single EC2 instance. Key components include:

  • Variable Declaration:

    • The instance_name variable allows users to specify the name for each EC2 instance. This promotes flexibility, enabling different names to be assigned easily.
  • Resource Block:

    • The aws_instance resource defines the properties of the EC2 instance, including its AMI and instance type. The instance is tagged with the name provided through the instance_name variable, ensuring that each instance can be uniquely identified in the AWS Management Console.

2. Root Module: Configuration and Logic

The root module, defined in main.tf, orchestrates the creation of multiple EC2 instances using the child module. Here’s how it works:

  • Variable Definitions:

    • instance_count: This variable dictates how many EC2 instances will be created, defaulting to 3. It provides the ability to scale the number of instances up or down based on needs.
    • instance_names: This list variable contains predefined names for each EC2 instance. This ensures that when instances are created, they can be easily distinguished by their assigned names.
  • Module Invocation:

    • The module block calls the ec2_instances module.
    • The count meta-argument is set to var.instance_count, allowing Terraform to create the specified number of instances.
    • The instance_name for each instance is assigned dynamically using var.instance_names[count.index]. This indexing allows for seamless mapping of names to instances.

3. Output Block: Retrieving Instance IDs

At the end of the configuration, the output block provides a way to retrieve the IDs of all created EC2 instances. By referencing module.ec2_instances[*].id, you can access the IDs in a straightforward manner, which is particularly useful for subsequent operations or outputs.

How It Works

When this configuration is applied, Terraform will:

  1. Create the number of EC2 instances specified by instance_count.
  2. Assign each instance a unique name from the instance_names list based on its index in the creation process.
  3. Return the IDs of the created instances, making it easy to reference them in future configurations or for monitoring purposes.

Here's a comprehensive completion that covers the limitations of the count meta-argument with well-explained details and examples:

Limitations of the count Meta Argument in Terraform

The count meta-argument in Terraform is a versatile feature that enables you to create multiple instances of a resource or module based on a specified count value. While it’s a powerful tool for scaling, it also comes with some important limitations and caveats that can lead to challenges if not carefully managed. Let’s explore these limitations and understand when it might be better to consider alternative approaches, such as for_each.

1. Lack of Flexibility with Dynamic Blocks

The count meta-argument doesn’t work well with dynamic blocks. This limitation arises because the count argument controls the creation of entire resource instances rather than individual blocks within those resources. Therefore, you can't use count to create multiple dynamic blocks within a single resource configuration.

2. Unintended Changes Due to Ordering

One of the most significant limitations of using count is its reliance on the order of the list used to generate resource instances. Changes in the order of this list can lead to potentially destructive changes in your infrastructure, as Terraform may interpret these changes as the need to delete and recreate resources—even if the actual data hasn’t changed.

Example:

Let's consider an example with AWS EC2 instances where the count meta-argument is based on a list variable.

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["linux_server_1", "linux_server_2", "linux_server_3", "linux_server_4"]
}

resource "aws_instance" "linux_instance" {
   count         = length(var.instance_names)
   instance_type = "t2.micro"
   ami           = "ami-12345678"  # Example AMI ID

   tags = {
     Name = var.instance_names[count.index]
   }
}
Enter fullscreen mode Exit fullscreen mode

With this configuration, Terraform creates instances based on the instance_names list as follows:

  • aws_instance.linux_instance[0] is assigned to linux_server_1
  • aws_instance.linux_instance[1] is assigned to linux_server_2
  • aws_instance.linux_instance[2] is assigned to linux_server_3
  • aws_instance.linux_instance[3] is assigned to linux_server_4

However, if we change the instance_names variable to:

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["linux_server_1", "linux_server_3", "linux_server_4"]
}
Enter fullscreen mode Exit fullscreen mode

Terraform would now interpret the instances as:

  • aws_instance.linux_instance[0] for linux_server_1
  • aws_instance.linux_instance[1] for linux_server_3
  • aws_instance.linux_instance[2] for linux_server_4

Notice that linux_server_2 has been removed, and the remaining instances have shifted their positions. As a result, Terraform will plan to destroy linux_server_2 and recreate linux_server_3 and linux_server_4 because their index positions have changed. This can cause unexpected downtime or disruptions, especially in a production environment with a large number of instances.

The Risks of Using count

This issue becomes even more critical with stateful resources such as databases (e.g., RDS instances) where data loss can occur if resources are unintentionally recreated. Relying on count in such scenarios can be risky and might lead to downtime or data loss if not handled carefully.

A Better Alternative: Using for_each

For use cases where you need more control and flexibility, the for_each meta-argument offers a safer and more reliable option. Unlike count, for_each allows you to create resources based on a map or set of strings, and the resource instances are uniquely identified by their keys rather than their index. This makes it much more robust against changes in ordering.

For example, using for_each:

variable "instance_names" {
  description = "List of names for each EC2 instance"
  type        = list(string)
  default     = ["linux_server_1", "linux_server_2", "linux_server_3", "linux_server_4"]
}

resource "aws_instance" "linux_instance" {
   for_each = toset(var.instance_names)
   instance_type = "t2.micro"
   ami           = "ami-12345678"  # Example AMI ID

   tags = {
     Name = each.key
   }
}
Enter fullscreen mode Exit fullscreen mode

With this approach, each instance is created with a unique key, meaning changes in the list won’t lead to the destruction and recreation of resources. This makes for_each a much safer choice for dynamic configurations.

Final Thoughts

While the count meta-argument is a convenient tool for scaling resources, its limitations, particularly with dynamic blocks and ordering sensitivity, can make it unsuitable for more complex configurations. It's crucial to be aware of these limitations to avoid unintended disruptions in your infrastructure. When you need greater control, flexibility, and reliability, consider using for_each as a more robust alternative in your Terraform configurations. This ensures that your infrastructure remains resilient and adaptable to changes without the risk of unnecessary downtime or data loss.

Top comments (0)