Study guide

8.2 Built-in Functions

HashiCorp Certified: Terraform Associate 004

Track your progress as you read

Finished sections get a check mark.

1 Introduction & Exam Overview

1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan

2 Infrastructure as Code with Terraform

2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows

3 Terraform Fundamentals: Providers & State

3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State

4 Core Terraform Workflow: Init & Validate

4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration

5 Plan, Apply, Destroy, and Formatting

5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments

6 Resources, Data Sources, and References

6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns

7 Variables, Outputs, and Complex Types

7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set

8 Dynamic Configuration and Dependencies

8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules

9 Conditions, Secrets, and Sensitive Data

9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments

10 Terraform Modules

10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions

11 Terraform State Management

11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations

12 Maintain Infrastructure with Terraform

12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics

13 HCP Terraform

13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations

8.2 Built-in Functions

Key Takeaways Explain This Topic AI

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:

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.

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.

  • 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 / 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.