Chapter 4: Variables, Outputs, and Expressions
Topic 27

Built-in Functions

ExpressionsFunctions

Terraform ships a library of built-in functions for strings, collections, encoding, filesystem, date, and crypto — merge, lookup, coalesce, jsonencode, templatefile, cidrsubnet, and dozens more. There are no user-defined functions in core HCL — provider-defined functions arrived in 1.8 — so this fixed library plus expressions is your whole toolbox for transforming data.

That constraint shapes how you write Terraform. You cannot define a helper to factor out a repeated transformation, so the skill is recognizing which built-in already does what you want. Most "I wish I could write a function for this" moments dissolve into a merge, a for expression, or a templatefile once you know the library.

Collection Functions

merge combines maps with later keys winning, concat joins lists, flatten collapses nested lists, and lookup reads a map key with a fallback. keys, values, and contains inspect a collection. These are the workhorses for reshaping the data that feeds for_each and dynamic blocks.

locals.tf — collection functions
locals {
  all_tags = merge(var.default_tags, var.extra_tags)

  # lookup with a default avoids erroring on a missing key
  size = lookup(var.sizes, var.environment, "t3.micro")
}

String Functions

format builds a string from a template and arguments, join and split convert between a list and a delimited string, replace substitutes text, and lower/upper/trimspace normalize. These keep name construction readable instead of a pile of nested interpolations.

locals.tf — string functions
locals {
  name     = format("%s-%s-%03d", var.project, var.environment, var.index)
  az_list  = join(",", var.availability_zones)
  slug     = lower(replace(var.display_name, " ", "-"))
}

Encoding Functions

jsonencode and yamlencode turn an HCL value into a correctly-escaped JSON or YAML string, and their decode counterparts parse one back. This is the right way to build an IAM policy or a config document — you write the structure as native HCL and let the function produce valid serialized text, instead of concatenating braces and quotes by hand.

main.tf — jsonencode for a policy
resource "aws_iam_role_policy" "app" {
  role   = aws_iam_role.app.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = "${aws_s3_bucket.data.arn}/*"
    }]
  })
}

Filesystem and Template Functions

file reads a file's contents into a string, and templatefile renders a separate template file with a map of variables — the clean way to produce user-data or a config file. Keeping the template in its own file, rather than inline as a heredoc, makes it readable and lets editors highlight it correctly.

main.tf — templatefile for user-data
resource "aws_instance" "web" {
  ami           = "ami-0abc123"
  instance_type = "t3.micro"
  user_data     = templatefile("${path.module}/init.sh.tftpl", {
    app_env = var.environment
    port    = 8080
  })
}

Network and Numeric Functions

cidrsubnet carves a subnet out of a larger CIDR block by adding bits and selecting an index, and cidrhost returns a specific host address within a range. Computing these by hand is how overlapping or wrong ranges slip in; the functions make subnet math deterministic. min, max, ceil, and floor round out the numeric set.

locals.tf — subnet math with cidrsubnet
locals {
  # /16 base, add 8 bits to get /24s, by index
  subnet_a = cidrsubnet("10.0.0.0/16", 8, 0)   # 10.0.0.0/24
  subnet_b = cidrsubnet("10.0.0.0/16", 8, 1)   # 10.0.1.0/24
}
Common Mistakes
  • Reaching for a function that doesn't exist — there are no arbitrary user-defined functions in core — instead of restructuring with the ones provided.
  • Calling lookup without a default on a key that may be absent, getting an error where a fallback was intended.
  • Hand-computing subnet CIDRs instead of cidrsubnet, producing overlapping or wrong ranges that peering later rejects.
  • Embedding a rendered template inline with string concatenation rather than templatefile, making it unreadable and error-prone.
  • Concatenating a JSON policy by hand instead of jsonencode, so a special character in an ARN produces invalid JSON.
Best Practices
  • Use templatefile for user-data and rendered config, keeping the template in its own .tftpl file.
  • Compute subnet ranges with cidrsubnet and cidrhost rather than by hand, so the math is deterministic.
  • Provide a default to lookup (or use try) so a missing key degrades gracefully instead of erroring.
  • Build structured documents with jsonencode and yamlencode, never string concatenation.
  • Reach for merge to combine tag maps and coalesce to pick the first non-null value, instead of nested conditionals.
Comparable tools CloudFormation intrinsic functions are far fewer and clumsier Pulumi uses the host language's full standard library Ansible uses Jinja2 filters for the same transformations

Knowledge Check

You want a helper that factors out a transformation you repeat in three places. What does core HCL let you do?

  • Nothing custom — core has no user-defined functions, so you restructure with the built-ins or a local
  • Define a top-level function block in the file and call it by name like any built-in helper would be invoked
  • Import a callable function from another module's exported outputs and invoke it by name from the parent
  • Write an inline lambda with the => operator and reuse the closure

Why pass a default to lookup(var.sizes, var.environment, "t3.micro")?

  • If the key is absent, the default is returned instead of the expression erroring
  • The default encrypts the looked-up value before it is returned to the calling expression
  • lookup requires exactly three arguments and fails with only two
  • The default makes the key match case-insensitively against the map

What problem does templatefile solve over an inline heredoc?

  • It keeps the template in its own file, rendered with named variables, so it stays readable and highlightable
  • It is the only construct in the language that can interpolate a variable into a string literal at render time
  • It encrypts the rendered user-data output before it ever reaches the launching instance
  • It executes the rendered template as a shell script at plan time

When does cidrsubnet prevent a real bug?

  • It computes non-overlapping subnet ranges deterministically, avoiding the overlaps hand-math produces that later break peering
  • It validates that each derived CIDR block is publicly routable and non-overlapping before any VPC peering connection is established
  • It automatically assigns each computed subnet to a distinct availability zone in the region
  • It encrypts traffic flowing between the subnets it computes from the base block

You got correct