Terraform Count
Learn how Terraform count works, how count.index is used, how to create conditional resources, how to reference counted resources, and when count is a good fit.
What Is the count Meta-Argument?
The count meta-argument lets Terraform create multiple instances from a single resource block. Instead of copy-pasting the same EC2 instance, subnet, or IAM user block several times, you define one block and tell Terraform how many copies to make.
At its simplest, count looks like this:
resource "aws_instance" "web" {
count = 3
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
Terraform expands that one block into three instances:
aws_instance.web[0]aws_instance.web[1]aws_instance.web[2]
That indexing behavior is the foundation of count. Every instance is identified by a numeric position starting at zero.
count is called a meta-argument because it is not specific to one resource type. Many resources and modules can use it. It changes how Terraform interprets the block rather than configuring the cloud provider object itself.
Why count Is Useful
Without count, repeating resources means repeating code. That creates several problems:
- more typing
- more chances for inconsistency
- harder updates later
- poor scalability as infrastructure grows
With count, you keep the declaration compact and let Terraform handle repetition.
A simple example is creating multiple nearly identical instances:
variable "instance_count" {
type = number
default = 2
}
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index + 1}"
}
}
Now the number of instances can be changed by adjusting a variable instead of rewriting resource blocks.
How count Works Internally
When Terraform sees count, it evaluates the expression to a whole number. If the result is 0, no instances are created. If the result is 1, exactly one instance exists. If the result is 5, Terraform creates five instances and assigns them indexes from 0 to 4.
That means a counted resource is no longer referenced like a single object. Even if count = 1, Terraform still treats it as a collection of instances. You must reference it with an index.
For example:
resource "aws_instance" "web" {
count = 1
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
The correct reference is:
aws_instance.web[0].id
Not:
aws_instance.web.id
That is an important beginner detail.
Using count.index
Inside a block that uses count, Terraform gives you access to a special value called count.index.
count.index is the zero-based number of the current instance being created.
Example:
resource "aws_instance" "web" {
count = 3
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
This produces names like:
web-0web-1web-2
If you want human-friendly numbering, add one:
Name = "web-${count.index + 1}"
That produces:
web-1web-2web-3
Common uses of count.index
- naming repeated resources
- selecting a value from a list
- aligning related resources by position
- generating per-instance tags or labels
The key idea is that count.index gives each repeated instance a way to know which copy it is.
Using count with Lists
One of the most common beginner patterns is combining count with a list variable.
Imagine you want to create one subnet per CIDR block:
variable "subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
Here is what happens:
length(var.subnet_cidrs)decides how many subnets to createcount.indexselects the matching CIDR block from the list- the same index is used to select an availability zone
This pattern is powerful because it lets you drive repeated infrastructure from structured inputs.
Why list alignment matters
When you use count with multiple lists, those lists must stay aligned by position. If the second subnet CIDR belongs in the second availability zone, your lists must stay in sync.
That can work well for small and stable configurations, but it becomes fragile if items are inserted, reordered, or removed later. This is one reason for_each is often preferred for named objects.
Creating Multiple Resources from a Variable
A common beginner pattern is to expose the number of resources through an input variable:
variable "instance_count" {
description = "Number of application instances"
type = number
default = 2
}
resource "aws_instance" "app" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "app-${count.index + 1}"
}
}
This is useful when different environments need different sizes:
- dev may use
1 - staging may use
2 - production may use
4
The same Terraform code can handle all of them.
Conditional Resource Creation with count
Another very common use of count is conditional creation. Because count can be 0 or 1, it becomes a simple feature toggle.
Example:
variable "create_instance" {
type = bool
default = true
}
resource "aws_instance" "web" {
count = var.create_instance ? 1 : 0
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
If var.create_instance is true, Terraform creates one instance. If it is false, Terraform creates none.
This is especially useful for:
- optional bastion hosts
- optional monitoring resources
- environment-specific infrastructure
- learning examples where a feature can be toggled on and off
Why the ternary expression works well here
Terraform needs count to evaluate to a number. A boolean alone is not enough. The ternary expression converts the condition into either 1 or 0.
count = var.create_instance ? 1 : 0
That small pattern appears in many real-world Terraform codebases.
Referencing Counted Resources
Once count is used, the resource becomes a list-like collection of instances. You must reference specific elements by index or use splat syntax for all instances.
Referencing a single counted resource
aws_instance.web[0].id
This is the first instance.
Referencing all instance IDs
aws_instance.web[*].id
This returns a list of IDs from every counted instance.
Example output
output "web_instance_ids" {
value = aws_instance.web[*].id
}
If count = 3, the output is a list containing three instance IDs.
Referencing a conditionally created resource
Conditional resources need extra care. If count = 0, there is no [0] element.
That means this is only safe when the resource exists:
aws_instance.web[0].id
In real modules, you often protect optional references using expressions, conditionals, or outputs that return null when nothing exists.
For example:
output "web_instance_id" {
value = var.create_instance ? aws_instance.web[0].id : null
}
This avoids invalid index errors.
count with Modules
count is not limited to resources. You can also use it on modules.
module "app_server" {
count = 2
source = "./modules/ec2-instance"
instance_name = "app-${count.index + 1}"
}
That tells Terraform to create two module instances:
module.app_server[0]module.app_server[1]
This can be useful, but the same indexing tradeoffs still apply. If stable identities matter, for_each is usually a better long-term fit.
The Big Problem with count: List Ordering
count is simple, but it has a major downside: instance identity is tied to position, not meaning.
Suppose you create resources from this list:
variable "names" {
default = ["app", "api", "worker"]
}
resource "aws_instance" "service" {
count = length(var.names)
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = var.names[count.index]
}
}
Terraform maps them like this:
[0]->app[1]->api[2]->worker
Now imagine you remove the middle item and change the list to:
["app", "worker"]
Terraform now sees:
[0]->app[1]->worker
The old [1] used to be api, but now [1] means worker. Terraform may destroy and recreate resources after the removed element because the indexes shifted.
This is the classic count-ordering problem.
Why this matters operationally
If a list represents real infrastructure identities, removing one item from the middle can cause unnecessary replacement of later items. That can mean:
- downtime
- changed resource names
- changed IDs
- unnecessary churn in plans
That is why count works best when order is stable or identity does not matter very much.
When count Works Well
count is a strong choice when:
- you need a simple number of identical resources
- instances are naturally index-based
- order is stable and not expected to change much
- you want to enable or disable one resource with
0or1 - the repeated resources are disposable or low-risk
Examples:
- create 3 identical test instances
- create one optional NAT gateway in a learning environment
- create one subnet per fixed CIDR list in a small example
- create N EBS volumes for a lab setup
count shines when the repetition problem is fundamentally numeric.
When for_each Is Better
for_each is usually a better choice when:
- each instance has a meaningful name or key
- the collection may grow or shrink over time
- order should not determine identity
- you want stable references like
resource["prod"] - each item has its own settings
If you are creating resources for dev, staging, and prod, or for named buckets, teams, or applications, for_each usually models that better than numeric indexes.
Practical Comparison: count vs for_each
| Question | count | for_each |
|---|---|---|
| How are instances identified? | By numeric index | By map key or set member |
| Best for | Repeating N similar items | Named or distinct items |
| Sensitive to reordering? | Yes | Much less |
| Simple conditional creation? | Very easy with 0 or 1 | Possible, but less direct |
| Readability of references | Lower for large collections | Higher when keys are meaningful |
A simple rule of thumb is:
- use
countwhen the answer is a number - use
for_eachwhen the answer is a collection of named things
Best Practices for Using count
1. Keep list ordering stable
If your design depends on list position, document that clearly and avoid unnecessary reordering.
2. Use count.index only where it adds value
It is helpful for selecting list elements or building names, but if you are forcing everything through numeric indexes, that may be a sign that for_each is a better model.
3. Be careful with optional resources
Whenever count can become 0, protect downstream references so Terraform does not try to access [0] when nothing exists.
4. Prefer meaningful outputs
If your configuration creates multiple instances, output lists clearly:
output "instance_ids" {
value = aws_instance.app[*].id
}
5. Do not fight the identity model
If order changes often, do not keep patching around count. Switch to for_each instead.
Common Beginner Mistakes
Mistake 1: Forgetting indexes when referencing counted resources
Once count is present, references need [index] or [*] syntax.
Mistake 2: Assuming count.index starts at 1
It starts at 0. Add one only for display-friendly names.
Mistake 3: Using count for named business objects
If items are naturally identified by names like prod, blue, or payments, for_each is usually the better design.
Mistake 4: Ignoring reordering risk
The biggest conceptual trap with count is believing indexes are stable identities. They are not. They are just positions.
Final Thoughts
Terraform count is one of the easiest ways to reduce repetition. It helps you create multiple similar resources, work through lists with count.index, and toggle resources on or off with simple conditionals.
Its biggest strength is also its biggest limitation: it is based on numeric positions. That makes it great for straightforward repetition, but weaker for collections where each item has its own durable identity.
If you remember these three points, you will use count well:
countcreates N instances from one blockcount.indexgives you the current zero-based index- list reordering can cause unwanted resource replacement
Use it when the problem is numeric. Reach for for_each when the problem is really about named items.
Knowledge Check
Question 1: count.index
What does count.index represent inside a resource that uses count?
Question 2: Conditional Resources
Which expression is a common Terraform pattern for creating a resource only when var.create_instance is true?
Question 3: Ordering Risk
Why can removing a middle element from a list used with count cause unexpected resource replacement?