We close the HCL chapter with the tools that make your code smart and efficient: conditionals (making decisions) and loops (creating many resources without repeating yourself). Without them, you’d have to copy and paste the same block over and over. With them, you write once and Terraform generates whatever is needed.

The problem: don’t repeat yourself

Imagine you need three identical servers. Without loops, you’d write three almost identical resource blocks. If you needed to change something, you’d have to change it in all three. That’s tedious and error-prone.

Loops solve this: you declare the resource once and indicate “create it N times.” Let’s look at the tools.

count: create N copies

The count argument creates multiple copies of a resource. You give it a number and Terraform creates that many.

resource "aws_instance" "web" {
  count         = 3                  # ← creates 3 instances
  ami           = "ami-0c1234567890abcde"
  instance_type = "t3.micro"
  tags = {
    Name = "web-${count.index}"      # web-0, web-1, web-2
  }
}
  • count = 3 creates three instances.
  • count.index is the number of each copy (0, 1, 2), useful for giving them different names.

Analogy: count is like telling a photocopier “make me 3 copies of this document.” All the same, numbered.

count is also useful for conditionals (create a resource or not):

resource "aws_instance" "bastion" {
  count = var.create_bastion ? 1 : 0   # 1 if true, 0 if false
  # ...
}

If var.create_bastion is true, it creates 1; if it’s false, it creates 0 (none). This is a very common trick to make a resource “optional.”

for_each: create copies from a collection

The for_each argument creates a copy for each element of a map or set. Unlike count (which uses numbers), for_each uses meaningful keys.

resource "aws_instance" "server" {
  for_each      = {
    web = "t3.micro"
    api = "t3.small"
    db  = "m5.large"
  }
  ami           = "ami-0c1234567890abcde"
  instance_type = each.value           # the value: t3.micro, t3.small, m5.large
  tags = {
    Name = each.key                    # the key: web, api, db
  }
}
  • Creates one instance for each entry in the map.
  • each.key is the key (web, api, db).
  • each.value is the value (t3.micro, etc.).

Analogy: for_each is like having a list of custom orders: “for web, a t3.micro; for api, a t3.small; for db, an m5.large.” Each with its own characteristics.

count vs for_each: which should I use?

This is an important decision and a typical source of confusion:

count for_each
Based on A number A map or a set
Identifies each copy by Position (0, 1, 2…) Meaningful key
Good for Identical copies or optional resources Resources with their own identity
Problem when deleting one in the middle Reorders and may recreate others Doesn’t affect the rest

Practical rule:

  • Use count to create N identical copies or to make a resource optional (count = condition ? 1 : 0).
  • Use for_each when each resource has its own identity (different names, different configurations). It’s safer when adding or removing elements.

Why for_each is usually preferred: with count, if you delete the element in the middle of a list, the following ones “shift” and Terraform may destroy and recreate resources unnecessarily. With for_each, each resource is tied to its key, so removing one doesn’t affect the others. That’s why many professionals prefer for_each except for simple cases.

The for expression: transforming collections

Don’t confuse the for_each loop (which creates resources) with the for expression (which transforms data). The for expression generates a new list or map from another, similar to a formula.

locals {
  names        = ["web", "api", "db"]
  names_upper  = [for n in local.names : upper(n)]
  # result: ["WEB", "API", "DB"]
}

Read it as: “for each n in the names list, return upper(n).” It’s a compact way to transform all elements of a collection.

Useful example: convert a list of names into a map, or filter elements that meet a condition. It’s an advanced tool you’ll see in real code, but at first it’s enough to recognize it.

Conditionals: the ternary operator

To make decisions within an expression, Terraform uses the ternary operator (the same as in many languages):

condition ? value_if_true : value_if_false

Examples:

instance_type = var.environment == "prod" ? "m5.large" : "t3.micro"
# If the environment is "prod", use m5.large; otherwise, t3.micro

count = var.high_availability ? 2 : 1
# If you want high availability, create 2; otherwise, 1

Analogy: it’s like saying “is it production? Then the big server; if not, the small one.” A quick decision in a single line.

A realistic example

variable "environment" {
  type    = string
  default = "dev"
}

variable "subnets" {
  type = map(string)
  default = {
    public-a  = "10.0.1.0/24"
    public-b  = "10.0.2.0/24"
  }
}

resource "aws_subnet" "this" {
  for_each   = var.subnets              # one subnet per entry
  vpc_id     = aws_vpc.main.id
  cidr_block = each.value               # the range of each subnet
  tags = {
    Name = each.key                     # public-a, public-b
    Type = var.environment == "prod" ? "production" : "development"  # conditional
  }
}

This code creates one subnet for each entry in the subnets map (for_each loop), and tags each one according to the environment (ternary conditional). You wrote one block and Terraform generates as many subnets as you define, without repeating code.

What you should remember

  • count: creates N copies of a resource (count = 3) or makes it optional (count = condition ? 1 : 0). Identifies copies by position (count.index).
  • for_each: creates a copy for each element of a map or set, with its own identity (each.key, each.value). Safer when adding/removing elements.
  • Rule: count for identical or optional copies; for_each when each resource is different (usually preferable).
  • The for expression transforms collections (don’t confuse with the for_each loop).
  • The ternary operator condition ? a : b makes decisions in one line (ideal for differentiating environments).

With this, you’ve finished Chapter 10 and now know how to read and write HCL. In Chapter 11 we’ll look at two fundamental pieces for Terraform to work: providers (how it talks to AWS) and state (how it remembers what it has created).

Cloud, AWS & Terraform — From Zero to Expert

Chapter 1 · What is cloud computing

Chapter 2 · The cloud market and major providers

Chapter 3 · Regions, availability zones and edge

Chapter 4 · Compute: EC2

Chapter 5 · Storage: S3

Chapter 6 · Networking: VPC

Chapter 7 · Identity and access: IAM

Chapter 8 · Managed databases

Chapter 9 · Why Infrastructure as Code

Chapter 10 · HCL: the Terraform language

Chapter 11 · Providers and state

Chapter 12 · Your first real infrastructure in Terraform

Chapter 13 · Load balancing and auto scaling

Chapter 14 · Serverless with Lambda

Chapter 15 · Messaging and events

Chapter 16 · Content delivery and DNS

Chapter 17 · Containers on AWS

Chapter 18 · Modules: reuse and composition

Chapter 19 · Workspaces and environment management

Chapter 20 · Remote backends and locking

Chapter 21 · Infrastructure testing

Chapter 22 · Terraform in CI/CD

Chapter 23 · Defense in depth

Chapter 24 · Observability: logs, metrics and traces

Chapter 25 · Cost optimization

Chapter 26 · High availability and disaster recovery

Chapter 27 · AWS Well-Architected Framework

Chapter 28 · Serverless architectures at scale

Chapter 29 · Data platforms on AWS

Chapter 30 · Multi-account and landing zones

Chapter 31 · Platform Engineering and Internal Developer Platform

Chapter 32 · Relevant AWS certifications

Chapter 33 · Projects to consolidate what you've learned

Chapter 34 · Resources and community

© Copyright 2024. All rights reserved