Day 10 of the 30-Day Terraform Challenge — and today I learned that Terraform is actually a programming language in disguise.
For the past 9 days, I've been writing infrastructure like it's 2010: one resource at a time, copy-pasting blocks, and feeling clever when I only duplicated code three times instead of five.
Today I discovered loops and conditionals. And I'm never going back.
The Problem: Copy-Paste Engineering
Let me show you what I was doing before today:
# Creating 3 users? Copy-paste!
resource "aws_iam_user" "alice" { name = "alice" }
resource "aws_iam_user" "bob" { name = "bob" }
resource "aws_iam_user" "charlie" { name = "charlie" }
# Security group with 3 rules? Copy-paste!
ingress { from_port = 80 }
ingress { from_port = 443 }
ingress { from_port = 22 }
This works. Until you need 10 users. Or 50 security group rules. Or you need to change something across all of them.
There had to be a better way.
There is.
Tool 1: count — The Simple Loop
count creates multiple copies of a resource. Think of it as a for-loop for infrastructure.
# Create 3 identical IAM users
resource "aws_iam_user" "users" {
count = 3
name = "user-${count.index}"
}
count.index gives you the position (0, 1, 2). This creates:
- user-0
- user-1
- user-2
You can also use it with a list:
variable "user_names" {
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "users" {
count = length(var.user_names)
name = var.user_names[count.index]
}
The Problem Nobody Warns You About
Here's where count gets dangerous.
Imagine your user list: ["alice", "bob", "charlie"]
Terraform tracks resources by their index position:
- Index 0 = alice
- Index 1 = bob
- Index 2 = charlie
Now you remove "alice" from the list: ["bob", "charlie"]
The list shifts:
- Index 0 = bob (was alice!)
- Index 1 = charlie (was bob!)
What Terraform sees: Index 0 changed from alice to bob → destroy alice, create bob. Index 1 changed from bob to charlie → destroy bob, create charlie.
Bob and Charlie get destroyed and recreated even though they still exist. Any data, IPs, or attached resources are lost.
This is the count trap. It's subtle, destructive, and catches everyone once.
Tool 2: for_each — The Safe Loop
for_each solves the index problem by using keys instead of positions.
# Using a set
variable "user_names" {
type = set(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "users" {
for_each = var.user_names
name = each.value
}
Now Terraform tracks resources by their value (alice, bob, charlie), not their position.
Remove "alice" from the set? Only Alice is destroyed. Bob and Charlie stay exactly where they are.
The Power of Maps
Where for_each really shines is with maps, because you can carry extra configuration per item:
variable "users" {
type = map(object({
department = string
admin = bool
}))
default = {
alice = { department = "engineering", admin = true }
bob = { department = "marketing", admin = false }
}
}
resource "aws_iam_user" "users" {
for_each = var.users
name = each.key
tags = {
Department = each.value.department
Admin = tostring(each.value.admin)
}
}
Now each user gets unique tags based on their configuration. Try doing that with count.
Tool 3: for Expressions — Transform Data Without Creating Resources
for expressions reshape data. They don't create infrastructure—they transform what you already have.
# Uppercase all names
output "upper_names" {
value = [for name in var.user_names : upper(name)]
}
# ["ALICE", "BOB", "CHARLIE"]
# Create a map from a list
output "user_map" {
value = { for idx, name in var.user_names : idx => name }
}
# { "0" = "alice", "1" = "bob", "2" = "charlie" }
# Filter and transform
output "admin_users" {
value = {
for name, user in aws_iam_user.users : name => user.arn
if user.admin == true
}
}
# Only admin users appear in the output
These are incredibly useful for creating clean outputs that other configurations can consume.
Tool 4: Ternary Conditionals — Make Decisions
The ternary operator is Terraform's if/else:
condition ? true_value : false_value
Making Resources Optional
variable "enable_autoscaling" {
type = bool
default = true
}
resource "aws_autoscaling_policy" "scale_out" {
count = var.enable_autoscaling ? 1 : 0
# ... policy configuration
}
When enable_autoscaling = false, count becomes 0 and the policy is never created.
Environment-Specific Configuration
variable "environment" {
type = string
default = "dev"
}
locals {
instance_type = var.environment == "production" ? "t3.medium" : "t3.micro"
extra_tags = var.environment == "production" ? {
Criticality = "high"
Backup = "enabled"
} : {}
}
Now you have one codebase that works for dev (small, cheap) and production (big, protected).
Putting It All Together
Here's how I refactored my web server cluster module:
Before: Hardcoded security group rules, no autoscaling options, repetitive outputs.
After:
# Optional autoscaling policies
resource "aws_autoscaling_policy" "scale_out" {
count = var.enable_autoscaling ? 1 : 0
name = "${var.cluster_name}-scale-out"
autoscaling_group_name = aws_autoscaling_group.web.name
scaling_adjustment = 1
}
# Flexible security group rules
resource "aws_security_group_rule" "additional" {
for_each = var.additional_sg_rules
type = "ingress"
from_port = each.value.from_port
to_port = each.value.to_port
security_group_id = aws_security_group.instance.id
}
# Clean outputs
output "autoscaling_policies" {
value = var.enable_autoscaling ? {
scale_out = aws_autoscaling_policy.scale_out[0].name
} : {}
}
When to Use What: The Cheat Sheet
| Tool | Best For | Avoid When |
|---|---|---|
| count | Identical resources, fixed numbers | Lists that might change order |
| for_each | Resources with unique identities, maps, changing lists | Creating thousands of resources (state file gets huge) |
| for expressions | Data transformation, outputs, locals | Creating actual infrastructure |
| ternary | Simple conditional values, resource toggles | Complex nested logic (use locals instead) |
The Count Trap: A Real Example
I built a simple IAM user setup with count. It worked perfectly. Then I removed a user from the middle of the list. terraform plan showed that every user after that position would be recreated.
I tested with random IDs attached to each user. After removing one, the IDs of remaining users changed. They were destroyed and recreated.
The cost? If these were EC2 instances, I'd lose their IPs, attached volumes, and any stateful data. If these were databases, I'd lose data.
The fix? I refactored to use for_each. Now removing any user affects only that user.
What I Learned
Loops eliminate repetition. One block now does the work of ten.
count is simple but dangerous. It works until your list changes. Then it breaks things in ways that are hard to debug.
for_each is safer. It respects identity, not position. Use it whenever your resources have unique names.
Conditionals make code reusable. One module now serves dev and production, with features that turn on and off based on variables.
for expressions are the glue. They transform raw resources into clean outputs that others can consume.
The Bottom Line
Before today, I wrote infrastructure by copy-pasting. Now I write it with loops and conditionals.
My code is:
- Shorter — 200 lines instead of 500
- Safer — removing items doesn't break everything
- Smarter — dev gets small instances, production gets big ones
- More maintainable — change one place, all environments update
Terraform isn't just a tool for creating resources. It's a language for describing infrastructure. And today, I finally started treating it like one.
P.S. If you're still writing 10 separate resource blocks for 10 similar resources, stop. Learn for_each. Your future self will thank you.
Top comments (0)