Terraform

Terraform Built-in Functions Quick Guide: length, lookup, and templatefile

2026-04-19
NicheeLab Editorial Team

Terraform's built-in functions are the key to keeping declarations simple while still handling variability. length, lookup, and templatefile in particular show up frequently even at the Associate level.

This article walks through their behavior based on the official spec and consolidates the points that tend to appear on the exam alongside real-world best practices.

Terraform Expression Language and Function Basics

Terraform functions are evaluated as expressions and resolved at plan time wherever possible. When unknown values are involved (for example, from external data sources), functions that cannot defer evaluation may raise errors. Functions are treated as pure functions with no side effects.

Types matter. length works on strings, lists, sets, and maps (including objects), but not on numbers or booleans. lookup retrieves a value from a map and only returns the default when the key is absent (the fact that it returns null when the key exists but the value is null is a popular exam trap). templatefile takes a file path and a variable map and renders the template.

  • Functions are declarative and side-effect free; the result depends only on the inputs
  • null and an empty collection are different. length([]) is 0, length(null) is an error
  • path.module is the module-relative path; pairing it with templatefile is the standard idiom

Conceptual flow of evaluation involving functions

Inputs (variables, data) ---> Expressions (functions)
                                 |         \
                                 |          +--> locals
                                 v
                         Resource arguments ----> Plan ----> Apply

Files: templatefile(path.module/...) --reads--> Template --renders--> String for arguments

Function basics: a minimal example of types and evaluation

variable "names" { type = list(string) default = ["app", "db"] }
variable "tags"  { type = map(string)  default = {} }

locals {
  name_count = length(var.names)                 # 2
  maybe_owner = try(var.tags["owner"], null)     # null if absent
  # path.module is the current module folder
}

length: Measuring lists, maps, and strings

length is the most common foundational function. It returns the length of a string or the number of elements in a collection (list/set/map/tuple/object). On the Associate exam it typically shows up combined with count or for_each.

Watch out for null and unknown values. length(null) raises an invalid-argument error. If unknown values may be involved, guard with coalesce against a same-typed empty value, or pair it with try/can.

  • String length is measured in characters (not bytes)
  • length on a map or object returns the number of keys
  • Null-guard example: length(coalesce(var.tags, tomap({})))

Practical patterns with length

variable "subnets" { type = list(string) default = [] }
variable "tags"    { type = map(string)  default = null }

locals {
  subnet_count = length(var.subnets)                         # 0 or more
  safe_tag_count = length(coalesce(var.tags, tomap({})))     # replace null with empty map
  non_empty_name = length(trimspace(var.name)) > 0           # string emptiness check
}

resource "null_resource" "per_subnet" {
  count = length(var.subnets)
}

# When the value may be unknown
locals {
  # Safely assign with try (use tomap({}) as the second arg to keep types aligned)
  maybe_tags = try(var.tags, tomap({}))
  tag_count  = length(maybe_tags)
}

lookup vs. map subscript access

The standard safe way to fetch a value from a map is lookup(map, key, default). It only returns the default when the key is absent; if the key exists and its value is null, it returns null. This behavior is a popular exam topic.

Direct subscript map["key"] errors out on a missing key. If the key may be missing or unknown, use try(map["key"], default) or lookup. To fall back when the value is an empty string or empty collection, combine with trimspace, length, and a conditional.

  • The general-purpose key-existence check is contains(keys(map), key)
  • Use try(map["k"], null) when you want to branch on whether the value is null
  • Handle empty strings explicitly with patterns like trimspace(x) != ""
ApproachMissing keyWhen value is nullMain use case
lookup(m, "k", "def")Returns defReturns null (not def)Use the default only when the key is missing
m["k"]Error (Invalid index)nullWhen the key is guaranteed to exist
try(m["k"], "def")Returns defReturns defForce the default for missing keys or null values

Safe retrieval examples with lookup / subscript / try

variable "meta" { type = map(string) default = {} }

locals {
  # Use "dev" only on missing key; keep null as null
  env_lookup = lookup(var.meta, "environment", "dev")

  # Force "dev" even on missing key or null
  env_force_default = try(var.meta["environment"], "dev")

  # Key existence check
  has_env = contains(keys(var.meta), "environment")

  # Fall back when empty or whitespace
  owner_raw = try(var.meta["owner"], null)
  owner = (owner_raw != null && trimspace(owner_raw) != "") ? trimspace(owner_raw) : "unknown"
}

templatefile: Rendering Templates from Files

templatefile(path, vars) reads a file and interpolates from the vars map. Building the path relative to path.module keeps it safe when the module is distributed. Inside the template you reference values with the ${var} form and can also use control directives like %{ if }.

When passing complex structures into a template, run them through jsonencode or yamlencode first; that plays nicely with shell scripts and cloud-init.

  • If the template is at a relative path, use ${path.module}/...
  • Only the variables you pass to templatefile are visible inside the template
  • Move large strings into templates and keep the HCL side minimal

templatefile example (externalizing user data)

# main.tf
variable "cluster_name" { type = string }
variable "tags" { type = map(string) default = {} }

locals {
  user_data = templatefile("${path.module}/user_data.sh.tmpl", {
    cluster_name = var.cluster_name,
    tags_json    = jsonencode(var.tags)
  })
}

# user_data.sh.tmpl (example template contents)
#cloud-config
runcmd:
  - echo "Cluster: ${cluster_name}" > /etc/motd
  - echo 'Tags: ${tags_json}' >> /etc/motd
%{ if length(trimspace(cluster_name)) == 0 }
  - echo "WARN: cluster name is empty" >> /var/log/setup.log
%{ endif }

Helper Functions that Pair Well: merge, coalesce, try, and more

In practice you compose functions to build robust expressions. merge merges maps, coalesce returns the first non-null value, and try returns the value of the first expression that succeeds. join/split, compact, distinct, and toset are staples for shaping collections.

The trick is to distinguish unknown, missing, and empty. Choose between null and an empty string or empty collection based on intent, and convert explicitly when necessary.

  • merge(map1, map2, ...) lets later arguments take precedence
  • coalesce(x, y) returns x when x is non-null. Keep types aligned (e.g., tomap({}))
  • compact(list) removes empty strings, distinct removes duplicates, toset drops ordering

Combining helper functions

variable "base_tags" { type = map(string) default = { app = "web" } }
variable "extra_tags" { type = map(string) default = null }
variable "raw_subnets" { type = list(string) default = ["subnet-a", "", "subnet-b", "subnet-a"] }

locals {
  tags = merge(var.base_tags, coalesce(var.extra_tags, tomap({})))

  # Drop empty strings -> dedupe -> turn into a set
  subnets_clean = toset(distinct(compact(var.raw_subnets)))

  # Build a CSV
  subnet_csv = join(",", sort(tolist(subnets_clean)))

  # Use try for fallible retrieval
  env = try(var.extra_tags["environment"], "dev")
}

Common Associate Exam Points and Practical Tips

The exam targets edge cases (null, missing, empty), evaluation timing (plan vs. apply), and the choice between path variables. Knowing the pitfalls keeps you from leaving easy points on the table.

In real work, the playbook is: design expressions to be safe in the face of unknowns, push templating into templatefile, and build tags or labels robustly with merge and lookup/try.

  • lookup falls back to the default only on missing keys; it returns null when the value is null
  • count = length(list) is straightforward; use toset(distinct(list)) when you want for_each to deduplicate
  • Inside a templatefile template, only the variables passed to templatefile are visible
  • Beware mixing up path.module (module-relative) and path.root (root-relative)
  • jsonencode is commonly used when embedding cloud-init configs or policy documents

Fixing common pitfalls

# Bad: errors on missing key
# local.env = var.tags["environment"]

# Good: tolerates missing key and null
locals {
  env = try(var.tags["environment"], "dev")
}

# Bad: passing null straight into length
# local.c = length(var.tags)

# Good: guard with the same type
locals {
  c = length(coalesce(var.tags, tomap({})))
}

Check Your Understanding

Associate

問題 1

You want to fetch the key "team" from the map var.tags, return "platform" only when the key is absent, and leave the result as null when the key exists but its value is null. Which expression best satisfies these requirements?

  1. lookup(var.tags, "team", "platform")
  2. coalesce(var.tags["team"], "platform")
  3. try(var.tags["team"], null) != null ? var.tags["team"] : "platform"
  4. contains(keys(var.tags), "team") ? "platform" : var.tags["team"]

正解: A

lookup(map, key, default) returns the default only when the key is missing and returns null when the key exists with a null value. B errors on a missing key, C switches to the default when the value is null (different from the requirement), and D has its condition reversed and does not meet the requirement.

Frequently Asked Questions

Can I reference Terraform variables like var.x directly inside a templatefile template?

No. Only the keys of the map you pass to templatefile are visible inside the template. Pass every value you need explicitly via the second argument.

How does length handle multibyte characters when measuring string length?

It counts characters (not bytes), so length("あ") returns 1.

When should I use lookup versus try?

Use lookup when you only want to fall back to the default on missing keys. Use try when you want any exceptional situation (missing key, null, etc.) to collapse to the default. In practice pick based on requirements and keep null vs. empty clearly distinguished.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.