Functions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Functions are how the Terraform language computes things it has no operator for: building a name from parts, deriving subnets from a network range, reading a file, hashing a value, turning a data structure into JSON, or recovering from a missing attribute. You cannot define your own functions in a configuration (the only way to add one is to write a provider that exposes it), so the built-in library, plus whatever the providers in use contribute, is the whole vocabulary. This page describes that library as documented for Terraform 1.17 and OpenTofu 1.13: how a call is written, every category of functions with Bookshelf examples, the regular expression, file and template functions, the functions that handle sensitive and ephemeral values, the functions and symbol that the built-in terraform provider offers, which functions are pure and which are not, how to experiment with them in terraform console, and what changed in recent releases.

It follows Types and Expressions, which introduces the call syntax, conversion rules and string templates, and it is used by every later page that builds values from other values, in particular Variables, Outputs and Locals, Meta-Arguments: count, for_each and depends_on and Lifecycle, Validation and Checks. The treatment of secrets that goes beyond the functions on this page is in Sensitive Data, Ephemeral Values and Write-Only Arguments.

All examples use the Bookshelf scenario (catalog-api, reviews-api, web, PostgreSQL, Redis, a covers bucket). Unless an example says otherwise it needs no provider and no cloud account: the functions are evaluated by Terraform itself, so the configurations below run with terraform init and terraform apply on a laptop.

How a function call works

A call is the function name, an opening parenthesis, comma-separated arguments and a closing parenthesis. It is an expression, so it can appear wherever an expression is accepted: in an argument, a local value, an output, a for expression, a condition or inside another call. The name is always a plain identifier; there is no method syntax and no way to pass a function around as a value.

terraform {
  required_version = ">= 1.15"
}

locals {
  # A call can be nested in another call and mixed with operators and conditionals.
  replicas = max(2, ceil(450 / 100))
  label    = upper(substr("catalog-api", 0, 7))
}

output "replicas" {
  value = local.replicas
}

output "label" {
  value = local.label
}

Four general rules explain most surprises:

  • Argument expansion. A list, set or tuple can be passed as separate arguments by following it with ... (three ASCII periods) in the call, but only in a function call: max([12, 54, 3]...) is 54.

  • Types are converted where possible. Arguments are converted to the parameter types the function declares. A function that takes number accepts the string three in quotes, and coalesce(1, "hello") returns a string, not a number. The conversion rules are those of Types and Expressions.

  • Marks travel with values. Passing a sensitive value to a function marks the result sensitive, and an unknown argument usually makes the result unknown (shown as (known after apply)). The functions that deliberately remove or inspect a mark are described in Functions for sensitive and ephemeral values.

  • Support depends on the file type. Standard .tf and .tf.json files accept every function. Other configuration files, such as the Terraform Stacks .tfcomponent.hcl and .tfdeploy.hcl files, support only a subset; the function overview lists which functions work in which file type.

Functions contributed by providers are called with a prefixed name, provider::<local_name>::<function>(...), where the local name is the key under required_providers. They are covered in Provider-defined functions and the built-in terraform provider.

The function library by category

The tables below list every function in the Terraform 1.17 documentation by category, with the signature for the ones used in the examples. Where OpenTofu has a function that Terraform lacks, the table says so; the complete list of differences is in Terraform and OpenTofu differences.

Numeric functions

Function Purpose

abs

Absolute value of a number.

ceil, floor

Round up or down to a whole number.

log

Logarithm of a number in a given base. Since Terraform 1.17 it raises an error instead of returning infinity.

max, min

Greatest or smallest of one or more numbers; expand a list with ....

parseint

Parse a string as an integer in a base between 2 and 62: parseint("FF", 16) is 255.

pow

First argument raised to the power of the second. Also an error on an infinite result since 1.17.

signum

-1, 0 or 1 according to the sign of the number.

The arithmetic operators cover addition, subtraction, multiplication, division and remainder; the functions above cover rounding, extremes and exponents. The typical use in infrastructure code is sizing: a number of replicas, a disk size or a count of subnets is computed from a load estimate and then clamped to a minimum.

variable "expected_rps" {
  type        = number
  description = "Expected peak requests per second for catalog-api."
  default     = 450
}

variable "rps_per_replica" {
  type        = number
  description = "Requests per second one catalog-api replica can serve."
  default     = 100
}

variable "min_replicas" {
  type    = number
  default = 2
}

locals {
  # 450 / 100 = 4.5, ceil gives 5, and the floor of two replicas never applies here.
  replicas_needed = max(var.min_replicas, ceil(var.expected_rps / var.rps_per_replica))

  # Gibibytes to bytes for an argument that expects bytes: 2^30.
  one_gib = pow(2, 30)
}

Some results from terraform console:

> max(5, 12, 9)
12
> floor(-1.5)
-2
> parseint("FF", 16)
255
> signum(-13)
-1
> pow(2, 30)
1073741824

String functions

Function Purpose

chomp

Remove trailing newline characters.

endswith, startswith, strcontains

Boolean tests on a string: suffix, prefix, substring.

format, formatlist

printf-style formatting of one string, or of one string per list element.

indent

Add a number of spaces to the start of every line except the first.

join, split

Concatenate a list into a string with a separator, or divide a string at a separator.

lower, upper, title

Change the case of letters; title capitalizes the first letter of each word.

regex, regexall

Regular expression matching, see Regular expressions.

replace

Replace each occurrence of a substring. A substring wrapped in forward slashes is treated as a regular expression.

strrev

Reverse the characters of a string.

substr

Extract a substring by offset and maximum length, counted in Unicode characters. A negative offset counts from the end, and a length of -1 means the rest of the string.

templatestring

Render a template held in a named string, see templatefile and templatestring.

trim, trimprefix, trimsuffix, trimspace

Remove a set of characters, a prefix, a suffix or whitespace from the ends of a string.

format is the workhorse. Its specification supports the verbs %s, %d, %f, %e, %g, %t, %q, %x, %X, %b, %o and %%, a width and a precision, flags such as 0 (pad with zeros) and - (left-align), an explicit argument position such as %[1]s, and %#v, which prints any value as JSON and is handy in validation error messages. For a single substitution, an interpolation sequence in a template is usually more readable than format("Hello, %s!", var.name); reach for format when you need padding, precision or several arguments.

variable "environment" {
  type    = string
  default = "dev"
}

locals {
  prefix = format("bookshelf-%s", var.environment)

  # Zero-padded ordinal: bookshelf-dev-catalog-api-01
  first_catalog_name = format("%s-catalog-api-%02d", local.prefix, 1)

  # One DNS name per service.
  dns_names = formatlist("%s.bookshelf.example.com", ["catalog", "reviews", "www"])

  # Normalize a Terraform identifier into a DNS label.
  dns_label = replace("catalog_api", "_", "-")

  # Strip a leading v from a version tag.
  image_version = trimprefix("v1.4.2", "v")

  # Parse the major version out of the tag.
  major_version = split(".", trimprefix("v1.4.2", "v"))[0]
}
> format("%s-catalog-api-%02d", "bookshelf-dev", 1)
"bookshelf-dev-catalog-api-01"
> join(", ", formatlist("%s.bookshelf.example.com", ["catalog", "reviews", "www"]))
"catalog.bookshelf.example.com, reviews.bookshelf.example.com, www.bookshelf.example.com"
> replace("catalog_api", "_", "-")
"catalog-api"
> trimprefix("v1.4.2", "v")
"1.4.2"
> substr("bookshelf-prod", 0, 9)
"bookshelf"
> startswith("reviews-api", "reviews")
true
> join("-", ["bookshelf", "dev", "web"])
"bookshelf-dev-web"

Two details cause most string-function bugs. replace with a plain substring replaces every occurrence, not only the first. And format with a list argument does not iterate: format("%s", ["a", "b"]) is an error, while formatlist produces one string per element and accepts scalars that it repeats for every element.

Collection functions

Function Purpose

alltrue, anytrue

Whether all, or any, of the elements of a list are true.

chunklist

Split a list into fixed-size chunks.

coalesce, coalescelist

First argument that is not null or an empty string; first list argument that is not empty.

compact

Remove null and empty-string elements from a list of strings.

concat

Combine two or more lists into one.

contains

Whether a list, tuple or set contains a value. Since Terraform 1.16 it can test for null.

distinct

Remove duplicates from a list, keeping the first occurrence.

element

One element of a list, with wrap-around for an index beyond the end and, since Terraform 1.10, negative indices.

flatten

Replace nested lists by their elements, recursively.

index

Position of the first element equal to a value.

keys, values

The keys, or the values, of a map or object, in lexical order of the keys.

length

Number of elements of a collection, or characters of a string.

lookup

Value for a key of a map, or a default when the key is absent.

matchkeys

Subset of one list chosen by matching the elements of another.

merge

Combine maps or objects; the later argument wins on a duplicate key. Since 1.16 it accepts null objects.

one

The only element of a list, set or tuple of zero or one elements; null when empty, an error when larger.

range

A list of numbers from a start to a limit by a step.

reverse

The same sequence in reverse order.

setintersection, setproduct, setsubtract, setunion

Set operations; setproduct is the Cartesian product.

slice

Consecutive elements of a list, by start and end index.

sort

Strings in lexicographic order.

sum

Sum of a list or set of numbers.

transpose

Turn a map of lists into a map of keys by element.

zipmap

Build a map from a list of keys and a list of values.

The documentation still lists list and map as deprecated since Terraform 0.12. Use tolist and tomap (or the literal syntax [...] and {...}) instead; the OpenTofu function listing has no list or map at all.

Collections in the language are immutable values, so every function here returns a new collection. The ones that matter most for infrastructure code are merge (tags), lookup and coalesce (defaults), flatten and setproduct (reshaping data into the single flat collection that for_each needs) and zipmap (pairing names with computed values).

locals {
  services = {
    catalog-api = { port = 8080, replicas = 2 }
    reviews-api = { port = 8081, replicas = 1 }
    web         = { port = 80, replicas = 3 }
  }

  common_tags = {
    project     = "bookshelf"
    environment = var.environment
    managed_by  = "terraform"
  }

  # Later arguments win: the service tag is added, and a team could override environment here.
  catalog_tags = merge(local.common_tags, { service = "catalog-api" })

  # keys() and values() order follow the sorted keys: catalog-api, reviews-api, web.
  service_names  = keys(local.services)
  total_replicas = sum([for svc in values(local.services) : svc.replicas])

  # One element per container, flattened to a single list that for_each can consume as a set.
  container_names = flatten([
    for name, svc in local.services : [
      for n in range(svc.replicas) : format("%s-%02d", name, n + 1)
    ]
  ])

  # A default for a service without a custom port.
  cache_port = lookup({ for name, svc in local.services : name => svc.port }, "redis", 6379)

  # Every service in every environment: four combinations, order preserved.
  fqdns = [
    for pair in setproduct(["dev", "prod"], ["catalog", "reviews"]) :
    "${pair[1]}.${pair[0]}.bookshelf.example.com"
  ]
}

The snippet assumes the environment variable of the previous example. With that configuration in the working directory, terraform console evaluates the locals:

> local.total_replicas
6
> join(", ", local.service_names)
"catalog-api, reviews-api, web"
> local.container_names
[
  "catalog-api-01",
  "catalog-api-02",
  "reviews-api-01",
  "web-01",
  "web-02",
  "web-03",
]
> local.cache_port
6379
> local.fqdns
[
  "catalog.dev.bookshelf.example.com",
  "reviews.dev.bookshelf.example.com",
  "catalog.prod.bookshelf.example.com",
  "reviews.prod.bookshelf.example.com",
]

A few more collection results, taken from the function pages:

> element(["a", "b", "c"], 3)
"a"
> element(["a", "b", "c"], -1)
"c"
> zipmap(["a", "b"], [1, 2])
{
  "a" = 1
  "b" = 2
}
> coalesce("", "b")
"b"
> merge({a="b", c="d"}, {e="f", c="z"})
{
  "a" = "b"
  "c" = "z"
  "e" = "f"
}
> flatten([["a", "b"], [], ["c"]])
[
  "a",
  "b",
  "c",
]

Four cautions apply to this category:

  • lookup(map, key, default) is documented with a required default; the two-argument form is deprecated because it is equivalent to the index syntax map[key].

  • element is only needed for its wrap-around behavior; use list[index] otherwise.

  • coalesce treats an empty string as missing, and all its arguments must convert to one type: coalesce({}, "hello") is an error.

  • one is the idiomatic way to read a value from a resource created with count = condition ? 1 : 0, for example one(terraform_data.redis[*].output), which is the single value or null.

Encoding functions

Function Purpose

base64encode, base64decode

Standard Base64 of the UTF-8 bytes of a string, and back.

base64gzip

Compress a string with gzip and encode it as Base64.

csvdecode

Parse CSV text with a header row into a list of maps.

jsonencode, jsondecode

Convert a value to a JSON string, or parse JSON into a value.

textencodebase64, textdecodebase64

Convert text to and from Base64 in another character encoding, such as UTF-16LE.

urlencode

Percent-encode a string for use in a URL query.

yamlencode, yamldecode

Convert a value to a YAML 1.2 string, or parse YAML into a value.

jsonencode and yamlencode are the functions to prefer whenever the goal is a JSON or YAML document: they handle quoting, escaping and delimiters, they take ordinary Terraform values, and, for JSON, Terraform can show a structural diff in the plan, whereas it treats block-style YAML as one multi-line string. The documentation recommends JSON even when the remote system also accepts YAML, unless humans will read or edit the result. Neither round-trips exactly, because the target formats cannot represent every Terraform type.

locals {
  # An IAM-style policy for the covers bucket. Encoding a value avoids hand-written braces and quotes.
  covers_read_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = "arn:aws:s3:::bookshelf-covers-example/*"
    }]
  })

  # A Kubernetes-style ConfigMap payload for catalog-api, as YAML for people to read.
  catalog_settings_yaml = yamlencode({
    server   = { port = 8080 }
    database = { host = "postgres.bookshelf.internal", name = "catalog" }
  })

  # A small CSV roster decoded into a list of objects.
  teams = csvdecode("name,owner\ncatalog,alice\nreviews,bob")
}
> jsonencode({name = "catalog-api", port = 8080})
"{\"name\":\"catalog-api\",\"port\":8080}"
> jsondecode("{\"replicas\": 2}").replicas
2
> base64encode("catalog")
"Y2F0YWxvZw=="
> base64decode("Y2F0YWxvZw==")
"catalog"
> urlencode("Hello World!")
"Hello+World%21"
> textencodebase64("Hello World", "UTF-16LE")
"SABlAGwAbABvACAAVwBvAHIAbABkAA=="

base64gzip is used to fit a larger document, such as a cloud-init payload, into an argument with a size limit. OpenTofu adds the inverse, base64gunzip, and urldecode (both since OpenTofu 1.7); Terraform has neither. In OpenTofu 1.13 the exact bytes that base64gzip produces changed to a different but equivalent encoding, so a resource argument that stores its result can show a diff after the upgrade.

Filesystem functions

Function Purpose

abspath

Convert a path to an absolute path.

basename, dirname

The last element of a path, or the path without its last element.

file

Content of a file as a UTF-8 string.

filebase64

Content of a file as Base64, which is the way to read binary files.

fileexists

Whether a regular file exists at a path.

fileset

The set of file names matching a glob pattern below a path.

pathexpand

Replace a leading ~ with the home directory.

templatefile

Render a template file with variables, see templatefile and templatestring.

Three symbols belong with these functions because they supply the path argument: path.module (the directory of the module whose expression is being evaluated), path.root (the directory of the root module) and path.cwd (the directory in which Terraform was started). Almost every file function should start from path.module, so that the module keeps working when it is called from somewhere else.

The most important rule is that the file functions read the disk while Terraform loads and plans the configuration. Functions do not take part in the dependency graph, so they can only read files that already exist when the run starts. A file generated during the run, for example by a local_file resource, cannot be read with file; the local_file data source reads a file while respecting resource dependencies, but generating files and reading them back in the same run is a design smell. Content that is not valid UTF-8 makes file fail, so use filebase64 for binary content.

fileset returns a set of paths relative to its first argument, always with forward slashes. The patterns are * (any characters but a separator), ** (any characters including separators, only as a whole path component), ?, {a,b} alternatives and [class] character classes. A common use is one resource per matched file:

terraform {
  required_version = ">= 1.15"
}

# Expected layout next to this file:
#   nginx/web.conf
#   nginx/catalog.conf
#   covers/seed/cover-001.jpg  (binary files)
locals {
  nginx_files = fileset(path.module, "nginx/*.conf")
  cover_seeds = fileset("${path.module}/covers/seed", "*.jpg")
}

resource "terraform_data" "nginx_config" {
  for_each = local.nginx_files

  # Changing a file changes its hash and therefore replaces the resource.
  triggers_replace = filesha256("${path.module}/${each.value}")
}

output "nginx_file_count" {
  value = length(local.nginx_files)
}

Terraform 1.13 started to check that the filesystem functions return the same result during apply as they did during plan, to catch a file that changed in between, and Terraform 1.14 relaxed the check for filesystem functions evaluated in provider configuration, where a different result is acceptable.

templatefile and templatestring

Both functions render the template language that is also used inside quoted strings and heredocs: interpolation sequences such as ${name} and directives such as %{ for item in items }...%{ endfor } and %{ if cond }...%{ endif }. The template language itself is described in Types and Expressions; here is how the two functions deliver a template.

templatefile(path, vars) reads a file and renders it with the variables of the vars object. Each key of the object becomes a variable inside the template; names must start with a letter and contain only letters, digits and underscores. The template may call any function except templatefile itself (recursion is not allowed). The recommended file extension is .tftpl: Terraform does not require it, but editors use it to pick better highlighting. Like file, templatefile reads UTF-8 text, can only read files that exist when the run starts, and returns an error for invalid UTF-8.

Create a template for the part of the web reverse-proxy configuration that lists the catalog backends:

mkdir -p templates
cat > templates/catalog-upstream.conf.tftpl <<'EOF'
%{ for addr in catalog_addrs ~}
server ${addr}:${port};
%{ endfor ~}
EOF

Render it:

locals {
  catalog_upstream = templatefile("${path.module}/templates/catalog-upstream.conf.tftpl", {
    catalog_addrs = ["10.0.1.10", "10.0.1.11"]
    port          = 8080
  })
}

The value of local.catalog_upstream is this text, one line for each address, each ending with a newline:

server 10.0.1.10:8080;
server 10.0.1.11:8080;

The ~ markers strip the whitespace around the directives, including the indentation of the following line. That is why the body has no leading spaces: an indented server line would lose its indentation. Use ~ on the side where you want the whitespace removed and expect the final newline of the template to remain.

templatestring(template, vars) was added in Terraform 1.9. It renders a template that is not in a file but in a string, typically one fetched from a remote location or built in a local value, and its first argument must be a reference to an object defined in the module, such as a local value or a data source attribute. The documentation states that the template expression cannot be given directly as the first argument; that is deliberate, because a template literal in the call would simply be a normal string template. The second argument is the object of variables.

locals {
  # $$ keeps the sequence literal, so it is rendered later by templatestring and not now.
  banner_template = "Welcome to $${shop}, running in $${env}"

  banner = templatestring(local.banner_template, {
    shop = "Bookshelf"
    env  = var.environment
  })
}
> local.banner
"Welcome to Bookshelf, running in dev"

Building a template dynamically in a local value and then rendering it is a last resort: the result is hard to understand, it is sensitive to unexpected input and, because built-in functions can read the local filesystem, an untrusted template could include data from files on the machine running Terraform. Prefer templatefile for templates stored in your own repository.

The legacy template_file data source

Tutorials written for Terraform 0.11 and earlier render templates with the template_file data source of the hashicorp/template provider, passing a template string (usually the result of file) and a vars map and then reading the rendered attribute. Since Terraform 0.12 the built-in templatefile function does the same without a provider, and the provider’s own documentation says to use the function instead unless you are on 0.11 or older. The provider repository is archived, no new releases are published, and its builds do not cover newer platforms, so configurations that still use it should be migrated: replace the data source by a templatefile call, move vars into the second argument and read the result directly instead of through .rendered.

# Before (legacy, do not copy):
#   data "template_file" "init" {
#     template = file("${path.module}/templates/init.tpl")
#     vars     = { catalog_url = "http://catalog-api:8080" }
#   }
#   user_data = data.template_file.init.rendered

# After:
locals {
  init_script = templatefile("${path.module}/templates/init.tftpl", {
    catalog_url = "http://catalog-api:8080"
  })
}

When not to template

A template is a tool for text whose shape is mostly fixed: a shell script, an nginx or HAProxy configuration, a cloud-init document, an email body. It is the wrong tool for structured data.

  • JSON and YAML. Do not write braces, quotes, commas and indentation in a template and fill holes in them. Build the value with ordinary expressions and encode it with jsonencode or yamlencode. A template file can still be used as a thin wrapper: a template whose entire content is a single call such as ${jsonencode({ backends = [for addr in ip_addrs : "${addr}:${port}"] })} produces valid JSON without any escaping, though the same call inline in a local value avoids the file.

  • Logic. A template with nested %{ for } and %{ if } directives is a program in a poor language with no tests. When there is real branching, compute the data in Terraform, pass the final lists and strings to the template and keep the template to simple loops and substitutions.

  • Secrets. A rendered template is a plain string value. If a secret flows into vars, the rendered text becomes sensitive and, if it is passed to a resource argument, ends up in the state. Prefer to fetch secrets at run time from the machine (a secret manager call in the user-data script) instead of baking them into rendered files.

  • Generated files. Neither function can read a file produced during the run.

locals {
  # Prefer this: the data is a value, and the encoder takes care of escaping and delimiters.
  catalog_config_json = jsonencode({
    listen   = ":8080"
    upstream = [for addr in ["10.0.1.10", "10.0.1.11"] : "${addr}:8080"]
  })
}

Date and time functions

Function Purpose

formatdate

Convert an RFC 3339 timestamp to another text format.

plantimestamp

The time at which the plan was made, as an RFC 3339 string (Terraform 1.5 and later).

timeadd

Add a duration such as 10m, 1h30m or -2h5m to a timestamp.

timecmp

Compare two timestamps: -1, 0 or 1.

timestamp

The current UTC time, as an RFC 3339 string.

Timestamps in the language are strings in RFC 3339 form, such as 2026-10-23T09:30:00Z. formatdate turns one into a machine-oriented format: YYYY, MM, DD, hh, mm, ss, EEE and ZZZ among others. It is not locale-aware and produces English month and day names only. timeadd uses the duration units ns, us, ms, s, m and h; there is no unit for days, so thirty days is 720h. timecmp takes the UTC offsets of its arguments into account.

> formatdate("YYYY-MM-DD", "2026-10-23T09:30:00Z")
"2026-10-23"
> timeadd("2026-10-23T09:30:00Z", "720h")
"2026-11-22T09:30:00Z"
> timecmp("2026-10-23T09:30:00Z", "2026-11-22T09:30:00Z")
-1

timestamp versus plantimestamp

The two current-time functions differ in when they are evaluated, and that decides where each is usable.

  • timestamp() changes every second. Terraform cannot predict it during planning, so the value is unknown in the plan and is taken when the plan is applied. Used directly in a resource argument it makes every run show a diff, so the documentation recommends against it there; the exceptions are a lifecycle ignore_changes to keep the first value, and a condition evaluated at apply time.

  • plantimestamp() returns the time of the plan and is fixed for the whole plan operation, so it can be used in places that need a known value during planning and validation, which makes it the right tool for time-based checks. It is not available in terraform console, and the value changes on every new plan. During a refresh-only plan it may be recalculated but is not passed to resources, so compare it only against timestamps that providers export, not against timestamps your own configuration generates.

A check block that warns before the Bookshelf certificate expires uses plantimestamp, because a check runs at the end of every plan and apply and a failing check is a warning that does not stop the run:

terraform {
  required_version = ">= 1.15"

  required_providers {
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

check "bookshelf_certificate" {
  data "tls_certificate" "web" {
    url = "https://www.bookshelf.example.com"
  }

  assert {
    # Fail the check when less than 30 days (720h) of validity remain.
    condition     = timecmp(plantimestamp(), timeadd(data.tls_certificate.web.certificates[0].not_after, "-720h")) < 0
    error_message = "The www.bookshelf.example.com certificate expires in less than 30 days."
  }
}

The check block itself, the scoped data source and what a failed check does to the run are explained in Lifecycle, Validation and Checks.

When the goal is a value that is recorded once and then stays stable, such as the creation date of a resource, use the time_static resource of the hashicorp/time provider instead of any function: it stores the time in the state and does not change on later runs.

terraform {
  required_version = ">= 1.15"

  required_providers {
    time = {
      source  = "hashicorp/time"
      version = "~> 0.14"
    }
  }
}

resource "time_static" "catalog_created" {}

output "catalog_created" {
  # Stable across runs, unlike timestamp().
  value = formatdate("YYYY-MM-DD", time_static.catalog_created.rfc3339)
}

Hash and crypto functions

Function Purpose

md5, sha1, sha256, sha512

Hexadecimal digest of a string.

base64sha256, base64sha512

Digest encoded as Base64 instead of hexadecimal. This is not the same as encoding the hexadecimal digest.

filemd5, filesha1, filesha256, filesha512, filebase64sha256, filebase64sha512

The same digests computed over the contents of a file, including binary files.

bcrypt

Blowfish-based password hash in the Modular Crypt Format, with an optional cost that defaults to 10.

rsadecrypt

Decrypt Base64 ciphertext with a PEM-encoded RSA private key.

uuid, uuidv5

A random UUID-format string, or a name-based UUID for a namespace and a name.

The file variants exist because file accepts only UTF-8 text: filebase64sha256 hashes arbitrary bytes. Hashes are used as change detectors (a resource is replaced when the hash of a source file changes), as stable identifiers derived from content, and for integrity values that APIs expect, such as the base64 SHA-256 of a function package.

locals {
  # Deterministic: the same input always gives the same fingerprint.
  nginx_config_hash = filesha256("${path.module}/nginx/web.conf")

  # A short, stable suffix derived from a name, for globally unique bucket names.
  covers_bucket_suffix = substr(sha256("bookshelf-covers-${var.environment}"), 0, 8)
}

resource "terraform_data" "web_config" {
  # Replace when the configuration file content changes.
  triggers_replace = local.nginx_config_hash
}
> base64sha256("hello world")
"uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="

Three warnings are worth keeping in mind. md5 and sha1 are fine as change detectors but are not secure hashes, so use sha256 or sha512 when integrity matters. bcrypt includes a random salt, so every call returns a different string for the same input and using it in a resource argument creates a diff on every plan; the documentation recommends it only in provisioner blocks or in data sources whose results feed provisioners. And uuid is also random on each call: it produces a spurious diff in resource arguments and is rarely the right choice, because the random provider keeps the generated value in the state (random_id offers equivalent randomness). Use uuidv5 when you need a stable identifier derived from a name.

IP network functions

Function Purpose

cidrhost

The IP address of one host in a prefix, by host number.

cidrnetmask

Convert an IPv4 prefix to a netmask.

cidrsubnet

One subnet inside a prefix, chosen by the number of extra bits and a network number.

cidrsubnets

Several consecutive subnets inside a prefix, sized by a list of extra bits and numbered automatically.

cidrcontains

OpenTofu only (since 1.7): whether an address or prefix is within a prefix.

cidrsubnet(prefix, newbits, netnum) extends the prefix length of prefix by newbits and fills the new bits with the binary value of netnum. For cidrsubnet("10.1.2.0/24", 4, 15) the new prefix is /28 and the four new bits are 1111, giving 10.1.2.240/28. Both IPv4 and IPv6 prefixes are accepted and the result uses the same family.

cidrsubnets(prefix, newbits...) allocates a sequence of consecutive ranges with possibly different sizes and numbers them automatically, starting with zero. This makes it the better tool for carving up a network plan in one expression. The documentation gives a rule to follow: once addresses from the result are assigned to real infrastructure, do not change any of the existing newbits arguments, because that would move every later range; appending new arguments is safe as long as address space remains.

locals {
  bookshelf_cidr = "10.20.0.0/16"

  # Four /24 networks, numbered in order. Never reorder or edit these entries once deployed.
  subnet_names = ["public-a", "public-b", "private-a", "private-b"]
  subnet_cidrs = zipmap(local.subnet_names, cidrsubnets(local.bookshelf_cidr, 8, 8, 8, 8))

  # A specific /24 chosen by network number.
  database_cidr = cidrsubnet(local.bookshelf_cidr, 8, 10)

  # The fifth usable address of the database subnet, e.g. for a static private IP.
  database_ip = cidrhost(local.database_cidr, 5)
}
> local.subnet_cidrs
{
  "private-a" = "10.20.2.0/24"
  "private-b" = "10.20.3.0/24"
  "public-a"  = "10.20.0.0/24"
  "public-b"  = "10.20.1.0/24"
}
> local.database_cidr
"10.20.10.0/24"
> local.database_ip
"10.20.10.5"
> cidrnetmask("10.20.0.0/16")
"255.255.0.0"
> join(" ", cidrsubnets("10.1.0.0/16", 4, 4, 8, 4))
"10.1.0.0/20 10.1.16.0/20 10.1.32.0/24 10.1.48.0/20"

The functions interpret IPv4 octets with leading zeros as decimal numbers, which is a historical accident that differs from systems that read them as octal; the documentation recommends not relying on it. For named, skippable allocations the hashicorp/subnets/cidr module wraps cidrsubnets with symbolic names.

Type conversion functions

Function Purpose

tobool, tonumber, tostring

Convert to a primitive type.

tolist, toset, tomap

Convert to a list, set or map. toset also removes duplicates.

convert

Convert to any type constraint, including optional() attributes with defaults (Terraform 1.15, OpenTofu 1.13).

try, can

Catch an error in an expression, see try and can.

sensitive, nonsensitive, issensitive, ephemeralasnull

Mark handling, see Functions for sensitive and ephemeral values.

type

Show the type of a value. Available only in terraform console.

Terraform converts values automatically where a type is expected, so explicit conversion is rarely required. It helps when the intent should be visible (a string read from a file used as a number), when a collection must become a set (to deduplicate it for for_each) and when a result has an ambiguous type.

convert(value, type_constraint), introduced in Terraform 1.15 and OpenTofu 1.13, evaluates the value and converts it to the type constraint written in the second argument, using the same syntax as the type argument of a variable, including optional() attributes with defaults. The older conversion functions remain for compatibility and each behaves as the matching convert call; for collections the documentation prefers convert with a precise element type because tolist and its siblings rely on the less precise any element type. OpenTofu additionally requires the defaults of optional attributes in the constraint to be constant values.

terraform {
  required_version = ">= 1.15"
}

locals {
  # A string read from configuration, used as a number.
  port = tonumber("8080")

  # Deduplicate environments before a for_each.
  environments = toset(["dev", "prod", "dev"])

  # Fill in defaults for optional attributes, with a precise type.
  services = convert(
    {
      catalog-api = {}
      web         = { replicas = 3 }
    },
    map(object({ replicas = optional(number, 2) }))
  )
}
> local.environments
toset([
  "dev",
  "prod",
])
> local.services
tomap({
  "catalog-api" = {
    "replicas" = 2
  }
  "web" = {
    "replicas" = 3
  }
})

try and can

try(expression, ...) evaluates its arguments in order and returns the first one that does not produce an error. If all fail, the error lists every problem it met. can(expression) evaluates one expression and returns true when it produced a result and false when it failed. Both catch only dynamic errors, errors caused by data that is known only at run time (a missing attribute, a failed conversion). They do not catch constructs that are invalid for every input, such as a malformed reference or a reference to an undeclared local value.

Use try to normalize data of uncertain shape, and keep it confined to a few local values, so the rest of the module can use plain references. Use can in variable validation to turn an error into a boolean; the documentation recommends it only for simple tests in validation rules and recommends try elsewhere.

variable "catalog_settings" {
  type        = any
  description = "Settings decoded from a YAML file; some attributes may be missing."
  default     = { replicas = 2 }
}

variable "catalog_hosts" {
  type    = any
  default = "catalog-api"
}

variable "service_name" {
  type    = string
  default = "catalog-api"

  validation {
    # regex errors when the pattern does not match; can turns that into false.
    condition     = can(regex("^[a-z][a-z0-9-]{2,30}$", var.service_name))
    error_message = "The service name must be lowercase letters, digits and hyphens, 3 to 31 characters, starting with a letter."
  }
}

locals {
  # The only place that deals with missing attributes.
  catalog = {
    replicas = try(var.catalog_settings.replicas, 1)
    port     = try(var.catalog_settings.port, 8080)
    tags     = try(var.catalog_settings.tags, {})
  }

  # A value that may arrive as a single string or as a list of strings.
  hosts = try(
    [tostring(var.catalog_hosts)],
    tolist(var.catalog_hosts),
  )
}

Overusing either function hides real mistakes and produces a configuration nobody can reason about, which is why the documentation warns against suppressing errors with try beyond attribute and type checks. A try around a resource attribute that should always exist turns a clear plan error into a silent default.

Regular expressions

regex(pattern, string) and regexall(pattern, string) apply a regular expression to a string. The pattern syntax is that of the Go regular expression engine, as listed in the documentation (character classes, repetition, named groups written (?P<name>x), Unicode classes and zero-width assertions such as ^, $ and \b). Because a pattern is written as a quoted string, every backslash that belongs to the pattern must be doubled: \\d, not \d. A heredoc or the replace form with slashes avoids some of the noise.

The return type of regex depends on the capture groups in the pattern:

Capture groups Result of regex

none

a single string with the whole match

one or more unnamed groups

a list of the captured substrings, in order

one or more named groups

a map from group name to captured substring

Named and unnamed groups cannot be mixed in one pattern. regex raises an error when the pattern does not match at all, which is useful to fail fast but bad for testing; regexall returns a list with one element per match (an empty list when there is none), so length(regexall(...)) > 0 is the way to test for a match, and it has the same element types as regex. The replace function also accepts a pattern, when the search string is wrapped in forward slashes, and the replacement can use $1 or $name to insert capture groups.

locals {
  image = "registry.bookshelf.example.com/catalog-api:1.4.2"

  # Named groups give a map.
  parts = regex("^(?P<repo>[^:]+):(?P<tag>[0-9.]+)$", local.image)

  # Test without erroring when there is no match.
  is_semver_tag = length(regexall("^[0-9]+\\.[0-9]+\\.[0-9]+$", local.parts.tag)) > 0

  # Remove the tag with a pattern between slashes.
  image_without_tag = replace(local.image, "/:[0-9.]+$/", "")
}
> join(",", regexall("[a-z]+", "1234abcd5678efgh9"))
"abcd,efgh"
> length(regexall("[a-z]+", "123456789")) > 0
false
> regex("[a-z]+", "53453453.345345aaabbbccc23454")
"aaabbbccc"

The documentation’s own advice is to prefer a more specialized function when one exists: parse JSON with jsondecode, YAML with yamldecode, CIDR blocks with the cidr family of functions, and prefixes and suffixes with startswith, endswith, trimprefix and trimsuffix. A regular expression is hard to read and obscures intent.

Functions for sensitive and ephemeral values

Terraform tracks two kinds of marks on values. A sensitive mark makes the CLI redact the value in plan and apply output, but the value is still written to the state and the plan file. An ephemeral mark (Terraform 1.10, OpenTofu 1.11) keeps the value out of both files and restricts where it can be used. Four functions read or change these marks. The full model, including sensitive = true on variables and outputs, ephemeral resources and write-only arguments, is in Sensitive Data, Ephemeral Values and Write-Only Arguments.

Function Available since Behavior

sensitive(value)

Terraform 0.15

Returns a copy of any value marked sensitive. Prefer sensitive = true on the variable or output declaration when possible; use the function when the value comes from something defined in your module, such as a file.

nonsensitive(value)

Terraform 0.15

Returns the value without the sensitive mark. It is an error to call it on a value that is not sensitive, because the call would be redundant. It exposes the value in output, so use it sparingly.

issensitive(value)

Terraform 1.8, OpenTofu 1.7

Returns true if the value is marked sensitive, false otherwise. OpenTofu 1.11 made it return an unknown result for an unknown argument, because a value that is unknown during planning might become non-sensitive once it is known.

ephemeralasnull(value)

Terraform 1.10

Returns the value with every ephemeral part replaced by null and every non-ephemeral part kept. Given a map or object that mixes the two, it nullifies only the ephemeral entries.

A value is sensitive when it is declared so or derived from a sensitive value. Terraform propagates the mark conservatively, which sometimes marks more than necessary: a SHA-256 hash of a secret, or the one non-secret field of a decoded JSON document, is still sensitive. nonsensitive is the escape hatch for the cases where you know, from the properties of your system and its threat model, that the derived value is safe. It is your responsibility that the argument removes everything sensitive; if a secret appears in output because of an inappropriate call, that is a bug in the module. Leave a comment next to every use that says why it is safe.

terraform {
  required_version = ">= 1.15"
}

variable "database_password" {
  type        = string
  description = "Password of the PostgreSQL superuser. Supply it with TF_VAR_database_password."
  sensitive   = true
}

variable "registry_credentials_json" {
  type        = string
  description = "JSON with a user name and a password for the private registry. Supply it with TF_VAR_registry_credentials_json."
  sensitive   = true
}

locals {
  # Derived from a sensitive variable, so the whole decoded object is sensitive.
  registry = jsondecode(var.registry_credentials_json)

  # The user name is not secret in this setup; the password stays sensitive.
  # SAFE: only the user name field is exposed, never the password field.
  registry_user = nonsensitive(local.registry["username"])

  # A short fingerprint of a high-entropy password lets operators confirm which secret is deployed.
  # SAFE: eight hex characters of a SHA-256 digest of a long random password reveal nothing usable.
  password_fingerprint = nonsensitive(substr(sha256(var.database_password), 0, 8))

  # A secret defined inside the module, read from a file that is kept out of version control.
  # (illustrative: the file must exist next to the configuration)
  # seed_secret = sensitive(file("${path.module}/secrets/seed.txt"))
}

output "registry_user" {
  value = local.registry_user
}

output "database_password_fingerprint" {
  value = local.password_fingerprint
}

output "database_password_is_sensitive" {
  # Evaluates to true. Reading a mark does not expose the value.
  value = issensitive(var.database_password)
}
> var.database_password
(sensitive value)
> issensitive(var.database_password)
true
> issensitive("hello")
false
> nonsensitive("clear")

Error: Invalid function argument

Invalid value for "value" parameter: the given value is not sensitive, so this
call is redundant.

ephemeralasnull bridges ephemeral values to places that do not accept them. An ordinary output and a resource argument that is not write-only reject an ephemeral value, because both end up in the state or the plan. The function replaces the ephemeral parts by null, so the non-ephemeral rest can be used and the presence of a field can still be shown.

terraform {
  required_version = ">= 1.15"
}

variable "environment" {
  type    = string
  default = "dev"
}

variable "session_token" {
  type        = string
  description = "Short-lived token for the deployment. Supply it with TF_VAR_session_token."
  ephemeral   = true
  default     = null
}

output "deployment_summary" {
  # environment stays; session_token is replaced by null, so the output can be recorded in the state.
  value = ephemeralasnull({
    environment   = var.environment
    session_token = var.session_token
  })
}

Provider-defined functions and the built-in terraform provider

Since Terraform 1.8 (OpenTofu 1.7), providers can expose functions. A provider function is called as provider::<local_name>::<function_name>(...), where <local_name> is the key the provider has in required_providers; with a provider alias the form is provider::<local_name>::<alias>::<function_name>(...). The functions of external providers are documented in the Terraform Registry, in each provider’s own documentation, and the only requirement from the module is the usual required_providers entry. Writing a function for a provider is a provider development task. See Providers for how providers are installed and configured.

The terraform provider that is built into the Terraform core (source address terraform.io/builtin/terraform, unrelated to the old external hashicorp/terraform provider) offers three functions, since Terraform 1.8 and OpenTofu 1.10. They are provider functions, so the prefix is provider::terraform::, not a bare terraform:: namespace, and the provider must be declared in required_providers for them to be callable.

Function Purpose

provider::terraform::encode_tfvars

Takes an object and returns a string in .tfvars syntax. The attribute names must be valid variable names.

provider::terraform::decode_tfvars

Takes the content of a .tfvars file as a string and returns an object describing the raw values. Because it has no type information, the result uses the most general types (string, number, bool, object, tuple); convert lists, sets or maps with the conversion functions.

provider::terraform::encode_expr

Takes any value and returns a string with Terraform expression syntax approximating it. Its primary use is the tfe_variable resource of the hashicorp/tfe provider with hcl = true.

The documentation calls all three rarely needed and recommends exchanging data between configurations with data sources, not generated .tfvars files, and using jsonencode or yamlencode for general serialization because the exact syntax produced for some values may change in future versions.

terraform {
  required_version = ">= 1.15"

  required_providers {
    terraform = {
      source = "terraform.io/builtin/terraform"
    }
  }
}

locals {
  workspace_vars = {
    environment = "dev"
    zones       = ["a", "b"]
  }

  decoded = provider::terraform::decode_tfvars(<<-EOT
    environment = "dev"
  EOT
  )
}

output "decoded_environment" {
  value = local.decoded.environment
}

output "tfvars_preview" {
  # A string in .tfvars syntax. Inspect it; do not parse it by hand.
  value = provider::terraform::encode_tfvars({ environment = "dev" })
}

output "zones_expression" {
  value = provider::terraform::encode_expr(local.workspace_vars.zones)
}

The documentation for the same family also lists a symbol, terraform.applying, which is not a function but is referred to as one of the built-in terraform values (Terraform 1.10). It is true while Terraform runs an apply operation, including in destroy mode, and false during every other operation. Since it is an ephemeral value, it is only valid in ephemeral contexts: write-only arguments, ephemeral variables, local values, ephemeral resources, ephemeral outputs, provider configuration, and provisioner and connection configuration. It is not written to plan or state files. The standard use is to give a plan read-only credentials and an apply write credentials:

terraform {
  required_version = ">= 1.15"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

variable "read_role_arn" {
  type        = string
  description = "Role used by plan. Supply it with TF_VAR_read_role_arn."
}

variable "write_role_arn" {
  type        = string
  description = "Role used by apply. Supply it with TF_VAR_write_role_arn."
}

locals {
  role_arn = terraform.applying ? var.write_role_arn : var.read_role_arn
}

provider "aws" {
  region = "eu-west-1"

  assume_role {
    role_arn = local.role_arn
  }
}

OpenTofu documents the same symbol as terraform.applying and tofu.applying, and its built-in provider page documents encode_tfvars and decode_tfvars plus the same expression encoder (see Terraform and OpenTofu differences).

Pure and impure functions

The Terraform documentation does not label functions "pure" or "impure", but the distinction explains how functions behave in the plan and the apply, and the Terraform changelog itself speaks of impure functions. A working classification:

Kind Examples and behavior

Pure

The result depends only on the arguments: format, join, merge, lookup, cidrsubnet, sha256, jsonencode, formatdate, regex. Calling it twice with the same arguments gives the same value. If an argument is unknown during the plan, the result is generally unknown too.

Impure: random or clock

uuid and bcrypt (random salt) give a new value every call. timestamp returns a value that cannot be predicted during planning, so the plan shows it as unknown and the value is taken at apply time. Each of these causes a diff on every run if used in a resource argument.

Reads the world

file, filebase64, fileexists, fileset, templatefile, the file variants of the hash functions and the abspath-style path functions read the disk. They are deterministic only for as long as the files do not change; since Terraform 1.13 Terraform checks that the plan and the apply see the same result.

Fixed per plan

plantimestamp is the clock, but it is evaluated once for the plan and can be used where an impure value would not be acceptable, such as in checks and validation.

Practical rules follow from this:

  • Do not put an impure function in a resource argument that is compared on every plan. Move the value into the state once: time_static for a time, random_id or random_password for random data.

  • Keep file-reading functions pointed at committed, stable files, starting from path.module.

  • Do not use an impure function as the source of a count or for_each value, because those must be known when the plan is made.

  • To record a value at creation and then ignore it, combine the function with lifecycle { ignore_changes = [...] }; this is the documented exception for timestamp and uuid.

The consequences of unknown values for count and for_each, and the workarounds, are in Meta-Arguments: count, for_each and depends_on.

Experimenting with terraform console

terraform console opens an interactive prompt to evaluate expressions against the current configuration and state. It is the fastest way to learn a function, to test a regular expression, to see the exact type of a value and to debug a local that does not do what you expect. Close it with exit, Control-C or Control-D. The documentation’s examples for each function are console transcripts.

# Interactive session in the directory that contains the configuration
terraform console

# Non-interactive: pipe newline-separated expressions; only the output of the last one is printed
echo 'split(",", "catalog-api,reviews-api,web")' | terraform console

# Evaluate against a plan, so resource values that are not yet created are available
terraform console -plan

# Evaluate in the scope of a child module (Terraform 1.16 and later)
terraform console -scope='module.network'
terraform console -scope='module.network[0]'
terraform console -scope='module.network["key"]'
$ echo 'split(",", "catalog-api,reviews-api,web")' | terraform console
tolist([
  "catalog-api",
  "reviews-api",
  "web",
])

The points to remember:

  • Scope. By default expressions are evaluated in the root module, so variables, locals and resources of child modules are not visible. Since Terraform 1.16 the -scope option selects a child module or one of its instances.

  • State and lock. The console reads the state of the current workspace from the backend and holds the state lock, so you cannot run another command that modifies the state at the same time. In a project that has never been applied, the state is empty and the console is still good for literals and functions.

  • Planned values. Without -plan, resource attributes that do not exist yet in the state are shown as unknown, for example (known after apply). With -plan, Terraform makes a plan first, which is slower and, for a configuration that does work during planning (an external data source, for example), can have side effects.

  • Functions that behave differently. type exists only in the console and is meant for examining values, not for use in expressions; plantimestamp is not available in it; timestamp returns the current time at the moment you type it.

  • Sensitive output. Sensitive values print as (sensitive value). Do not paste real secrets into a console session: the session may be logged by a terminal recorder or shell history.

  • OpenTofu. The command is tofu console and works the same way.

Terraform and OpenTofu differences

The library is almost identical. The differences are the functions each tool added on its own and the version in which a shared function arrived.

Function or feature Terraform OpenTofu

Provider-defined functions (provider::name::function)

1.8

1.7

templatestring

1.9

1.7

issensitive

1.8

1.7

ephemeralasnull

1.10

Documented with the ephemeral values support (1.11)

convert

1.15

1.13

cidrcontains, base64gunzip, urldecode

Not available

1.7

assumeequal, assumenotnull and the other functions whose names begin with assume that hint at the final value of an unknown

Not available

1.13

Built-in provider functions encode_tfvars, decode_tfvars

1.8, together with encode_expr

1.10, documented on the built-in provider page with the expression encoder

Symbol for the apply phase

terraform.applying (1.10)

terraform.applying and tofu.applying

Console scope option

-scope (1.16)

The OpenTofu console page does not list -scope (or -plan)

The assume family of functions is OpenTofu-specific. It lets a module author state facts about a value that will be known only at apply time, for example that an identifier starts with vpc- or is not null, so OpenTofu can evaluate conditions during the plan instead of failing during the apply. Whenever possible OpenTofu infers such assumptions automatically. Because Terraform has no equivalent, a module that must support both tools should not use them.

Because both tools evaluate the same language, the examples on this page work unchanged with tofu, with these exceptions: the OpenTofu-only functions above, and .tf files that use convert need OpenTofu 1.13 or later. See OpenTofu for the full comparison of the two tools.

What changed recently

Version Change

Terraform 0.15

sensitive, nonsensitive and one.

Terraform 1.5

plantimestamp.

Terraform 1.8 (2024-04-10)

Provider-defined functions, the issensitive function, and the encode_tfvars, decode_tfvars and encode_expr functions of the built-in provider. OpenTofu 1.7 added provider-defined functions, templatestring, issensitive, cidrcontains, base64gunzip and urldecode.

Terraform 1.9 (2024-06-26)

The templatestring function. templatefile no longer fails when its path is marked sensitive; the rendered result is marked sensitive instead.

Terraform 1.10 (2024-11-27)

ephemeralasnull and the terraform.applying symbol; element accepts negative indices; a fix for plantimestamp returning an invalid date during validation. OpenTofu 1.10 added the .tfvars functions to its built-in provider.

Terraform 1.13

The filesystem functions are checked for a consistent result between plan and apply. Terraform 1.14 allowed them to return inconsistent results when evaluated in provider configuration.

Terraform 1.15 (2026-04-29)

The convert function. OpenTofu 1.13 added convert and the functions whose names begin with assume.

Terraform 1.16 (2026-08-26)

contains can test for null; merge no longer fails when given null objects; terraform console -scope.

Terraform 1.17 (2026-10-21)

log and pow raise errors instead of returning infinity, and no longer panic when the result is not a number.

Deprecations and removals: the list and map functions are marked deprecated since Terraform 0.12 in favor of tolist and tomap, and omitting the default of lookup is deprecated since 0.7. The template_file data source and its provider are archived. No built-in function was removed in these releases.

Where the books are outdated or wrong

The two reference books for this section were written for Terraform 1.9 and OpenTofu 1.8 and earlier. For the topics of this page that means:

  • The template_file data source, which the books present as the legacy way to render templates, is superseded by templatefile (and, for templates held in strings, templatestring). Do not add the template provider to new code.

  • The books predate convert, ephemeralasnull, terraform.applying, the -scope option of the console and the 1.17 error behavior of log and pow. Where they describe the function list, treat the official reference as the authority for the release you run.

  • Examples in older tutorials call list(...) and map(...) as functions or use the quoted interpolation form around a bare expression, such as a variable reference wrapped in quotes and dollar-brace; the modern forms are tolist, tomap and the plain expression.

  • The books treat templating as a general-purpose way to produce JSON and YAML; the official guidance is to build a value and encode it with jsonencode or yamlencode.

On any discrepancy, follow the documentation of the version you run.

Pitfalls

  • Calling timestamp() or uuid() in a resource argument. The value changes on every run, so every plan shows a diff. Use time_static, random_id or plantimestamp in checks.

  • Reading a generated file with file or templatefile. The function runs before the file exists. Make the content a value instead.

  • Forgetting that fileset and the file functions resolve from the current directory unless you start from path.module.

  • Expecting regex to return false when nothing matches; it raises an error. Use regexall with length or wrap regex in can.

  • Writing a pattern with single backslashes in a quoted string. The quoted string consumes the first backslash; write \\d.

  • Passing a sensitive value to a function and then being surprised that the output must be marked sensitive. The mark propagates to every derived value; remove it deliberately with nonsensitive, never casually.

  • Using nonsensitive on a secret to make an output readable. It also exposes the value in the plan output, the state and the logs of the pipeline.

  • Using try to hide errors from required attributes. Reserve it for optional or variably-shaped data.

  • Changing the existing newbits arguments of a cidrsubnets call after subnets exist. Append only.

  • Using a function that exists only in one tool in a module that is shared between Terraform and OpenTofu.

  • Using md5 or sha1 where an integrity guarantee is needed. They are not secure hashes.

  • Using bcrypt in a resource argument. The random salt changes the value on every call.