Terraform Tutorial — For Expressions
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.