Terraform Count Meta Argument: Examples, Best Practices, and Common Pitfalls
The count meta argument is one of Terraform's simplest yet most powerful features for creating multiple resource or module instances from a single configuration block. Instead of copying the same resource definition several times, you define it once and let Terraform generate the required number of instances automatically.
Whether you're provisioning multiple EC2 instances, creating several storage buckets, deploying identical virtual machines, or enabling resources conditionally, count helps reduce duplication while keeping your Infrastructure as Code (IaC) clean and maintainable.
However, count also has an important limitation: Terraform identifies resources using numeric indexes. If those indexes change, Terraform may replace existing infrastructure even when the underlying resources appear unchanged. Understanding this behavior is essential before using count in production environments.
If you're new to Terraform meta arguments, consider reading our guide on Terraform Meta Arguments first, since count is one of several mechanisms Terraform provides to control resource behavior.
Executive Summary
If you only remember a few things about Terraform count, make them these:
countcreates multiple instances of the same resource or module from one configuration block.- Every instance receives a numeric index through
count.index, starting at0. - Use
countwhen resources are nearly identical and differ only by an index or a few computed values. - Avoid
countfor long-lived resources whose identity must remain stable. - Changing list order or removing elements can shift indexes, causing Terraform to recreate resources.
- Use
for_eachinstead ofcountwhen resources have unique names, IDs, or other stable identifiers. - Always review
terraform planbefore applying changes to resources managed withcount.
Prerequisites
To follow the examples in this guide, you should have:
- Basic familiarity with Terraform configuration files.
- Terraform CLI installed on your machine.
- A basic understanding of:
- Providers
- Resources
- Variables
- Terraform state
- (Optional) An AWS account if you want to try the EC2 examples.
We'll use both the local provider and the AWS provider throughout this guide to demonstrate real-world usage.
What Is the count Meta Argument in Terraform?
The count meta argument tells Terraform how many instances of a resource or module should be created.
Instead of writing identical resource blocks multiple times, you define the resource once and specify the desired number of instances using count.
For example:
resource "aws_instance" "web" {
count = 3
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
}
Terraform interprets this configuration as:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
Although you wrote only one resource block, Terraform creates three independent resource instances.
This approach keeps your infrastructure code:
- More concise
- Easier to maintain
- Less error-prone
- More scalable
The official Terraform language documentation classifies count as one of Terraform's core meta arguments, meaning it modifies how Terraform processes a resource or module rather than configuring the resource itself.
Note
Unlike ordinary resource arguments (such as
amiorinstance_type), meta arguments influence Terraform's execution behavior.
Where Can You Use count?
Terraform supports count in multiple places, including:
- Resources
- Modules
Examples include:
resource "aws_s3_bucket" "logs" {
count = 3
}
and
module "network" {
count = 2
source = "./modules/network"
}
Terraform creates multiple independent instances in both cases.
How Terraform Processes count
During the planning phase, Terraform evaluates the value assigned to count.
For example:
count = 5
Terraform immediately expands the resource into five separate instances:
resource[0]
resource[1]
resource[2]
resource[3]
resource[4]
Each instance:
- Has its own state entry
- Has its own lifecycle
- Can be created or destroyed independently
- Receives its own numeric index
Internally, Terraform stores each instance separately inside the state file.
How the count Meta Argument Works
Terraform follows a straightforward workflow whenever it encounters count.
- Read the resource block.
- Evaluate the expression assigned to
count. - Determine how many instances are required.
- Create indexed resource instances.
- Store each instance independently in the Terraform state.
Suppose the configuration is:
resource "local_file" "pet" {
count = 3
filename = "/root/pet-${count.index}.txt"
content = "Terraform Demo"
}
Terraform generates:
local_file.pet[0]
local_file.pet[1]
local_file.pet[2]
These are not copies of one object—they are three independent resources tracked individually by Terraform.
Resource Addressing with count
Terraform assigns every instance a unique resource address.
Example:
local_file.pet[0]
local_file.pet[1]
local_file.pet[2]
These addresses appear in:
- Terraform state
- Terraform plan
- Terraform apply
- Terraform import
- Terraform output
This addressing model becomes especially important when modifying or troubleshooting infrastructure.
Understanding count.index
Whenever Terraform creates multiple instances using count, it automatically exposes the special expression:
count.index
count.index returns the numeric index of the current resource instance.
Indexing always begins at 0.
Example:
count = 4
Terraform assigns:
Instance 1 → count.index = 0
Instance 2 → count.index = 1
Instance 3 → count.index = 2
Instance 4 → count.index = 3
Since every resource receives a different index, you can use it to generate unique values.
For example:
resource "local_file" "pet" {
count = 3
filename = "/tmp/pet-${count.index}.txt"
content = "File ${count.index}"
}
Terraform creates:
pet-0.txt
pet-1.txt
pet-2.txt
Without count.index, every generated resource would attempt to use the same filename, resulting in conflicts.
Common Uses of count.index
You'll frequently use count.index for:
- Resource names
- Tags
- File paths
- DNS records
- Port numbers
- Availability zones
- List indexing
Example:
tags = {
Name = "web-${count.index}"
}
Result:
web-0
web-1
web-2
Basic Examples of Terraform count
Let's build progressively more realistic examples.
Example 1: Creating Multiple Local Files
resource "local_file" "notes" {
count = 3
filename = "/tmp/file-${count.index}.txt"
content = "Generated by Terraform"
}
Terraform creates:
file-0.txt
file-1.txt
file-2.txt
Notice that only one configuration block produces three resources.
Example 2: Creating Multiple EC2 Instances
Provision three identical EC2 instances.
resource "aws_instance" "web" {
count = 3
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
Terraform creates:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
Each instance receives a unique name:
web-0
web-1
web-2
This pattern works well for:
- Test environments
- Worker nodes
- Batch processing servers
- Temporary development infrastructure
Example 3: Creating Multiple Security Groups
resource "aws_security_group" "application" {
count = 2
name = "app-${count.index}"
}
Terraform produces:
app-0
app-1
Again, a single configuration block expands into multiple independent resources.
Using count with Variables and Lists
Hardcoding numbers is useful for demonstrations, but production infrastructure usually determines resource counts dynamically.
Terraform allows count to evaluate expressions instead of fixed integers.
Example: Using a Variable
variable "server_count" {
default = 4
}
resource "aws_instance" "web" {
count = var.server_count
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
}
Changing only the variable updates the number of resources Terraform creates.
For example:
server_count = 8
Terraform automatically plans four additional instances.
Creating Resources from a List
A common pattern combines count with the length() function.
variable "filenames" {
default = [
"/tmp/file1.txt",
"/tmp/file2.txt",
"/tmp/file3.txt"
]
}
resource "local_file" "example" {
count = length(var.filenames)
filename = var.filenames[count.index]
content = "Generated by Terraform"
}
Terraform evaluates:
length(var.filenames)
to:
3
Then creates:
local_file.example[0]
local_file.example[1]
local_file.example[2]
Each resource uses the filename located at the corresponding list index.
Why This Pattern Is Popular
This approach allows infrastructure to scale automatically as the input list changes.
For example:
3 filenames
↓
3 resources
Later:
6 filenames
↓
6 resources
No resource block needs to be duplicated.
A Practical Engineering Tip
In production, avoid relying on list order when resources have long-lived identities.
While using:
var.instances[count.index]
is convenient, removing or reordering list elements can change resource indexes and trigger unexpected replacements.
We'll explore this behavior in detail later in this guide.
Creating Conditional Resources with count
One of the most common real-world uses of count is creating resources only when a condition evaluates to true.
Instead of maintaining separate Terraform configurations for different environments, you can enable or disable resources dynamically.
Creating an Optional Resource
A typical pattern looks like this:
resource "aws_s3_bucket" "logs" {
count = var.create_logs_bucket ? 1 : 0
bucket = "company-logs"
}
If:
create_logs_bucket = true
Terraform creates:
aws_s3_bucket.logs[0]
If:
create_logs_bucket = false
Terraform creates nothing.
Example Using a Boolean Variable
variable "enable_monitoring" {
default = true
}
resource "aws_cloudwatch_log_group" "application" {
count = var.enable_monitoring ? 1 : 0
name = "/application/logs"
}
This approach is common when enabling:
- Monitoring
- Logging
- Debug infrastructure
- Temporary resources
- Disaster recovery environments
- Development-only infrastructure
Environment-Based Deployments
You can also create resources only for specific environments.
resource "aws_instance" "bastion" {
count = var.environment == "production" ? 1 : 0
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
}
Production:
Creates one bastion host
Development:
Creates none
This keeps configurations reusable while avoiding unnecessary infrastructure costs.
Best Practices for Conditional Resources
When using conditional count, keep these recommendations in mind:
- Return only whole numbers.
- Use
1to create a single resource. - Use
0to skip resource creation. - Keep conditional expressions simple and readable.
- Prefer boolean variables over complex nested expressions.
- Review execution plans carefully when changing conditions from
1to0, since Terraform will destroy the existing resource.
Engineering Insight
Conditional resources are widely used in production modules to enable optional components such as monitoring agents, bastion hosts, NAT gateways, backup vaults, or diagnostic settings. Keeping these resources behind simple boolean flags makes modules more reusable while reducing configuration duplication.
Using count with Terraform Modules
The count meta argument isn't limited to resources—you can also use it with module blocks. This allows you to create multiple instances of the same reusable module without duplicating configuration.
This pattern is useful when you need several identical environments, networks, or application stacks that differ only by an index or a few input variables.
Example: Creating Multiple VPC Modules
Suppose you have a reusable module located in ./modules/network.
module "network" {
count = 2
source = "./modules/network"
environment = "dev-${count.index}"
}
Terraform expands the configuration into:
module.network[0]
module.network[1]
Each module instance has its own:
- Resources
- Variables
- Outputs
- State entries
Although the module source is identical, Terraform treats each instance as an independent deployment.
Referencing Module Outputs
Module outputs also become indexed.
For example, if the module exports:
output "vpc_id" {
value = aws_vpc.main.id
}
You can access individual outputs like this:
module.network[0].vpc_id
or
module.network[1].vpc_id
To retrieve every VPC ID:
output "vpc_ids" {
value = module.network[*].vpc_id
}
Terraform returns a list containing the output from every module instance.
When to Use count with Modules
Using count on modules works well when every deployment is nearly identical.
Typical examples include:
- Development environments
- Test environments
- Multiple regional deployments with similar configurations
- Temporary lab environments
- Training environments
If each module instance requires unique names, keys, or configuration values, for_each is usually the better choice.
Real-World AWS EC2 Examples
Provisioning multiple virtual machines is one of the most common use cases for count.
Creating Identical EC2 Instances
resource "aws_instance" "web" {
count = 3
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
Environment = "development"
}
}
Terraform creates:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
AWS displays the instances as:
web-0
web-1
web-2
This approach is ideal when every server shares the same:
- AMI
- Instance type
- Security groups
- IAM role
- User data
- Storage configuration
Creating Worker Nodes
Many organizations deploy identical worker nodes for background processing.
variable "worker_count" {
default = 5
}
resource "aws_instance" "worker" {
count = var.worker_count
ami = var.worker_ami
instance_type = "t3.small"
tags = {
Name = "worker-${count.index}"
Role = "queue-worker"
}
}
Increasing:
worker_count = 8
adds three new instances without modifying the resource definition.
Creating Development Environments
Another common pattern uses variables to scale environments.
variable "environment" {}
resource "aws_instance" "application" {
count = var.environment == "production" ? 6 : 2
ami = var.ami
instance_type = "t3.micro"
}
Development receives fewer servers while production automatically provisions more.
Engineering Perspective
In practice, count works best when every EC2 instance is interchangeable.
Examples include:
- Auto-scaled workers
- CI/CD runners
- Kubernetes worker nodes
- Build servers
- Temporary testing infrastructure
For long-lived application servers with unique identities, for_each generally provides a safer resource lifecycle.
Using count with Data Sources
Terraform also supports count on many data sources.
This allows multiple data lookups using the same configuration block.
Example
variable "amis" {
default = [
"ami-11111111",
"ami-22222222"
]
}
data "aws_ami" "selected" {
count = length(var.amis)
owners = ["self"]
filter {
name = "image-id"
values = [var.amis[count.index]]
}
}
Terraform creates:
data.aws_ami.selected[0]
data.aws_ami.selected[1]
Each lookup is evaluated independently.
When Is This Useful?
Examples include:
- Looking up multiple AMIs
- Querying multiple VPCs
- Reading multiple Route53 zones
- Fetching multiple IAM policies
- Looking up multiple subnets
Keep in mind that not every provider supports every pattern equally, so always consult the provider documentation.
Visualizing count with Outputs
Outputs provide an easy way to inspect resources created with count.
Suppose you have:
resource "local_file" "notes" {
count = 3
filename = "/tmp/file-${count.index}.txt"
content = "Terraform"
}
Create an output:
output "generated_files" {
value = local_file.notes[*].filename
}
After running:
terraform output
Terraform returns:
generated_files = [
"/tmp/file-0.txt",
"/tmp/file-1.txt",
"/tmp/file-2.txt"
]
The splat operator ([*]) collects the value from every resource instance into a single list.
Outputting Specific Instances
You can also reference individual resources.
output "first_file" {
value = local_file.notes[0].filename
}
Terraform returns only the first resource.
Understanding Resource Addressing
Every Terraform resource has a unique address.
Without count:
aws_instance.web
With:
count = 3
Terraform generates:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
These addresses are used throughout Terraform.
For example:
terraform state list
returns:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
The addresses also appear during:
terraform planterraform applyterraform destroyterraform importterraform state
Understanding resource addressing is essential when debugging or migrating infrastructure.
Resource Addresses Remain Stable Only While Indexes Remain Stable
Terraform does not identify resources using names or tags.
Instead, it uses the resource address.
For example:
aws_instance.web[1]
is considered a completely different resource from:
aws_instance.web[2]
Even if both instances have identical configuration.
This design explains why changing indexes often leads to replacements.
Terraform State Internals
Terraform stores every managed object inside its state file.
When using count, every instance receives its own entry.
Example:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
Terraform tracks information such as:
- Resource address
- Provider
- Resource ID
- Attributes
- Dependencies
- Lifecycle metadata
Whenever you run:
terraform plan
Terraform compares:
- Configuration
- Current state
- Real infrastructure
The plan determines whether each indexed resource should be:
- Created
- Updated
- Replaced
- Destroyed
Why State Matters
Many engineers assume Terraform identifies resources using tags or names.
It doesn't.
Instead, Terraform primarily relies on:
resource address
+
state
This distinction becomes important when indexes change.
The Biggest Pitfall: Index Shifting
The most important concept to understand about count is index shifting.
It is also the primary reason experienced Terraform users often prefer for_each for long-lived infrastructure.
Consider this list:
variable "servers" {
default = [
"frontend",
"backend",
"database"
]
}
Terraform creates:
Index Resource
0 frontend
1 backend
2 database
Internally:
aws_instance.server[0]
aws_instance.server[1]
aws_instance.server[2]
Now Remove the First Item
Suppose the list becomes:
variable "servers" {
default = [
"backend",
"database"
]
}
Terraform now evaluates:
Index Resource
0 backend
1 database
Notice what changed:
Before
0 -> frontend
1 -> backend
2 -> database
becomes
After
0 -> backend
1 -> database
From Terraform's perspective:
aws_instance.server[0]
is no longer frontend
it is now backend
Therefore Terraform plans something similar to:
Destroy frontend
Replace backend
Replace database
Even though only one item was removed.
Visualizing Index Shifting
Initial configuration:
Index
0 → app-a
1 → app-b
2 → app-c
Updated configuration:
Index
0 → app-b
1 → app-c
Terraform interprets this as:
app-a → destroy
app-b → replace
app-c → replace
The infrastructure itself may appear unchanged to a human, but Terraform sees different resource identities because the addresses changed.
Why This Happens
Terraform does not compare resources based on:
- Name
- Tag
- Filename
- Hostname
Instead it compares:
aws_instance.server[0]
aws_instance.server[1]
aws_instance.server[2]
Once those addresses change, Terraform assumes the resources themselves have changed.
Production Recommendation
I've seen production deployments where removing a single list element unexpectedly produced a plan replacing dozens of resources.
The configuration was technically valid—the problem was that every resource after the removed element received a new index. Terraform behaved exactly as designed, but the outcome surprised the engineering team because resource identity had shifted.
For infrastructure with persistent identities—such as databases, IAM users, DNS records, or long-lived application servers—prefer for_each with stable keys instead of count.
Common Errors and Troubleshooting
Although count is straightforward, a few mistakes appear frequently.
Error: Invalid Index
Example:
filename = var.files[count.index]
If:
count = 5
but:
var.files
contains only 3 items
Terraform reports an invalid index error.
Solution
Ensure the list length matches the value assigned to count.
Using:
count = length(var.files)
is generally the safest approach.
Error: Reference to count.index Outside a Counted Resource
This configuration is invalid:
locals {
filename = "file-${count.index}"
}
The count object only exists inside resources or modules that define the count meta argument.
Solution
Move the expression into the counted block or pass the value explicitly.
Error: Count Depends on an Unknown Value
Terraform must know the value of count during the planning phase.
This is invalid:
count = aws_instance.web.id
The instance ID doesn't exist until after the resource is created.
Solution
Use variables, locals, data sources, or expressions that Terraform can evaluate before planning.
Unexpected Resource Replacement
If Terraform unexpectedly plans to replace many resources, check for:
- Reordered lists
- Removed list elements
- Inserted items in the middle of a list
- Changes to values indexed by
count.index
These are common indicators of index shifting.
Debugging Tips
When troubleshooting counted resources:
- Run
terraform planbefore every apply. - Use
terraform state listto inspect resource addresses. - Compare state addresses with your configuration.
- Confirm that list ordering has not changed.
- Avoid manually editing the Terraform state unless absolutely necessary.
- Consider migrating to
for_eachif stable resource identities are required.
Understanding how Terraform evaluates count, assigns indexes, and tracks resource addresses makes diagnosing most count-related issues much easier.
count vs for_each: Which Should You Use?
Both count and for_each allow Terraform to create multiple instances from a single configuration block, but they solve different problems.
Understanding resource identity is the key to choosing the right one.
Use count when the resources are almost identical and only differ by a numeric index. Use for_each when every resource has its own unique identity that should remain stable over time.
When to Use count
The count meta argument is the better choice when:
- You're creating a fixed number of identical resources.
- Resources differ only by
count.index. - Resource identity isn't important.
- Infrastructure is temporary or easily recreated.
- You want the simplest possible configuration.
Examples include:
- Test virtual machines
- CI/CD runners
- Worker nodes
- Temporary development infrastructure
- Demo environments
- Batch processing servers
Example:
resource "aws_instance" "worker" {
count = 5
ami = var.ami
instance_type = "t3.micro"
tags = {
Name = "worker-${count.index}"
}
}
When to Use for_each
Use for_each when each resource has its own stable identifier.
Examples include:
- IAM users
- DNS records
- S3 buckets
- Databases
- Kubernetes namespaces
- Long-lived EC2 instances
- Security groups with unique names
Example:
resource "aws_iam_user" "users" {
for_each = toset([
"alice",
"bob",
"charlie"
])
name = each.key
}
Terraform tracks:
aws_iam_user.users["alice"]
aws_iam_user.users["bob"]
aws_iam_user.users["charlie"]
Notice that the addresses are based on keys rather than numeric indexes.
Removing "bob" doesn't affect "alice" or "charlie".
Resource Identity
The biggest difference between the two approaches is how Terraform identifies resources.
With count:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
With for_each:
aws_instance.web["frontend"]
aws_instance.web["backend"]
aws_instance.web["database"]
Indexes are positional.
Keys are stable.
That's why for_each avoids most index-shifting problems.
Practical Decision Guide
Choose count when:
- Resources are interchangeable.
- You're scaling identical infrastructure.
- The number of instances is all that matters.
- Resources can safely be recreated.
Choose for_each when:
- Resources have unique names.
- Order may change.
- Infrastructure is stateful.
- Individual resources evolve independently.
- Long-term stability is important.
Engineering Insight
In many production codebases,
countis primarily used for optional resources (0or1) or homogeneous fleets of infrastructure. For anything with a business identity—such as users, databases, DNS records, or application environments—for_eachis generally the preferred pattern because it minimizes unexpected replacements.
Migrating from count to for_each
It's common for Terraform projects to begin with count and later migrate to for_each as infrastructure becomes more complex.
However, this migration requires careful planning because Terraform treats the new resource addresses as entirely different objects.
For example, imagine the original configuration:
resource "aws_instance" "web" {
count = 3
}
Terraform tracks:
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
You later refactor it to:
resource "aws_instance" "web" {
for_each = {
frontend = {}
backend = {}
worker = {}
}
}
Terraform now expects:
aws_instance.web["frontend"]
aws_instance.web["backend"]
aws_instance.web["worker"]
From Terraform's perspective, every resource address has changed.
Without additional migration steps, the plan will usually destroy the old resources and create new ones.
Migration Strategy
A safe migration generally follows these steps:
- Back up the Terraform state.
- Review the existing resource addresses.
- Introduce the new
for_eachconfiguration. - Map old addresses to new addresses.
- Validate the migration with
terraform plan. - Apply only after confirming no unexpected replacements.
Never assume Terraform automatically understands that:
aws_instance.web[0]
and
aws_instance.web["frontend"]
refer to the same infrastructure.
Using moved Blocks
Modern Terraform versions support moved blocks, which allow you to tell Terraform that a resource has moved from one address to another.
Instead of destroying and recreating infrastructure, Terraform updates the state to reflect the new address.
Example:
moved {
from = aws_instance.web[0]
to = aws_instance.web["frontend"]
}
You can create additional mappings as needed:
moved {
from = aws_instance.web[1]
to = aws_instance.web["backend"]
}
moved {
from = aws_instance.web[2]
to = aws_instance.web["worker"]
}
When Terraform processes these blocks, it updates the state so the existing resources are preserved under their new addresses.
Best Practice
Use
movedblocks whenever you're refactoring resource addresses in version-controlled Terraform configurations. They provide a declarative migration history that's easier to review and maintain than ad hoc state manipulation.
When Might terraform state mv Still Be Useful?
The terraform state mv command can also move resources within the state.
However, it's generally intended for one-time administrative operations.
For long-term maintainability:
- Prefer
movedblocks for code-based refactoring. - Reserve
terraform state mvfor exceptional operational tasks or legacy migrations.
Interaction with Other Meta Arguments
The count meta argument works alongside several other Terraform meta arguments.
Understanding how they interact helps avoid unexpected behavior.
count with depends_on
Terraform automatically builds most dependencies by analyzing resource references.
Occasionally, you need to define an explicit dependency.
Example:
resource "aws_instance" "web" {
count = 3
depends_on = [
aws_security_group.web
]
ami = var.ami
instance_type = "t3.micro"
}
Every EC2 instance waits until the security group has been created.
count with lifecycle
Lifecycle rules apply independently to every counted resource.
Example:
resource "aws_instance" "web" {
count = 2
lifecycle {
create_before_destroy = true
}
}
Terraform evaluates the lifecycle settings separately for:
aws_instance.web[0]
aws_instance.web[1]
This is particularly useful during rolling updates or infrastructure replacements.
count with Provider Configurations
You can combine count with provider aliases.
Example:
resource "aws_instance" "web" {
count = 2
provider = aws.us_east_1
ami = var.ami
instance_type = "t3.micro"
}
Every counted resource uses the specified provider configuration.
If deploying to multiple regions, many teams prefer separate modules with provider aliases rather than relying solely on indexed resources.
count with Modules
Remember that module instances behave exactly like resources.
Each module receives its own:
- State entry
- Resource graph
- Outputs
- Dependencies
This consistency makes Terraform's behavior predictable across different configuration types.
Performance Considerations
The count meta argument itself introduces very little overhead.
Performance depends primarily on the number of resources Terraform must evaluate.
For example:
count = 5
creates a negligible planning cost.
However:
count = 500
or
count = 5000
means Terraform must:
- Expand every instance.
- Build a dependency graph.
- Compare each resource with the current state.
- Query providers.
- Generate a plan for every object.
As infrastructure grows, planning and apply operations naturally take longer.
Tips for Large Deployments
For large-scale environments:
- Break infrastructure into reusable modules.
- Separate unrelated workloads into different state files.
- Minimize unnecessary resource recreation.
- Review plans before every apply.
- Avoid very large monolithic configurations.
Scalability is usually influenced more by overall Terraform architecture than by the choice between count and for_each.
Production Best Practices
After working with Terraform in production, several patterns consistently lead to more predictable infrastructure.
Use count Only for Homogeneous Resources
Good examples include:
- Worker nodes
- Test instances
- Temporary infrastructure
- Demo environments
- Build agents
These resources are typically interchangeable, making indexed identities acceptable.
Prefer for_each for Stable Resources
Avoid count for:
- Databases
- DNS records
- IAM users
- Production application servers
- Persistent storage
- Long-lived Kubernetes objects
Stable keys provide a much safer lifecycle.
Keep Conditional Logic Simple
Instead of deeply nested expressions, use readable boolean conditions.
Good example:
count = var.enable_logging ? 1 : 0
Simple expressions are easier to understand, review, and troubleshoot.
Avoid Depending on List Order
Lists change.
Indexes shift.
Resource identities change.
If the order of elements might evolve, use for_each with unique keys instead.
Always Review terraform plan
Before every production deployment:
- Review creations.
- Review updates.
- Review replacements.
- Investigate unexpected destroys.
Never assume a small configuration change results in a small infrastructure change.
Use Meaningful Resource Names
Even when using count.index, include descriptive prefixes.
Instead of:
server-0
prefer:
web-server-0
Clear naming improves debugging and operational visibility.
Document Design Decisions
If you intentionally choose count over for_each, document the reason.
Future maintainers will better understand the assumptions behind the configuration.
Related Terraform Concepts
To deepen your understanding of Terraform, explore these related topics:
- Terraform Meta Arguments
for_eachMeta Argument- Terraform Variables
- Local Values
- Conditional Expressions
- Terraform Functions
- Resource Dependencies
- Terraform Modules
- Terraform Outputs
- Terraform State
- Terraform Import
movedBlocks- Provider Aliases
- Workspaces
- Dynamic Blocks
- Lifecycle Meta Argument
These concepts frequently work together in production Infrastructure as Code projects.
Frequently Asked Questions (FAQs)
What is the count meta argument in Terraform?
The count meta argument tells Terraform how many instances of a resource or module to create from a single configuration block. Each instance receives a numeric index beginning at 0.
What does count.index do?
count.index returns the index of the current resource instance. It's commonly used to generate unique names, tags, file paths, and other values for each instance.
Can count be used with modules?
Yes. Terraform supports count on module blocks, allowing multiple instances of the same module to be created from a single configuration.
Can I use variables with count?
Absolutely. count accepts expressions, making it common to use variables, local values, conditional expressions, and functions such as length().
Example:
count = length(var.instances)
Why are my resources being recreated?
The most common reason is index shifting.
If you remove or reorder items in a list, Terraform assigns new indexes to the remaining resources. Since Terraform identifies counted resources by index, it interprets them as different objects and may plan replacements.
Can count create zero resources?
Yes.
Setting:
count = 0
prevents Terraform from creating any instances.
This is widely used for optional resources.
Can count be negative?
No.
Terraform requires count to evaluate to a non-negative whole number.
Can I reference individual instances?
Yes.
Example:
aws_instance.web[0]
references the first resource instance.
Is count evaluated during planning?
Yes.
Terraform must know the value of count during the planning phase. Expressions that depend on unknown values at plan time will produce errors.
Is count deprecated?
No.
count remains a core Terraform language feature and continues to be fully supported. However, HashiCorp recommends choosing between count and for_each based on the resource identity requirements of your infrastructure.
Conclusion
The Terraform count meta argument is one of the foundational building blocks for writing concise, maintainable Infrastructure as Code. By allowing multiple resource or module instances to be created from a single configuration block, it eliminates repetitive code and simplifies deployments involving homogeneous infrastructure.
Its simplicity, however, comes with an important trade-off. Because Terraform identifies counted resources by numeric index, changes to list ordering or resource counts can lead to index shifting and unexpected replacements. Understanding how Terraform assigns resource addresses and tracks state is therefore just as important as understanding the syntax itself.
In practice:
- Use
countfor interchangeable resources such as worker nodes, development environments, or optional infrastructure controlled by boolean conditions. - Use
for_eachwhen resources require stable identities, unique keys, or independent lifecycles. - Review every execution plan before applying changes, especially when modifying lists or refactoring existing infrastructure.
- When migrating from
counttofor_each, usemovedblocks to preserve resource identity and minimize unnecessary recreation.
Choosing the appropriate meta argument isn't simply a matter of syntax—it's a design decision that affects the long-term stability, maintainability, and predictability of your Terraform deployments. Mastering both count and for_each will help you build infrastructure that scales cleanly while avoiding some of the most common pitfalls encountered in real-world Infrastructure as Code projects.
8 free, 100% client-side tools for developers — no signup, no data uploads.
Explore all tools