count
By default a Terraform resource block manages a single real-world object. The count meta-argument changes that: it tells Terraform to create and manage N near-identical instances from one block. This is the simplest way to scale a resource up or down, and—because count accepts any whole number, including zero—it doubles as a clean idiom for conditionally creating infrastructure. count works identically in both Terraform 1.5+ and OpenTofu.
Creating multiple instances
Set count to a positive integer and Terraform produces that many instances of the resource. Each instance is distinguished by its position, and you reference the current position inside the block with count.index, a zero-based integer.
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
This declares three EC2 instances tagged web-0, web-1, and web-2.
Output:
Terraform will perform the following actions:
# aws_instance.web[0] will be created
# aws_instance.web[1] will be created
# aws_instance.web[2] will be created
Plan: 3 to add, 0 to change, 0 to destroy.
The list address: type.name[index]
When a resource uses count, its address becomes a list of instances. You access an individual instance by integer subscript, and the whole resource as a list (for example with the splat operator [*]).
# A single instance's private IP
output "first_ip" {
value = aws_instance.web[0].private_ip
}
# Every instance's private IP, as a list
output "all_ips" {
value = aws_instance.web[*].private_ip
}
You can also drive count from a list and index into that list with count.index, keeping per-instance values aligned:
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" "private" {
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 = "private-${count.index}"
}
}
Because instances are identified by position, removing an item from the middle of a count-driven list shifts every later index down by one. Terraform then plans to destroy and recreate the trailing instances even though they did not really change. For collections keyed by a stable identifier, prefer
for_each.
Conditional creation
A common pattern uses the ternary operator to turn a resource on or off. When count = 0 the resource is created zero times—effectively absent from the plan—and when count = 1 exactly one instance exists.
variable "enable_monitoring" {
type = bool
default = false
}
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
count = var.enable_monitoring ? 1 : 0
alarm_name = "high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 80
}
Note that even a conditional resource is still a list. To reference it elsewhere you must use index [0], and you should guard against the disabled case:
output "alarm_arn" {
value = var.enable_monitoring ? aws_cloudwatch_metric_alarm.cpu_high[0].arn : null
}
count vs for_each
Both meta-arguments create multiple instances, but they identify those instances differently. count keys by integer position; for_each keys by a map key or set member. That single difference drives the trade-offs below.
| Aspect | count | for_each |
|---|---|---|
| Accepts | A whole number | A map or set of strings |
| Instance key | Integer index ([0], [1]) | Stable string key (["us-east-1a"]) |
| Effect of reordering input | Recreates shifted instances | No effect; keys are stable |
| Best for | Identical copies, on/off toggles | Distinct objects with meaningful names |
| Reference syntax | aws_x.y[0], aws_x.y[*] | aws_x.y["key"], values(aws_x.y) |
Reach for count when the instances are truly interchangeable (a fixed pool of identical workers) or when you need the condition ? 1 : 0 toggle. Reach for for_each whenever each instance has a meaningful, stable identity—because removing or adding one item won’t churn the rest of the fleet.
You cannot use
countandfor_eachon the same resource block. Choose one per resource.
Best Practices
- Use
countfor genuinely identical instances and thecondition ? 1 : 0enable/disable idiom; reach forfor_eachwhen each instance has a stable identity. - Drive
countfromlength(...)of an ordered list rather than a hard-coded number so scaling is data-driven. - Avoid removing items from the middle of a count-backed list—append or use
for_eachto prevent needless recreation. - Always index conditional resources as
[0]and guard references with a ternary so plans don’t fail when the resource is disabled. - Expose collections with the splat operator (
resource[*].attr) for clean, list-valued outputs. - Keep per-instance data structures (CIDRs, AZs, names) in aligned lists indexed by
count.indexto avoid drift between values.