When you first start writing Terraform, you declare every resource manually. If you need three private subnets, you write three aws_subnet blocks. If you need five IAM users, you write five aws_iam_user blocks.
This works for simple tutorials, but in a true enterprise environment, hardcoding infrastructure is a massive anti-pattern. Real infrastructure needs to be dynamic, scaling up or down based on variable inputs without requiring massive code rewrites.
To write professional-grade Terraform, you must master its four primary dynamic tools: count, for_each, for expressions, and conditionals. Here is the definitive guide to how they work, when to use them, and the one dangerous trap that catches almost every junior engineer.
1. count: The Simple Loop
The count meta-argument is the simplest way to create multiple identical copies of a resource. You pass it an integer, and Terraform creates that many instances, tracking them with an index number (count.index) starting at 0.
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server-${count.index}" # Results in web-server-0, web-server-1, etc.
}
}
When to use it: Use count when the resources are truly identical and interchangeable, and you simply need N number of them.
2. The count Index Problem (The Danger Zone)
It is tempting to use count with the length() function to iterate over a list of strings, like a list of usernames. Do not do this.
The Broken Architecture
Imagine you have a list of developers who need IAM users:
variable "devs" {
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
count = length(var.devs)
name = var.devs[count.index]
}
Terraform maps these internally by their index:
aws_iam_user.team[0]= aliceaws_iam_user.team[1]= bobaws_iam_user.team[2]= charlie
What breaks: Suppose "alice" leaves the company, so you remove her from the list. The list is now ["bob", "charlie"].
Instead of just deleting Alice, Terraform looks at the new indexes:
Index 0 is now bob. Terraform renames Alice's user to Bob.
Index 1 is now charlie. Terraform renames Bob's user to Charlie.
Index 2 no longer exists. Terraform deletes Charlie's original user.
You just caused massive destructive churn across your entire team's access because a list shifted.
3. for_each: The Safe Loop
To solve the list-shifting problem, Terraform introduced for_each. Instead of using a fragile numerical index, for_each iterates over a map or a set of strings, using the actual string values as the unique identifier.
The Correct Architecture
If we refactor the IAM user example using a set:
variable "devs" {
type = set(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
for_each = var.devs
name = each.value
}
Now, Terraform tracks them like this:
aws_iam_user.team["alice"]
aws_iam_user.team["bob"]
If you remove "alice" from the variable, Terraform only deletes aws_iam_user.team["alice"]. Bob and Charlie are completely untouched because their identifiers never changed.
When to use it: Use for_each whenever you are creating multiple resources that have unique identities (like subnets with different CIDR blocks, or users with different names).
4. Conditionals: The Ternary Operator
Sometimes you don't need a loop; you just need an ON/OFF switch. Terraform handles conditional logic using the ternary operator: condition ? true_value : false_value.
The most powerful way to use this is by combining it with count to make an entire resource optional based on an environment variable.
For example, you might want an Auto Scaling Policy in Production, but not in Dev to save costs:
variable "enable_scaling_policy" {
description = "If true, creates an Auto Scaling policy"
type = bool
default = false
}
resource "aws_autoscaling_policy" "scale_up" {
# If true, count = 1 (resource is created). If false, count = 0 (resource is skipped).
count = var.enable_scaling_policy ? 1 : 0
name = "production-scale-up"
autoscaling_group_name = aws_autoscaling_group.asg.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
}
5. for Expressions: Reshaping Data
Do not confuse for_each with for expressions.
for_eachcreates resources.forexpressions reshape data.
Often, you need to output data from your module, but the raw Terraform state object is too messy. A for expression lets you loop through a complex object and extract exactly what you want, usually formatting it into a clean list or map.
Here is how you extract a clean dictionary mapping your private subnet names directly to their generated AWS Subnet IDs:
output "private_subnet_map" {
description = "A clean map of private subnet keys to their actual AWS Subnet IDs"
# Loops through the aws_subnet.private resources, grabbing the key (k) and the ID
value = { for k, subnet in aws_subnet.private : k => subnet.id }
}
Output result:
private_subnet_map = {
"private_app_1" = "subnet-0a1b2c3d4e5f6g7h8"
"private_app_2" = "subnet-0z9y8x7w6v5u4t3s2"
}
Final Thoughts
Mastering these four tools transitions you from writing static Terraform scripts to engineering highly reusable, scalable infrastructure modules.
Stop copy-pasting resource blocks. Start looping.
Top comments (0)