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.
count Is UsefulWithout count, repeating resources means repeating code. That creates several problems:
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.
count Works InternallyWhen 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].idNot:
aws_instance.web.idThat is an important beginner detail.
count.indexInside 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-2If you want human-friendly numbering, add one:
Name = "web-${count.index + 1}"That produces:
web-1web-2web-3count.indexThe key idea is that count.index gives each repeated instance a way to know which copy it is.
count with ListsOne 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.index selects the matching CIDR block from the listThis pattern is powerful because it lets you drive repeated infrastructure from structured inputs.
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.
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:
124The same Terraform code can handle all of them.
countAnother 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:
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 : 0That small pattern appears in many real-world Terraform codebases.
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.
aws_instance.web[0].idThis is the first instance.
aws_instance.web[*].idThis returns a list of IDs from every counted instance.
output "web_instance_ids" {
value = aws_instance.web[*].id
}If count = 3, the output is a list containing three instance IDs.
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].idIn 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 Modulescount 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.
count: List Orderingcount 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] -> workerNow imagine you remove the middle item and change the list to:
["app", "worker"]Terraform now sees:
[0] -> app[1] -> workerThe 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.
If a list represents real infrastructure identities, removing one item from the middle can cause unnecessary replacement of later items. That can mean:
That is why count works best when order is stable or identity does not matter very much.
count Works Wellcount is a strong choice when:
0 or 1Examples:
count shines when the repetition problem is fundamentally numeric.
for_each Is Betterfor_each is usually a better choice when:
resource["prod"]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.
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:
count when the answer is a numberfor_each when the answer is a collection of named thingscountIf your design depends on list position, document that clearly and avoid unnecessary reordering.
count.index only where it adds valueIt 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.
Whenever count can become 0, protect downstream references so Terraform does not try to access [0] when nothing exists.
If your configuration creates multiple instances, output lists clearly:
output "instance_ids" {
value = aws_instance.app[*].id
}If order changes often, do not keep patching around count. Switch to for_each instead.
Once count is present, references need [index] or [*] syntax.
count.index starts at 1It starts at 0. Add one only for display-friendly names.
count for named business objectsIf items are naturally identified by names like prod, blue, or payments, for_each is usually the better design.
The biggest conceptual trap with count is believing indexes are stable identities. They are not. They are just positions.
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:
count creates N instances from one blockcount.index gives you the current zero-based indexUse it when the problem is numeric. Reach for for_each when the problem is really about named items.
<MCQExercise exercise={{ id: 'tf-count-q1', title: 'Question 1: count.index', description: 'What does count.index represent inside a resource that uses count?', options: [ { id: 'a', text: 'The current workspace name' }, { id: 'b', text: 'The zero-based index of the current resource instance' }, { id: 'c', text: 'The total number of resources in the state file' }, { id: 'd', text: 'The index of the provider block being used' }, ], correctId: 'b', explanation: 'count.index is a special Terraform value available inside counted blocks. It represents the zero-based position of the current instance.', wrongExplanations: { a: 'Workspace names are available through terraform.workspace, not count.index.', c: 'count.index refers to the current counted instance, not all resources in state.', d: 'Providers are not indexed using count.index in this way.', }, }} />
<MCQExercise exercise={{ id: 'tf-count-q2', title: 'Question 2: Conditional Resources', description: 'Which expression is a common Terraform pattern for creating a resource only when var.create_instance is true?', options: [ { id: 'a', text: 'count = var.create_instance' }, { id: 'b', text: 'count = length(var.create_instance)' }, { id: 'c', text: 'count = var.create_instance ? 1 : 0' }, { id: 'd', text: 'count = true ? var.create_instance : 1' }, ], correctId: 'c', explanation: 'Terraform count expects a number, so the common pattern is to convert a boolean into 1 or 0 with a ternary expression.', wrongExplanations: { a: 'A boolean alone is not the right numeric value for count in this pattern.', b: 'length() does not apply meaningfully to a boolean value.', d: 'This expression does not produce the intended 1-or-0 behavior for conditional creation.', }, }} />
<MCQExercise exercise={{ id: 'tf-count-q3', title: 'Question 3: Ordering Risk', description: 'Why can removing a middle element from a list used with count cause unexpected resource replacement?', options: [ { id: 'a', text: 'Because Terraform identifies counted resources by numeric position, so later indexes shift' }, { id: 'b', text: 'Because count automatically converts all lists into sets' }, { id: 'c', text: 'Because resources created with count are never stored in state' }, { id: 'd', text: 'Because count.index is recalculated from resource IDs, not list order' }, ], correctId: 'a', explanation: 'Counted instances are identified by index. When a middle item is removed, later positions shift, so Terraform may think later resources have changed identity.', wrongExplanations: { b: 'count does not convert lists into sets; ordering remains important.', c: 'Counted resources are absolutely stored in state just like other managed resources.', d: 'count.index follows list position, which is exactly why reordering causes churn.', }, }} />
Because count identifies instances by position, not by value. Remove the second item from a list of four and everything after it shifts down an index, so Terraform sees [1] changing from one value to another, [2] changing, and [3] disappearing, and plans to destroy and recreate resources you never touched. On stateless resources this is merely wasteful; on anything with data attached it is an outage. This is the single strongest argument for for_each.
Use count for a genuine quantity of identical things, three identical workers where the index carries no meaning, and for conditional creation with count = var.enabled ? 1 : 0, which remains the idiomatic pattern. Use for_each whenever the instances are distinguishable, because it keys them by a stable string so adding or removing one affects only that one. If the list you are counting over could ever have an element removed from the middle, use for_each.
count = var.create_bucket ? 1 : 0. Zero instances means the resource is not created. Reference it as aws_s3_bucket.this[0], and note that any reference must handle the zero case or the plan errors when the resource does not exist, which is where one() or a try() wrapper helps. for_each over a conditionally empty map achieves the same thing and avoids the index-based referencing.
By index, aws_instance.web[0].id, or as a whole list with the splat expression aws_instance.web[*].id, which returns every instance's ID and is what you usually want for passing into a target group or a security group rule. Splat works cleanly even when count is zero, returning an empty list, which makes it safer than indexing directly in conditional cases.
Yes, count works on module blocks exactly as it does on resources, producing module.name[0]. The same positional-identity caveat applies and matters more, because recreating a module instance recreates every resource inside it. For anything beyond a simple conditional enable, for_each on the module block is the safer choice.
Learn how Terraform for_each works with sets, maps and objects, how each.key and each.value are used, how to reference instances and when it beats count.
Learn Terraform expressions: types, references, conditionals, for expressions, key function categories, try and can, and testing in terraform console.
Work with strings in Terraform: interpolation, format, case and whitespace helpers, replace and regex functions, templatefile and conditionals.