DEV Community

Mukami
Mukami

Posted on

Mastering Loops and Conditionals in Terraform

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 }
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Making Resources Optional

variable "enable_autoscaling" {
  type    = bool
  default = true
}

resource "aws_autoscaling_policy" "scale_out" {
  count = var.enable_autoscaling ? 1 : 0
  # ... policy configuration
}
Enter fullscreen mode Exit fullscreen mode

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"
  } : {}
}
Enter fullscreen mode Exit fullscreen mode

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
  } : {}
}
Enter fullscreen mode Exit fullscreen mode

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)