Built-in Functions
Terraform ships a library of built-in functions — string, collection, numeric, encoding, filesystem, and date families — and you call them inline anywhere an expression is allowed. There is no syntax for defining your own function in Terraform 1.x: you compose the built-ins, and you push genuinely reusable logic into a module.
Two functions earn their keep on GCP above all the rest. jsonencode builds IAM policies and other JSON arguments with correct escaping, and templatefile renders external files so a startup script or config stays a reviewable file instead of a wall of heredoc inside the .tf. The rest of the library is everyday shaping of labels, names, and lists.
String and Collection Families
The string family covers the everyday reshaping of names and labels: lower, replace, format, join and split, trimspace. The collection family does the same for lists and maps: merge, concat, keys, values, lookup, contains, flatten, distinct. These are the functions you reach for constantly to shape an input into the exact form a resource argument wants.
Encoding Functions
The encoding family — jsonencode and jsondecode, base64encode, yamlencode, urlencode — converts between HCL values and serialized formats. jsonencode is the one you reach for repeatedly on GCP, because IAM policies and many resource arguments want correctly-escaped JSON, and building that JSON by hand is where quoting bugs live.
jsonencode for GCP Policy Documents
Building a google_project_iam_policy document or a custom-role permission set as an HCL object and wrapping it in jsonencode produces valid JSON with correct quoting — instead of a fragile hand-built string that breaks the moment a value contains a quote or a trailing comma lands wrong. The reverse, jsondecode(file(...)), reads an external policy file back into HCL so you can manage it as data.
locals { processor_policy = jsonencode({ bindings = [{ role = "roles/pubsub.publisher" members = ["serviceAccount:${google_service_account.processor.email}"] }] }) # correct escaping, no hand-built quotes }
The object is readable HCL; jsonencode turns it into valid JSON. The service-account email interpolates cleanly, and there is no trailing-comma or quote-escaping bug to chase.
templatefile
templatefile renders an external template file with a set of variables — the clean way to produce a Cloud Run startup config, a cloud-init, or a parameterized SQL or JSON file without cramming multi-line content into the .tf. The template lives as its own reviewable file, and the resource passes it the values it needs.
resource "google_compute_instance" "runner" { # ... metadata_startup_script = templatefile("${path.module}/startup.sh.tftpl", { bucket = google_storage_bucket.raw.name }) }
The startup script stays a file a reviewer can read on its own, and Terraform substitutes bucket at plan time. Inlining the same script as a heredoc would bury it in the resource block and lose the separate review.
try, can, and coalesce
Three functions handle optional and missing data gracefully. can(...) turns an error into a boolean — the reason a regex check works inside a validation block. try(a, b, c) returns the first expression that does not error, useful when an attribute may or may not exist. coalesce and coalescelist return the first non-null argument, the tool for falling back from an optional input to a default. Reaching for these beats letting a missing-key lookup abort the whole plan.
No User-Defined Functions
Terraform 1.x has no way to define your own function in HCL — there is no function "..." {} block, and OpenTofu has none either. When you find yourself repeating a gnarly built-in composition, the answer is to factor the logic into a module or repeat the composition. The closest thing either offers is provider-defined functions — functions a provider registers that you call as provider::name::func — but those come from a provider plugin, not from your own HCL. OpenTofu pushes this further with experimental dynamic providers (Lua, Go) that let a provider compute functions from your config, a divergence from Terraform; still, neither tool lets you name a reusable expression inline.
function blocks. Reusable logic lives in modules; the model this course teaches.provider::name::func) exist in both tools; OpenTofu's only extra is experimental dynamic-provider functions in Lua or Go.Terraform 1.x — only the built-in library, with no user-defined functions. Reusable logic lives in modules or in repeated built-in compositions. This is the model this course teaches and the default for most teams.
OpenTofu — also has no HCL user-defined functions; you cannot define a function in your own configuration. Provider-defined functions (called as provider::name::func) exist in both tools — Terraform added them in 1.8, OpenTofu in 1.7 — so they are not a divergence. OpenTofu's genuine extra is experimental dynamic providers (Lua, Go) that compute functions from your config; either way, the function comes from a provider plugin, not from named expressions in your .tf.
- Hand-building an IAM policy or JSON argument as an interpolated string and shipping invalid JSON the moment a value contains a quote or the trailing comma is wrong —
jsonencodean HCL object instead and let it handle escaping. - Embedding a long startup script or config inline in the
.tfwhentemplatefile("startup.sh.tftpl", {...})would keep it a reviewable separate file. - Expecting to define a reusable function in HCL and discovering there is no such block in Terraform 1.x — and none in OpenTofu either; the logic has to go into a module or be repeated.
- Using
lookup(map, key)without a default and getting an error on a missing key whenlookup(map, key, fallback)ortrywas wanted. - Wrapping a value in
jsondecode(jsonencode(x))to "deep-copy" or normalize it when the original value would have served, adding a round-trip that obscures intent.
- Build every JSON document — IAM policies, structured resource arguments — with
jsonencodeover an HCL object, never as a hand-interpolated string. - Use
templatefilefor any multi-line or parameterized external content so the template stays a reviewable file separate from the.tf. - Reach for
try,coalesce, andcanto handle optional and missing values gracefully instead of letting a lookup error abort the plan. - Do not fight the lack of user-defined functions — neither Terraform 1.x nor OpenTofu lets you define one in HCL, so factor reusable logic into a module or, where a provider offers them, use provider-defined functions.
- Pass
lookupa fallback default for any key that may be absent, so a missing key degrades to the default instead of erroring mid-plan.
Knowledge Check
Why build a GCP IAM policy with jsonencode over an HCL object instead of a hand-interpolated string?
jsonencodehandles quoting and escaping, so a value containing a quote still produces valid JSON; a hand-built string ships malformed JSON- A hand-built string cannot interpolate a service-account email into a
membersarray the way an HCL object can jsonencodeis the only function thegoogle_project_iam_policyresource accepts in its policy argument- It encrypts the rendered policy document with the project KMS key before sending it to the IAM API, so the binding never travels in plaintext
What does templatefile buy over an inline heredoc for a startup script?
- The template stays a separate, reviewable file rendered with variables, instead of a multi-line block buried in the resource
- It runs the rendered script during the plan phase to validate that every command exits cleanly before any instance is created
- It is the only way to pass
varvalues into a Compute Engine startup script - An inline heredoc is capped at 256 lines while
templatefilehas no such limit
Which function turns an erroring expression into a boolean, the reason it is used inside a validation block?
can(...)— it returnstrueorfalseinstead of propagating the errorcoalesce(...)— it returns the first non-null argument from the list you pass ittry(...)— it returns the first of its expressions that evaluates without erroringlookup(...)— it returns the supplied default when a map key is missing
Where do Terraform 1.x and OpenTofu diverge on functions?
- Neither lets you define a function in HCL; both support provider-defined functions, and OpenTofu's only extra is experimental dynamic-provider functions computed in another language
- OpenTofu added user-defined
functionblocks authored in HCL that you can declare in a module and call by name anywhere, while Terraform 1.x is still limited to the fixed set of built-ins - OpenTofu dropped
jsonencodein favour ofyamlencodefor serializing structured values - They ship entirely different built-in function names across the standard library
You got correct