EthicsComputerCourses
Join training +91 90532 10052

count & for_each

Terraform & Ansible · Lesson 14 of 29 Intermediate 6 min read Updated

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:

HCL (Terraform)
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:

Terminal
terraform state list
Output
local_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:

Terminal
terraform plan
Output
  # 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:

HCL (Terraform)
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:

Output
local_file.office["jaipur"]
local_file.office["kochi"]
local_file.office["pune"]

Remove jaipur from the map and plan again:

Output
  # 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:

HCL (Terraform)
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

SituationUse
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 flagcount = var.enabled ? 1 : 0
Copies with different settingsfor_each over a map of objects

The on/off pattern is very common:

HCL (Terraform)
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:

HCL (Terraform)
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 count with a list of names. Removing one name shifts every later index and replaces resources.
  • Passing a list to for_each. Wrap it in toset(), or use a map.
  • Keys that are unknown until apply. for_each keys must be known during plan. Do not build keys from values such as a server's future ID.
  • Using both count and for_each on one block. It is not allowed; pick one.

Key points

  • count makes numbered copies; count.index is the position, starting at 0.
  • Removing an item from the middle of a count list shifts indexes and can replace other resources.
  • for_each makes named copies from a map or set; use each.key and each.value.
  • Prefer for_each whenever each copy has a stable identity.
  • count = condition ? 1 : 0 creates a resource only when needed.
  • Modules support count and for_each as 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
HCL (Terraform)
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"
}
Terminal
terraform apply
cat students/102.txt
Output
Farhan 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?

2. Inside a for_each over a map, what is each.value?

3. How do you create a resource only when var.enabled is true?

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