8.2 Built-in Functions

Key Takeaways

  • Built-in functions run in Terraform's language engine on the machine executing Terraform — they are not provider CRUD calls and they do not run in the remote API
  • OSS Terraform HCL has no user-defined function block; provider-defined functions exist and are called as provider::local_name::function_name(...)
  • High-yield 004 families are string, collection, numeric, encoding, filesystem, hash, and type conversion — know join, split, replace, format, lookup, element, merge, flatten, jsonencode, file, and templatefile by example
  • md5 and sha256 are checksums of UTF-8 text; they are not a password-hashing scheme, and collision concerns make md5 a poor security choice
  • try returns the first argument that does not error; can turns success into a boolean; coalesce / coalescelist pick the first non-empty value
Last updated: August 2026

8.2 Built-in Functions

Quick Answer: Call functions as name(arg1, arg2). They run in Terraform's language engine on the machine that is running Terraform — the CLI laptop or the HCP Terraform worker — not inside the provider's create/update/delete. OSS HCL has no function block. Provider-defined functions use provider::local_name::fn(...). md5 / sha256 are checksums, not password hashes. try / can / coalesce / coalescelist are the safe-fallback family.

This section finishes objective 4e — write dynamic configuration using expressions and functions — against Terraform 1.12. Official catalog: Built-in Functions (unversioned: Functions). Experiment with terraform console; every official function page shows console output.

Evaluated locally — not in the provider

A function call is an expression. Terraform's language engine evaluates it while it is building the graph and the plan. The AWS, Azure, or Google provider does not receive join(",", var.zones) as an RPC. By the time the provider sees availability_zone = "us-east-1a", the function has already run.

That locality has two exam consequences:

  1. No network round-trip. max(5, 12, 9) is 12 even if you have not run apply and even if no provider is configured. terraform console proves it.
  2. Filesystem functions see the disk at the start of the run. Official file docs: the function can be used only with files that already exist on disk at the beginning of a Terraform run. Functions do not participate in the dependency graph, so file() cannot wait for a resource that will write a file later in the same apply. Use a data source such as data.local_file if you truly need a graph edge.

No user-defined function in OSS HCL

Official 1.12 wording: you cannot define your own functions in the Terraform configuration language, but you can develop providers that expose functions. That is the 004 line. There is no function "slugify" { ... } block in community Terraform.

Terraform 1.8+ (and therefore 1.12) added provider-defined functions. Call them with a three-part name that starts with provider::, then the local name from required_providers, then the function:

locals {
  encoded = provider::terraform::encode_tfvars({
    example = "Hello!"
  })
}

Know that they exist and how they are spelled. The exam still expects the built-in catalog below, not a custom HCL function.

String functions — worked examples

join(",", ["a", "b", "c"])            # "a,b,c"
split(",", "a,b,c")                   # ["a", "b", "c"]
replace("hello-world", "-", "_")      # "hello_world"
format("web-%02d", 7)                 # "web-07"
lower("Prod")                         # "prod"
upper("prod")                         # "PROD"
trimspace("  east  ")                 # "east"

join concatenates a list of strings with a separator. split is the inverse and always returns a list. replace can take a literal substring or a regular expression wrapped as /pattern/. format is printf-style (%s, %d, %02d). lower / upper change case. trimspace strips leading and trailing Unicode space — the usual cleanup before you compare a tag or a name.

Collection functions

length(["a", "b", "c"])                              # 3  (also works on maps and strings)
lookup({ a = "ay", b = "bee" }, "c", "what?")        # "what?"
element(["a", "b", "c"], 3)                          # "a"  (index wraps: 3 % 3)
keys({ app = "t3.micro", job = "t3.small" })         # ["app", "job"]  (lexicographic)
values({ app = "t3.micro", job = "t3.small" })       # ["t3.micro", "t3.small"]
merge({ a = 1, b = 2 }, { b = 9, c = 3 })            # { a = 1, b = 9, c = 3 }  (later wins)
flatten([["a", "b"], [], ["c"]])                     # ["a", "b", "c"]
distinct(["a", "b", "a"])                            # ["a", "b"]
chunklist(["a", "b", "c", "d"], 2)                   # [["a", "b"], ["c", "d"]]
contains(["STAGE", "PROD"], var.environment)         # true or false

lookup(map, key, default) is the safe map read; omitting the default is deprecated because map[key] already does that. element(list, index) is 0-based and wraps (element(list, 3) on a 3-element list is the first item). Prefer list[index] unless you want that wrap. keys / values turn a map into lists. merge combines maps or objects; later arguments overwrite earlier keys. flatten replaces nested lists with a single sequence — the usual prelude to for_each over a nested structure. distinct drops duplicate list elements. chunklist splits a list into fixed-size pieces. contains tests membership in a list, tuple, or set.

NeedFunctionTrap
Length of a list, map, or stringlengthEmpty collections of different types are not always ==
Map value or a defaultlookup(map, key, default)Missing default is deprecated
List value, wrap aroundelement(list, i)list[i] does not wrap and errors on overflow
Combine mapsmergeRightmost key wins
Nested lists → one listflattenOften paired with for_each
Unique list itemsdistinctSets already de-duplicate
Membership testcontainsFirst argument is the collection

Numeric, encoding, filesystem, hash, types

Numeric. max(5, 12, 9) is 12. min is the smallest. ceil(5.1) is 6. floor(5.9) is 5. These accept numbers, not strings that merely look like numbers — convert first with tonumber.

Encoding. jsonencode(value) produces a JSON string; it is the standard way to build an IAM policy or a container definition in HCL. jsondecode(string) is the inverse. yamlencode(value) writes YAML 1.2 block syntax. Use jsonencode when a provider argument wants a JSON document and you would rather keep the structure in HCL.

Filesystem. file("${path.module}/policy.json") reads UTF-8 text that already exists. Invalid UTF-8 errors. templatefile("${path.module}/user_data.tftpl", { name = var.name }) reads the same kind of file and renders it with Terraform's string-template syntax (${ ... }). The second argument must be an object whose keys become template variables. Recursive templatefile calls are not allowed. Paths are relative to the current working directory unless you use path.module.

Hash. md5("hello world") is 5eb63bbbe01eeed093cb22bb8f5acdc3. sha256 is the stronger checksum. Official md5 docs warn that collision attacks exist (RFC 6151) and tell you to think before using it for anything security-sensitive. These functions hash a UTF-8 string and return lowercase hex. They are not a password store. Do not put md5(var.password) on a user resource and call it hashing. Terraform's bcrypt function exists for password hashes and is impure (new hash every call), which is why it cannot feed for_each.

Type conversion. tostring(12), tonumber("12"), tolist(set), toset(list), tomap({ ... }). toset(["b", "a", "b"]) drops the duplicate and forgets order. That conversion is exactly how you prepare a list for for_each. The old list() and map() functions are deprecated after 0.12 — use tolist / tomap.

locals {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = [aws_s3_bucket.logs.arn]
    }]
  })
  user_data = templatefile("${path.module}/boot.tftpl", {
    greeting = var.project
  })
  fingerprint = sha256(local.policy)   # checksum of the document, not a password
}

try, can, coalesce, coalescelist

These four show up whenever decoded JSON or YAML might be missing a key.

locals {
  raw = yamldecode(file("${path.module}/service.yaml"))
  name   = tostring(try(local.raw.name, null))
  groups = try(local.raw.groups, [])
  title  = coalesce(var.override_title, var.default_title, "untitled")
  cidrs  = coalescelist(var.public_cidrs, var.fallback_cidrs, ["10.0.0.0/16"])
}

variable "timestamp" {
  type = string
  validation {
    condition     = can(formatdate("", var.timestamp))
    error_message = "timestamp must be a valid RFC 3339 value."
  }
}
FunctionReturnsUse when
try(expr1, expr2, ...)The first argument that does not errorOptional attributes on decoded data
can(expr)true if expr succeeds, else falseVariable validation conditions
coalesce(v1, v2, ...)First value that is not null and not ""String defaults
coalescelist(l1, l2, ...)First list that is not emptyList defaults

Official can docs: for most fallback-value cases, prefer try because it is shorter. can shines in validation blocks, where you need a boolean, not the value itself. coalesce does not catch errors; a missing attribute still fails. That is why decoded YAML uses try, not coalesce, for optional keys.

004 traps for functions

  • Functions run in Terraform, not in the provider, and not during init.
  • There is no user-defined function in OSS HCL. Provider-defined functions use provider::name::fn.
  • file / templatefile require the file to exist before the run starts.
  • md5 / sha256 are checksums. They are not how you store passwords.
  • element wraps; index syntax does not. lookup needs the default you actually mean.
  • merge overwrites from the right. flatten is what you run before for_each on nested lists.
  • try swallows errors. can returns a bool. coalesce only skips null and empty string.
Test Your Knowledge

On Terraform 1.12, where is a built-in function such as join(",", var.zones) evaluated?

A
B
C
D
Test Your Knowledge

Which statement about Terraform 1.12 functions is correct?

A
B
C
D
Test Your Knowledge

In terraform console, what does lookup({ a = "ay", b = "bee" }, "c", "what?") return?

A
B
C
D