Terraform Tutorial — For Expressions

2 minute read

For expressions let you transform one collection into another. They’re HCL’s equivalent of list comprehensions in Python or .map() in JavaScript.


Basic For Expression — Lists

The syntax uses square brackets to produce a list:

variable "names" {
  default = ["alice", "bob", "carol"]
}

locals {
  upper_names = [for name in var.names : upper(name)]
  # Result: ["ALICE", "BOB", "CAROL"]
}

How to read it: “For each name in var.names, produce upper(name).”


Transforming Lists

Add a Suffix

variable "server_names" {
  default = ["web", "api", "worker"]
}

locals {
  fqdns = [for name in var.server_names : "${name}.example.com"]
  # Result: ["web.example.com", "api.example.com", "worker.example.com"]
}

Include the Index

locals {
  indexed_names = [for i, name in var.server_names : "${i}: ${name}"]
  # Result: ["0: web", "1: api", "2: worker"]
}

For Expression — Maps

Use curly braces to produce a map (key => value):

variable "users" {
  default = ["alice", "bob", "carol"]
}

locals {
  user_roles = {for user in var.users : user => "developer"}
  # Result: { alice = "developer", bob = "developer", carol = "developer" }
}

Transforming Map Values

variable "tags" {
  default = {
    Name = "web-server"
    Env  = "dev"
  }
}

locals {
  upper_tags = {for k, v in var.tags : k => upper(v)}
  # Result: { Name = "WEB-SERVER", Env = "DEV" }
}

Filtering with if

Add an if clause to include only certain elements:

variable "ports" {
  default = [22, 80, 443, 3306, 5432, 8080]
}

locals {
  web_ports = [for port in var.ports : port if port < 1024]
  # Result: [22, 80, 443]
}

Filter with Objects

variable "users" {
  type = list(object({
    name  = string
    admin = bool
  }))
  default = [
    { name = "alice", admin = true },
    { name = "bob",   admin = false },
    { name = "carol", admin = true },
  ]
}

locals {
  admin_names = [for user in var.users : user.name if user.admin]
  # Result: ["alice", "carol"]
}

for_each vs for Expression

These are different things that beginners often confuse:

Feature for expression for_each meta-argument
Purpose Transform collections in expressions Create multiple resource instances
Used in locals, output, variable defaults resource, module blocks
Syntax [for x in list : ...] for_each = toset(...)
Result A new list or map Multiple resource instances

for_each Example

variable "bucket_names" {
  default = ["logs", "assets", "backups"]
}

resource "aws_s3_bucket" "buckets" {
  for_each = toset(var.bucket_names)

  bucket = "${var.project}-${each.value}"
}

This creates three S3 buckets — one for each name in the list. Each instance is identified by the set value: aws_s3_bucket.buckets["logs"], aws_s3_bucket.buckets["assets"], etc.

Using for_each with a Map

variable "buckets" {
  type = map(object({
    versioning = bool
    acl        = string
  }))
  default = {
    logs = {
      versioning = true
      acl        = "private"
    }
    assets = {
      versioning = false
      acl        = "public-read"
    }
  }
}

resource "aws_s3_bucket" "buckets" {
  for_each = var.buckets

  bucket = "${var.project}-${each.key}"

  tags = {
    Name       = each.key
    Versioning = each.value.versioning
  }
}

Practical Example: One S3 Bucket Per Environment

Combining for_each with a for expression:

variable "environments" {
  default = ["dev", "staging", "prod"]
}

resource "aws_s3_bucket" "env_buckets" {
  for_each = toset(var.environments)

  bucket = "myapp-${each.value}-data"

  tags = {
    Environment = each.value
    ManagedBy   = "terraform"
  }
}

output "bucket_arns" {
  value = {for env, bucket in aws_s3_bucket.env_buckets : env => bucket.arn}
}

The output produces a map like:

{
  "dev"     = "arn:aws:s3:::myapp-dev-data"
  "staging" = "arn:aws:s3:::myapp-staging-data"
  "prod"    = "arn:aws:s3:::myapp-prod-data"
}

What’s Next?

You can now transform and filter collections. Next, let’s learn about terraform fmt and terraform validate — two commands that keep your HCL clean and correct.


← Lists, Maps & Objects terraform fmt & validate →