Terraform Count and For Each Creating Multiple Resources
Writing one resource block per server works fine when you need two servers. It does not work when you need twenty. Terraform provides two meta-arguments — count and for_each — that let a single resource block create multiple resources. This topic explains both, when to use each one, and how to reference specific instances.
The Problem: Repetitive Resource Blocks
Without count or for_each, creating three identical web servers requires three separate resource blocks:
resource "aws_instance" "web_1" { ... }
resource "aws_instance" "web_2" { ... }
resource "aws_instance" "web_3" { ... }
This does not scale. It also creates three separately named resources that are hard to manage together. Count and for_each turn this into a single, manageable block.
Using count
The count meta-argument tells Terraform how many copies of a resource to create. Terraform creates them all from the same block.
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server-${count.index}"
}
}
This creates three EC2 instances. Inside the block, count.index gives the current iteration number starting from 0: 0, 1, 2.
Referencing count Instances
# All instances as a list aws_instance.web[*].public_ip # A specific instance (index 0) aws_instance.web[0].id # Second instance aws_instance.web[1].id
Using count with a Variable
variable "server_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.server_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
Change server_count from 3 to 5, run apply, and Terraform adds two more instances. Reduce it to 1, and Terraform destroys two.
The Problem with count: Index-Based Identity
Count identifies resources by index (0, 1, 2). This causes a painful problem when you remove an item from the middle of a list.
Diagram: The Index Shift Problem
Before removal: web[0] = server-A web[1] = server-B <-- you delete this web[2] = server-C After removal: web[0] = server-A (unchanged) web[1] = server-C (shifted from index 2) Terraform sees: web[1] changed (server-B → server-C) → DESTROY and RE-CREATE server-C! web[2] removed → DESTROY this index
Removing server-B causes server-C to be destroyed and recreated unnecessarily — because its index changed. This is where for_each excels.
Using for_each
The for_each meta-argument creates one resource instance per item in a map or set. Each instance is identified by a unique key — not by a number. This makes it immune to the index shift problem.
variable "servers" {
type = map(string)
default = {
web = "t3.micro"
api = "t3.small"
worker = "t3.medium"
}
}
resource "aws_instance" "app" {
for_each = var.servers
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value
tags = {
Name = each.key
}
}
Inside the block, each.key is the map key ("web", "api", "worker") and each.value is the map value ("t3.micro", etc.).
Referencing for_each Instances
# A specific instance by key aws_instance.app["web"].public_ip aws_instance.app["api"].id # All instances as a map aws_instance.app
for_each with a Set of Strings
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.key
}
The toset() function converts a list to a set, removing duplicates and making it suitable for for_each.
count vs for_each — When to Use Which
| Situation | Use |
|---|---|
| Creating N identical resources (same config, just more) | count |
| Creating resources with unique names, sizes, or configs | for_each |
| The list of items might change (items could be removed) | for_each |
| Resources are truly interchangeable (load balancer backends) | count |
When in doubt, prefer for_each. It is safer and more explicit about which instance is which.
Key Points
countcreates multiple resource instances identified by index (0, 1, 2…); usecount.indexinside the block.- Removing a middle item from a count list shifts indexes and can cause unexpected destroy-and-recreate operations.
for_eachcreates instances identified by a unique string key; useeach.keyandeach.valueinside the block.- for_each is immune to the index shift problem because keys are stable even when items are removed.
- Use
toset()to convert a list of strings to a set before passing it tofor_each.
