Terraform Count Meta Argument: Examples, Best Practices, and Common Pitfalls

Published: 2025-08-17
27 min read
Share:

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:

  • count creates multiple instances of the same resource or module from one configuration block.
  • Every instance receives a numeric index through count.index, starting at 0.
  • Use count when resources are nearly identical and differ only by an index or a few computed values.
  • Avoid count for long-lived resources whose identity must remain stable.
  • Changing list order or removing elements can shift indexes, causing Terraform to recreate resources.
  • Use for_each instead of count when resources have unique names, IDs, or other stable identifiers.
  • Always review terraform plan before applying changes to resources managed with count.

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 ami or instance_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.

  1. Read the resource block.
  2. Evaluate the expression assigned to count.
  3. Determine how many instances are required.
  4. Create indexed resource instances.
  5. 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.


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 1 to create a single resource.
  • Use 0 to skip resource creation.
  • Keep conditional expressions simple and readable.
  • Prefer boolean variables over complex nested expressions.
  • Review execution plans carefully when changing conditions from 1 to 0, 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 plan
  • terraform apply
  • terraform destroy
  • terraform import
  • terraform 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 plan before every apply.
  • Use terraform state list to 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_each if 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, count is primarily used for optional resources (0 or 1) or homogeneous fleets of infrastructure. For anything with a business identity—such as users, databases, DNS records, or application environments—for_each is 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:

  1. Back up the Terraform state.
  2. Review the existing resource addresses.
  3. Introduce the new for_each configuration.
  4. Map old addresses to new addresses.
  5. Validate the migration with terraform plan.
  6. 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 moved blocks 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 moved blocks for code-based refactoring.
  • Reserve terraform state mv for 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_each Meta Argument
  • Terraform Variables
  • Local Values
  • Conditional Expressions
  • Terraform Functions
  • Resource Dependencies
  • Terraform Modules
  • Terraform Outputs
  • Terraform State
  • Terraform Import
  • moved Blocks
  • 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 count for interchangeable resources such as worker nodes, development environments, or optional infrastructure controlled by boolean conditions.
  • Use for_each when 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 count to for_each, use moved blocks 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.

Free Engineering ToolsNEW

8 free, 100% client-side tools for developers — no signup, no data uploads.

Explore all tools