In today’s blog post, we will learn terraform count meta argument. The count meta argument is used mainly for iteration (looping). For example, using count you can provision multiple resources with the same configuration block or use count with a conditional expression to execute a terraform block based on some condition. The count meta argument accepts a whole number as input.
In this blog post, we will cover how to master terraform count meta argument, exploring its core functionality, how to use it effectively across different Terraform blocks, important considerations that you have to keep in mind while using count meta argument.
The Problem: Manual Duplication and Inflexibility
Before count
, creating multiple similar resources often involved duplicating same configurations block. Imagine needing three identical web servers:
# Without `count` - Manual Duplication
resource "aws_instance" "web_server_01" {
ami = "ami-xxxxxxxxxxxxxxxxx" # Replace with valid AMI
instance_type = "t2.micro"
tags = {
Name = "web-server-01"
}
}
resource "aws_instance" "web_server_02" {
ami = "ami-xxxxxxxxxxxxxxxxx"
instance_type = "t2.micro"
tags = {
Name = "web-server-02"
}
}
resource "aws_instance" "web_server_03" {
ami = "ami-xxxxxxxxxxxxxxxxx"
instance_type = "t2.micro"
tags = {
Name = "web-server-03"
}
}
The drawbacks are immediately apparent:
- Verbosity: A lot of copy-pasting, making configurations long and cumbersome.
- Error-Prone: Easy to introduce typos or inconsistencies when repeating blocks.
- Scalability Challenges: Adding a fourth server means manually adding another block; scaling down means deleting blocks. This is not infrastructure as code as much as infrastructure as copy-paste.
- Maintenance Overhead: Updating a common attribute (e.g., instance type) requires changing it in multiple places.
count
solves these problems by allowing you to define a resource (or data source, or module) block once and tell Terraform to create multiple instances of it.
Introducing count
The count
meta-argument tells Terraform how many identical (or very similar) instances of a resource, data source, or module block to create. It takes a whole number, and based on that number, Terraform creates that many separate, manageable instances.
When count
is used, Terraform exposes a special variable called count.index
within the block. count.index
is a zero-based number that uniquely identifies each instance. For example, if count = 3
, count.index
will be 0
, 1
, and 2
for the respective instances.
resource "resource_type" "resource_name" {
count = 3 # Creates 3 instances of this resource
# ... configuration ...
# You can use count.index here to differentiate instances
}
Where count Can Be Used (and how count.index works)
The power of count
extends beyond just resources, making it versatile for various infrastructure patterns.
1. Resources
This is the most common and intuitive use of count
: provisioning multiple instances of a given resource type.
Purpose: Create N
identical (or nearly identical) resources from a single definition.
Syntax:
resource "type" "name" {
count = N
# ... other resource configurations ...
}
Accessing Individual Instances: When you use count
, the resource becomes a list of objects. You access individual instances using square brackets and their count.index
: aws_instance.web_server[0]
, aws_instance.web_server[1]
, etc.
Example: Dynamic Naming and Attributes
# Create a VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
# Define a list of availability zones
variable "azs" {
description = "List of AZs for subnets"
type = list(string)
default = ["ap-south-1a", "ap-south-1b", "ap-south-1c"]
}
# Create multiple subnets, one for each AZ
resource "aws_subnet" "public" {
count = length(var.azs) # Create a subnet for each AZ in the list
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index) # Dynamically calculate CIDR
availability_zone = var.azs[count.index] # Assign different AZs
tags = {
Name = "public-subnet-${count.index}"
}
}
# Create a cluster of web servers, using a specific subnet for each
resource "aws_instance" "web_server" {
count = 2 # Create 2 web servers
ami = "ami-0abcdef1234567890" # Replace with valid AMI
instance_type = "var.instance_type" # Example: var.instance_type = "t2.micro"
subnet_id = aws_subnet.public[count.index].id # Assign to respective public subnet
tags = {
Name = "web-server-${count.index}" # Unique name for each server
}
}
# Example of conditional resource creation
resource "aws_s3_bucket" "log_bucket" {
count = var.create_s3_bucket ? 1 : 0 # Create bucket only if var.create_s3_bucket is true
bucket = "my-conditional-log-bucket-unique"
}
In the aws_subnet
example, count.index
allows us to iterate through a list of AZs and derive unique CIDR blocks for each subnet. For aws_instance
, count.index
creates unique names and assigns them to different subnets (assuming we have at least as many subnets as instances). Setting count = 0
(e.g., using a conditional var.create_s3_bucket ? 1 : 0
) is a common pattern to conditionally provision resources.
2. Data Sources
count
can also be used with data
blocks to query details about multiple existing, similar infrastructure objects.
Purpose: Fetch information about N
pre-existing objects that follow a pattern.
Syntax:
data "type" "name" {
count = N
# ... other data source configurations ...
}
Accessing Individual Instances: Similar to resources, data sources become a list of objects: data.type.name[index].attribute
.
Example: Querying Multiple AMIs
# Assuming you have a consistent naming convention for your AMIs, e.g., "my-app-ami-1", "my-app-ami-2"
variable "ami_names" {
description = "List of AMI names to fetch"
type = list(string)
default = ["my-app-ami-1", "my-app-ami-2"]
}
data "aws_ami" "app_amis" {
count = length(var.ami_names)
most_recent = true
owners = ["self"]
name_regex = "${var.ami_names[count.index]}.*" # Dynamically construct regex for each AMI name
}
output "found_ami_ids" {
value = [for ami in data.aws_ami.app_amis : ami.id]
}
Here, data.aws_ami.app_amis
will fetch two separate AMI details, allowing you to iterate through them or use their attributes individually.
3. Modules
When you want to deploy a reusable Terraform module multiple times, count
can be applied to the module
block. This creates N
independent instances of the module, each with its own set of resources defined within it.
Purpose: Instantiate a reusable module N
times, effectively deploying N
identical stacks.
Syntax:
module "name" {
count = N
source = "./path/to/module"
# ... module input variables ...
}
Accessing Individual Instances: Module outputs become a list when count
is used: module.my_module[index].output_name
.
Example: Deploying Multiple Identical Environments
# modules/app_stack/main.tf (defines VPC, subnet, EC2, SG, etc.)
# modules/app_stack/outputs.tf (defines app_endpoint, vpc_id, etc.)
variable "environments" {
description = "List of environment names to deploy"
type = list(string)
default = ["dev", "prod"]
}
module "app_environment" {
count = length(var.environments)
source = "./modules/app_stack"
# Pass environment-specific variables
env_name = var.environments[count.index]
instance_type = var.environments[count.index] == "prod" ? "m5.large" : "t3.micro"
vpc_cidr_block = "10.${10 + count.index}.0.0/16" # Example: 10.10.0.0/16 for dev, 10.11.0.0/16 for prod
}
output "app_endpoints" {
value = { for idx, env in var.environments : env => module.app_environment[idx].app_endpoint }
}
This pattern allows you to deploy complete, isolated application stacks for development and production, or for different teams, all from a single module.
4. Output Values (Implicit Usage)
While count
is not directly placed inside an output
block, count.index
is frequently used within output
values to transform or collect data from count
-managed resources, data sources, or modules. When a resource (or data, or module) uses count
, it naturally becomes a list.
Purpose: Aggregate attributes from multiple count
-managed blocks into a single output list or map.
Example: Exposing IPs of a Scaled Cluster
# (Assuming aws_instance.web_server from earlier example)
output "web_server_public_ips" {
description = "Public IPs of the web servers"
value = [for server in aws_instance.web_server : server.public_ip]
}
output "web_server_names_by_index" {
description = "Names of web servers indexed"
value = { for idx, server in aws_instance.web_server : idx => server.tags.Name }
}
The for
expression is a powerful way to iterate over the list created by count
and project specific attributes into a desired output format.
Best Practices and Considerations
Mastering count
involves more than just knowing the syntax; it requires understanding its behavior and limitations.
- Readability vs. Complexity: Use
count
when you have truly similar resources. If resources have wildly different configurations, separate resource blocks (or even separate modules) might be clearer, even if it means more lines of code. The goal is maintainability. - Impact of Changing
count
Value:- Increasing
count
: Terraform will create new instances for the newcount.index
values. This is generally safe. - Decreasing
count
: Terraform will destroy the instances with the highestcount.index
values. This is a destructive operation and requires careful review of theterraform plan
. Always double-check the plan before applying acount
reduction in a production environment.
- Increasing
for_each
vs.count
:count
is best for simple numeric scaling where the order or number is the primary differentiator (e.g., “I need 5 web servers”). If you reducecount
from 5 to 3, Terraform destroys instances[3]
and[4]
.for_each
is better when you need to manage resources based on unique string keys (e.g., “I need a server for ‘dev’, ‘test’, and ‘prod'”). If you remove ‘test’ from afor_each
loop, only the ‘test’ server is destroyed, regardless of its position in a list.for_each
maintains resource identity based on the key, making it safer for non-numerical, identity-sensitive collections.- Rule of thumb: If you need to refer to instances by a name or identifier other than a simple number,
for_each
is often the superior choice. If the order does not matter, or a simple numerical index is sufficient,count
is perfectly fine.
- Using
count
with Modules: When you applycount
to a module, Terraform treats each module instance as a separate entity. This means each module instance will manage its own state and dependencies. - Zero Count for Conditional Creation: A powerful pattern for conditionally creating a resource (or data, or module) is to set
count = 0
if a condition is false, andcount = 1
if it is true.
resource "aws_db_instance" "rds" {
count = var.create_database ? 1 : 0
# ... db configuration ...
}
Conclusion
The count
meta-argument makes your infrastructure more scalable, and simplifies maintenance by allowing you to define a single blueprint for multiple similar components. By understanding how count
operates with count.index
across resources, data sources, and modules, you can unlock terraform true potential in your infrastructure as code journey. While for_each
often provides a more robust solution for identity-sensitive collections, count
remains an indispensable tool for straightforward numerical scaling.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com