terraform · level 4

Expressions & Functions

for / splat / conditional + the built-in stdlib.

100 XP

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.