DevOps Peach

Advanced Terraform · Modules and Code Structure · Lesson 5 of 5

Built-in Functions

Terraform has no user-defined functions, but more than a hundred built-ins: strings, collections, encoding, files and templates, IP networks, type conversion, dates and hashes, plus functions shipped by providers.

Practice interactively →

Terraform expressions can call built-in functions: name(arg1, arg2). You cannot write your own functions in HCL; when a calculation gets complex, move it into locals with clear names, or use a provider-defined function (Terraform 1.8+). Try any function in terraform console before you use it.

flowchart LR
  IN["Input: variables, files"] --> S["String: format, join, replace"]
  IN --> C["Collection: merge, flatten, lookup"]
  IN --> F["Files: file, templatefile"]
  S --> L["locals"]
  C --> L
  F --> L
  L --> E["Encoding: jsonencode, yamlencode"]
  E --> R["resource arguments"]
  %% step IN: Raw input arrives as variables, data sources and files in the module folder.
  %% step S: String functions build names, labels and URLs from that input.
  %% step C: Collection functions reshape lists and maps, for example merging default and custom tags.
  %% step F: File functions read files relative to path.module and render templates with variables.
  %% step L: Keep the result in locals with readable names instead of one long nested call.
  %% step E: Encoding functions turn HCL values into the JSON or YAML documents that many arguments expect.
  %% step R: The final value is passed to resource arguments.

The families you use every day

Family Functions Example → result
String format, join, split, replace, lower, upper, trimspace, substr, regex, startswith format("shop-%s-%02d", "dev", 3) → "shop-dev-03"
Collection length, merge, concat, flatten, keys, values, lookup, contains, distinct, zipmap, setproduct, element, slice merge({a=1}, {b=2}) → {a=1, b=2}
Numeric min, max, ceil, floor, abs, pow max(2, var.replicas)
Encoding jsonencode, jsondecode, yamlencode, yamldecode, base64encode, csvdecode jsonencode({port = 80}) → "{\"port\":80}"
Files file, templatefile, fileset, filebase64, fileexists file("${path.module}/init.sh")
IP network cidrsubnet, cidrhost, cidrnetmask, cidrsubnets cidrsubnet("10.0.0.0/16", 8, 2) → "10.0.2.0/24"
Type conversion tostring, tonumber, tolist, toset, tomap, try, can, type try(var.cfg.port, 8080)
Date and time timestamp, formatdate, timeadd, plantimestamp formatdate("YYYY-MM-DD", plantimestamp())
Hash and crypto sha256, md5, filesha256, uuid, bcrypt filesha256("app.zip")

Patterns worth knowing

locals {
  tags     = merge(var.default_tags, var.extra_tags)            # later maps win
  subnets  = [for i in range(3) : cidrsubnet(var.cidr, 8, i)]    # 10.0.0.0/24, 10.0.1.0/24, ...
  pairs    = setproduct(var.envs, var.apps)                     # every env x app
  app_cfg  = jsonencode({ db = var.db_host, debug = var.debug })
  user_data = templatefile("${path.module}/cloud-init.yaml.tftpl", {
    hostname = var.name
    packages = var.packages
  })
  port     = try(var.settings.port, 8080)                        # fallback if missing
}

templatefile reads a template file and fills ${...} and %{ for } directives from the map you pass; it replaces the old template_file data source. Use path.module so the path works no matter where Terraform runs.

Functions that bite

Function Trap
timestamp() A new value on every run, so the resource shows a change on every plan. Use plantimestamp() or time_static
uuid() Same problem: new value each run
file() Reads at plan time from disk; it cannot read files that a resource creates during apply
lookup(map, key) Without a default it errors on a missing key; prefer lookup(map, key, default) or try()
element(list, i) Wraps around past the end instead of failing

Provider-defined functions

Providers can ship functions, called as provider::<name>::<function>(...), for example provider::aws::arn_parse(var.role_arn). The provider must be listed in required_providers.

Traps: functions run during plan, so they cannot see values that are only known after apply ((known after apply) flows through instead); and can() is for validation conditions, while try() returns a fallback value.

Practice interactively →