resource block 5 times is not an option. It violates the DRY principle and makes maintenance impossible. Terraform provides two primary looping constructs: count and for_each. Understanding when to use which is the hallmark of a Senior Terraform Engineer.1. The Simplest Loop: count#
The count meta-argument accepts a whole number (integer) and simply instructs Terraform to create that many identical copies of the resource.
Practice: Creating a Fleet with count#
Create a new directory named terraform-loops and navigate into it:
mkdir terraform-loops
cd terraform-loopsCreate a main.tf file and paste the following:
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "developers" {
# Terraform will create 3 identical users
count = 3
# How do we name them differently? We use count.index!
name = "developer-user-${count.index}"
}When you run terraform plan, Terraform will generate:
developer-user-0developer-user-1developer-user-2
The count.index object is a special variable injected by Terraform that represents the current iteration number (starting at 0).
The “List Shifting” Problem with count#
While count is extremely easy to use, it has a fatal flaw when combined with lists. Consider this configuration:
variable "usernames" {
type = list(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
count = length(var.usernames)
name = var.usernames[count.index]
}Terraform creates the users. Internally in the .tfstate file, Terraform maps them using array indices:
aws_iam_user.team[0]= “alice”aws_iam_user.team[1]= “bob”aws_iam_user.team[2]= “charlie”
The Catastrophe: What happens if Alice leaves the company, and you remove "alice" from the middle of the list? The list becomes ["bob", "charlie"].
When you run terraform plan, Terraform sees that index [0] which used to be Alice, must now be Bob. Index [1] which used to be Bob, must now be Charlie. Index [2] must be destroyed.
Terraform will attempt to rename Bob to Alice, rename Charlie to Bob, and completely delete Charlie’s old user account. This “list shifting” behavior will cause massive outages in production if applied to databases or servers.
2. The Robust Loop: for_each#
To solve the list shifting problem, Terraform introduced the for_each meta-argument. Instead of relying on fragile integer indices (0, 1, 2), for_each relies on the robust keys of a map (dictionary) or a set of strings.
Practice: Safe Iteration with for_each#
Let’s rewrite the IAM user creation using for_each.
variable "robust_usernames" {
type = set(string) # Notice we use a SET, not a LIST
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "robust_team" {
for_each = var.robust_usernames
# 'each.key' and 'each.value' represent the current item in the set
name = each.value
}When you run terraform plan, Terraform maps them internally using their string keys:
aws_iam_user.robust_team["alice"]= “alice”aws_iam_user.robust_team["bob"]= “bob”aws_iam_user.robust_team["charlie"]= “charlie”
The Resolution: If Alice leaves and you remove her from the set, Terraform will see that the key "alice" is gone. It will simply destroy Alice’s user. Bob (["bob"]) and Charlie (["charlie"]) remain completely untouched because their string keys have not shifted!
| Feature | count | for_each |
|---|---|---|
| Input Type | Integer (e.g., 3) | Map ({}) or Set of Strings (["a"]) |
| Iterator Variable | count.index (0, 1, 2) | each.key and each.value |
| State Tracking | By Array Index ([0]) | By String Key (["alice"]) |
| Best Used For | Identical anonymous resources (e.g., 5 identical web servers) | Distinct, named resources (e.g., 3 databases with different configurations) |
3. Advanced for_each with Maps#
The true power of for_each unlocks when you pass it a Map of Objects. This allows you to deploy multiple resources that share the same underlying architecture but have slightly different configurations.
variable "subnets" {
type = map(object({
cidr_block = string
az = string
}))
default = {
"frontend" = { cidr_block = "10.0.1.0/24", az = "us-east-1a" }
"backend" = { cidr_block = "10.0.2.0/24", az = "us-east-1b" }
"database" = { cidr_block = "10.0.3.0/24", az = "us-east-1c" }
}
}
resource "aws_subnet" "network" {
# We loop over the entire map!
for_each = var.subnets
vpc_id = "vpc-12345678" # Hardcoded for example brevity
# each.value gives us access to the object properties
cidr_block = each.value.cidr_block
availability_zone = each.value.az
tags = {
# each.key gives us the map key ("frontend", "backend", "database")
Name = "${each.key}-subnet"
}
}This single resource block dynamically spins up 3 entirely distinct AWS Subnets across 3 different Availability Zones, neatly tagged and perfectly tracked in the State File.
4. Conditional Resource Creation (Zero Count)#
There is one specific scenario where count is superior to for_each: Feature Toggles (Conditional Creation).
What if you have a Terraform module, and you only want to deploy a Load Balancer if the environment is “production”?
Since Terraform does not have traditional if statements for resources, you use count coupled with a ternary operator:
variable "environment" {
default = "staging"
}
resource "aws_lb" "production_only_load_balancer" {
# If environment is production, count is 1 (Create it).
# If environment is anything else, count is 0 (Do not create it).
count = var.environment == "production" ? 1 : 0
name = "prod-lb"
load_balancer_type = "application"
}This “Zero Count” trick is the industry standard for toggling infrastructure components on and off programmatically.
Troubleshooting & Common Errors#
The "for_each" value depends on resource attributes that cannot be determined until apply- Root Cause: You are trying to loop over a list of values (e.g., AWS IDs) that haven’t been created yet. Terraform must know the exact keys of a
for_eachmap during theplanphase. - Solution: You cannot use
for_eachon dynamically generated data (like an ID generated by another resource). You must either hardcode the keys or usecountif order doesn’t matter.
- Root Cause: You are trying to loop over a list of values (e.g., AWS IDs) that haven’t been created yet. Terraform must know the exact keys of a
Invalid value for "for_each" argument: set of string required.- Root Cause: You passed a standard
list(string)into afor_eachblock. - Solution: Wrap your list in the
toset()function to convert it into a Set before passing it tofor_each(e.g.,for_each = toset(var.my_list)).
- Root Cause: You passed a standard
Conclusion & Next Steps#
You have conquered infrastructure iteration. You now know how to conditionally toggle resources using count = 0, and how to safely loop over complex configurations using for_each without risking the dreaded “List Shifting” catastrophe.
However, loops like for_each only duplicate entire resource blocks. What if you want to loop over a configuration block inside a single resource (like creating 10 Ingress rules inside one single Security Group)?
In Episode 8: Generating Nested Configurations with Dynamic Blocks, we will explore how to dynamically construct nested blocks on the fly!

