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
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 nofunctionblock. Provider-defined functions useprovider::local_name::fn(...).md5/sha256are checksums, not password hashes.try/can/coalesce/coalescelistare 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:
- No network round-trip.
max(5, 12, 9)is12even if you have not runapplyand even if no provider is configured.terraform consoleproves it. - Filesystem functions see the disk at the start of the run. Official
filedocs: 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, sofile()cannot wait for a resource that will write a file later in the same apply. Use a data source such asdata.local_fileif 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.
| Need | Function | Trap |
|---|---|---|
| Length of a list, map, or string | length | Empty collections of different types are not always == |
| Map value or a default | lookup(map, key, default) | Missing default is deprecated |
| List value, wrap around | element(list, i) | list[i] does not wrap and errors on overflow |
| Combine maps | merge | Rightmost key wins |
| Nested lists → one list | flatten | Often paired with for_each |
| Unique list items | distinct | Sets already de-duplicate |
| Membership test | contains | First 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."
}
}
| Function | Returns | Use when |
|---|---|---|
try(expr1, expr2, ...) | The first argument that does not error | Optional attributes on decoded data |
can(expr) | true if expr succeeds, else false | Variable validation conditions |
coalesce(v1, v2, ...) | First value that is not null and not "" | String defaults |
coalescelist(l1, l2, ...) | First list that is not empty | List 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/templatefilerequire the file to exist before the run starts.md5/sha256are checksums. They are not how you store passwords.elementwraps; index syntax does not.lookupneeds the default you actually mean.mergeoverwrites from the right.flattenis what you run beforefor_eachon nested lists.tryswallows errors.canreturns a bool.coalesceonly skips null and empty string.
On Terraform 1.12, where is a built-in function such as join(",", var.zones) evaluated?
Which statement about Terraform 1.12 functions is correct?
In terraform console, what does lookup({ a = "ay", b = "bee" }, "c", "what?") return?