Expressions & Functions
for / splat / conditional + the built-in stdlib.
Expressions & Functions
HCL is more expressive than it looks. Conditionals, splat operators, for-expressions, and a stdlib of built-in functions cover most config-time logic without dropping to a separate language.
Conditionals
Ternary, like most languages:
resource "aws_instance" "web" {
instance_type = var.environment == "prod" ? "m5.large" : "t3.micro"
monitoring = var.environment == "prod"
}
For multi-branch picks, use a map lookup:
locals {
type_per_env = {
dev = "t3.micro"
staging = "t3.small"
prod = "m5.large"
}
}
resource "aws_instance" "web" {
instance_type = local.type_per_env[var.environment]
}
Splat operators
When you have a list of resources (count or for_each), [*] extracts a single attribute from each:
resource "aws_instance" "web" {
count = 3
}
output "ips" {
value = aws_instance.web[*].public_ip # list of 3 IPs
}
The full splat is [*].attr. There's also a "full splat" form for nested attributes that the short form can't express.
for-expressions
A for-expression transforms a collection. Wrap in [ ] to produce a list, in { } to produce a map:
locals {
user_logins = ["alice", "bob", "carol"]
user_ids = [for u in local.user_logins : "user-${u}"]
# ["user-alice", "user-bob", "user-carol"]
user_lookup = { for u in local.user_logins : u => "user-${u}" }
# { alice = "user-alice", bob = "user-bob", carol = "user-carol" }
}
Add if to filter:
locals {
long_logins = [for u in local.user_logins : u if length(u) > 4]
# ["alice", "carol"]
}
Built-in functions
Terraform ships a generous stdlib. The everyday ones:
| Function | Use |
|---|---|
length(x) |
length of a list / map / string |
keys(m) / values(m) |
extract keys / values from a map |
lookup(m, k, default) |
safe map lookup with a fallback |
merge(a, b, c) |
merge maps; later keys win |
concat(a, b) |
concat lists |
flatten(xs) |
flatten one level of nesting |
toset(xs) / tolist(xs) |
type conversion |
format(fmt, args...) |
C-style sprintf |
join(sep, xs) / split(sep, s) |
string ↔ list |
lower / upper / title |
string casing |
cidrsubnet(prefix, newbits, idx) |
carve a subnet out of a CIDR |
file(path) / templatefile(path, vars) |
read a file (optionally rendered) |
jsonencode / jsondecode |
encode/decode JSON values |
yamlencode / yamldecode |
YAML equivalent |
base64encode / base64decode |
binary encoding |
templatefile is especially common for cloud-init / userdata / IAM policy JSON. You write the template once and pass the variables in.
Heredoc strings
For multi-line content, use a heredoc:
resource "aws_iam_policy" "logs" {
policy = <<-EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "${aws_s3_bucket.logs.arn}/*"
}
]
}
EOT
}
The <<- form strips leading indentation. ${...} interpolation works inside the heredoc.
For policy JSON specifically, prefer jsonencode(...) over a heredoc — you get type checking + automatic escaping:
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.logs.arn}/*"
}]
})
Tip
If you're computing something complex inline and using it in three places, hoist it into a locals block. The expression itself doesn't get faster, but plans and diffs stay readable.