Lesson 11 of 16
Section 3: Variables, Expressions, Loops & Meta-Arguments40 min read & walkthrough

Loops & Conditionals: Count vs For_Each & Splat Operators

01 / COUNT
Numeric Loops

Best for simple identical scaling and conditional binary resource toggles (0 or 1).

02 / FOR_EACH
Keyed Maps

Iterates over maps and sets using stable keys, eliminating destructive index shifts.

03 / SPLAT
List Projections

Extract attributes across all instances instantly with the splat operator '[*]'.

The Big Idea

When provisioning multiple similar resources (such as 3 public subnets or 5 S3 buckets), copy-pasting the same resource block is messy, error-prone, and violates DRY principles. Terraform provides two meta-arguments for iteration: count and for_each.

While count appears simple at first glance, it has a famous trap known as the Index Shift Bug. Because count tracks resources by an array index ([0], [1], [2]), removing an element from the middle of your list causes Terraform to destroy and recreate every subsequent resource in the list.

for_each solves this completely: it iterates over maps or sets using meaningful string keys. If you delete one key, Terraform destroys only that specific resource, leaving every other resource untouched. Senior engineers reserve count exclusively for conditional switches (count = var.enabled ? 1 : 0) and use for_each for everything else.

💡

Jargon Buster: Plain-English Translations

  • Count Meta-Argument: An argument that accepts a whole number to create N instances of a resource indexed by integer keys.
  • Index Shift Trap: The unintended recreation of multiple resources caused by removing a middle item from a count-indexed list.
  • For_Each Meta-Argument: An argument that accepts a map or set of strings to create instances tracked by unique, stable keys.
  • Splat Expression ([*]): A concise syntax to project a single attribute across a collection of resource instances, e.g. 'aws_subnet.public[*].id'.

Count Trap vs Resilient For_Each Iteration

# 1. The Anti-Pattern: Count on Lists (Vulnerable to Index Shifts)
variable "bad_subnets" {
  type    = list(string)
  default = ["subnet-a", "subnet-b", "subnet-c"]
}
# Removing "subnet-a" will re-index b to 0 and c to 1, causing unwanted recreation!

# 2. The Senior Solution: For_Each with Stable Keyed Maps
variable "subnets_config" {
  type = map(object({
    cidr = string
    az   = string
  }))
  default = {
    "public_us_east_1a" = { cidr = "10.0.1.0/24", az = "us-east-1a" }
    "public_us_east_1b" = { cidr = "10.0.2.0/24", az = "us-east-1b" }
    "public_us_east_1c" = { cidr = "10.0.3.0/24", az = "us-east-1c" }
  }
}

resource "aws_subnet" "public" {
  for_each          = var.subnets_config # Keyed iteration
  vpc_id            = aws_vpc.core.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  tags = {
    Name = each.key # Unique, stable name identifier
  }
}

# 3. Conditional Resource Toggle (Safe use of count)
variable "enable_bastion" {
  type    = bool
  default = false
}

resource "aws_instance" "bastion" {
  count         = var.enable_bastion ? 1 : 0 # 1 if true, 0 if false
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}
⚠️

Beginner Traps & Gotchas

1. Passing a raw list of strings to for_each

The Gotcha: Attempting 'for_each = ["a", "b"]' and getting a type error.

The Fix: for_each requires a map or a set. Wrap lists in the toset() function: 'for_each = toset(var.list_of_strings)'.

2. Using count for databases or stateful infrastructure

The Gotcha: Using 'count = length(var.databases)' and having production databases destroyed when changing the list order.

The Fix: Never use count for stateful resources like RDS, EFS, or EBS volumes. Always use for_each with immutable keys.