count & for_each
By the end of this lesson you will be able to create several copies of a resource from one block, choose correctly between count and for_each, and avoid the "index shift" trap that makes Terraform destroy the wrong things. You will see both behaviours in real plans using local files.
Why repeat a block?
Suppose Aarav's company opens offices in three cities and each needs its own config file. You could write three resource blocks, but then four cities means editing code again, and copy-paste mistakes creep in.
Terraform has two meta-arguments (arguments that work on any resource type) for making many copies: count and for_each. Think of a printing shop in Lucknow. count is "print 3 copies"; the copies are only numbered 1, 2, 3. for_each is "print one copy for each name on this list"; every copy carries a name.
Step 1: Use count
count takes a whole number. Inside the block, count.index gives the copy number, starting at 0:
variable "cities" {
type = list(string)
default = ["pune", "jaipur", "kochi"]
}
resource "local_file" "office" {
count = length(var.cities)
filename = "${path.module}/offices/${var.cities[count.index]}.conf"
content = "office=${var.cities[count.index]}\n"
file_permission = "0644"
}Run terraform apply. The resources get numbered addresses:
terraform state listlocal_file.office[0] local_file.office[1] local_file.office[2]
Step 2: See the index-shift problem
Now the Jaipur office closes. Remove "jaipur" from the list, so it becomes ["pune", "kochi"], and plan:
terraform plan # local_file.office[1] must be replaced
-/+ resource "local_file" "office" {
~ content = <<-EOT # forces replacement
- office=jaipur
+ office=kochi
EOT
~ filename = "./offices/jaipur.conf" -> "./offices/kochi.conf" # forces replacement
}
# local_file.office[2] will be destroyed
- resource "local_file" "office" {
- filename = "./offices/kochi.conf" -> null
}
Plan: 1 to add, 0 to change, 2 to destroy.The output is trimmed. Surprise: Terraform wants to touch Kochi too! Kochi moved from position 2 to position 1, so office[1] must become Kochi, and office[2] must go. With files this is harmless. With servers or databases, the Kochi server would be destroyed and rebuilt for no reason.
This is like a cinema row where seats are numbered. If the person in seat 2 leaves and everyone shifts one seat left, everyone after them has "changed seats", even though nobody new arrived.
Step 3: Use for_each instead
for_each takes a map or a set of strings. Each copy is named by its key, so removing one item affects only that item:
variable "offices" {
type = map(string)
default = {
pune = "Hinjewadi"
jaipur = "Malviya Nagar"
kochi = "Kakkanad"
}
}
resource "local_file" "office" {
for_each = var.offices
filename = "${path.module}/offices/${each.key}.conf"
content = "office=${each.key}\narea=${each.value}\n"
file_permission = "0644"
}Inside the block, each.key is the map key (pune) and each.value is its value (Hinjewadi). The addresses now use keys:
local_file.office["jaipur"] local_file.office["kochi"] local_file.office["pune"]
Remove jaipur from the map and plan again:
# local_file.office["jaipur"] will be destroyed
- resource "local_file" "office" {
- filename = "./offices/jaipur.conf" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.Only Jaipur is affected. Pune and Kochi are untouched. (If you are converting a real project from count to for_each, add moved blocks first, as shown in Import, Moved & Removed Blocks.)
for_each with a set
If you only need names, pass a set. Convert a list with toset(), because for_each does not accept lists directly:
resource "local_file" "team" {
for_each = toset(["priya", "rohan", "meera"])
filename = "${path.module}/team/${each.key}.txt"
content = "Welcome, ${each.key}!"
}For a set, each.key and each.value are the same.
When to use which
| Situation | Use |
|---|---|
| Copies that each have a stable name (cities, users, buckets) | for_each |
| Identical copies where order does not matter, such as "3 worker VMs" | count is acceptable |
| Create a resource or not, based on a flag | count = var.enabled ? 1 : 0 |
| Copies with different settings | for_each over a map of objects |
The on/off pattern is very common:
resource "local_file" "debug_log" {
count = var.debug ? 1 : 0
filename = "${path.module}/debug.log"
content = "debug mode on"
}Refer to it as local_file.debug_log[0], and only when var.debug is true. A safer way is one(local_file.debug_log[*].filename), which returns null when there are no copies.
for_each on modules
Modules accept count and for_each too. Using the notice module from the previous lesson:
module "notice" {
source = "./modules/notice"
for_each = { jaipur = "Water off Sunday", lucknow = "Metro till 11 pm" }
city = each.key
message = each.value
}The addresses become module.notice["jaipur"].local_file.this, and outputs are read as module.notice["jaipur"].path.
Common mistakes
- Using
countwith a list of names. Removing one name shifts every later index and replaces resources. - Passing a list to
for_each. Wrap it intoset(), or use a map. - Keys that are unknown until apply.
for_eachkeys must be known during plan. Do not build keys from values such as a server's future ID. - Using both
countandfor_eachon one block. It is not allowed; pick one.
Key points
countmakes numbered copies;count.indexis the position, starting at 0.- Removing an item from the middle of a
countlist shifts indexes and can replace other resources. for_eachmakes named copies from a map or set; useeach.keyandeach.value.- Prefer
for_eachwhenever each copy has a stable identity. count = condition ? 1 : 0creates a resource only when needed.- Modules support
countandfor_eachas well.
Exercise
Write a variable "students" as a map of objects, where each key is a roll number and each value has name and city. Use for_each to write one file per student called <roll>.txt that contains "<name> from <city>". Use these students: 101 Ananya from Pune, 102 Farhan from Lucknow.
Show solution
variable "students" {
type = map(object({
name = string
city = string
}))
default = {
"101" = { name = "Ananya", city = "Pune" }
"102" = { name = "Farhan", city = "Lucknow" }
}
}
resource "local_file" "student" {
for_each = var.students
filename = "${path.module}/students/${each.key}.txt"
content = "${each.value.name} from ${each.value.city}"
file_permission = "0644"
}terraform apply
cat students/102.txtFarhan from Lucknow
Each file is addressed by roll number, such as local_file.student["101"], so adding or removing a student never disturbs the others.
Quick Quiz
1. You use count over ["pune", "jaipur", "kochi"] and remove "jaipur". What happens?
count identifies copies by position, so later items shift.
2. Inside a for_each over a map, what is each.value?
each.key is the key and each.value is its value.
3. How do you create a resource only when var.enabled is true?
A count of 0 creates nothing; 1 creates one copy.
Learn Terraform & Ansible with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
