Advanced Terraform

Built-in Functions & Expressions

● Intermediate ⏱ 20 min read terraform

Terraform has no custom function support — you can't define your own functions. Instead, it ships a rich library of built-in functions that cover everything from string manipulation to CIDR subnet math. Knowing which functions exist and what they do is the difference between a configuration that fights its inputs and one that transforms them cleanly.

The terraform console

Before writing functions into a configuration, test them interactively. terraform console opens a REPL where you can evaluate any Terraform expression against your current state and variables:

$ terraform console
> upper("hello")
"HELLO"
> cidrsubnet("10.0.0.0/16", 8, 0)
"10.0.0.0/24"
> length(["a", "b", "c"])
3
> exit

Run it inside a Terraform working directory to have access to var.*, local.*, and data source values. It's the fastest way to debug complex expressions without a plan/apply cycle.

String Functions

String functions are the most commonly used in configurations, primarily for naming, formatting, and parsing.

FunctionExampleResult
upper(s)upper("hello")"HELLO"
lower(s)lower("WORLD")"world"
title(s)title("hello world")"Hello World"
trimspace(s)trimspace(" hi ")"hi"
trim(s, chars)trim("/path/", "/")"path"
replace(s, old, new)replace("a-b-c", "-", "_")"a_b_c"
split(sep, s)split(",", "a,b,c")["a","b","c"]
join(sep, list)join("-", ["a","b","c"])"a-b-c"
substr(s, offset, len)substr("hello", 0, 3)"hel"
startswith(s, prefix)startswith("terraform", "terra")true
endswith(s, suffix)endswith("main.tf", ".tf")true
strcontains(s, sub)strcontains("prod-east", "prod")true
format(fmt, …)format("%-10s %5d", "price", 42)"price 42"
formatlist(fmt, list)formatlist("item-%d", [1,2,3])["item-1","item-2","item-3"]
regex(pattern, s)regex("(\\w+)-", "prod-east")["prod"]
regexall(pattern, s)regexall("[0-9]+", "v1.2.3")["1","2","3"]

Common patterns in naming resources:

locals {
  # Normalize a name: lowercase, replace spaces with hyphens
  safe_name = lower(replace(var.project_name, " ", "-"))

  # Truncate to fit a resource name limit (e.g., S3 bucket = 63 chars)
  bucket_name = substr("${local.safe_name}-${var.environment}", 0, 63)

  # Build a tag-friendly name
  resource_prefix = format("%s-%s-%s", var.company, var.env, var.region)
}

Collection Functions

Collection functions operate on lists, maps, and sets — essential for transforming variable inputs into the shape resources expect.

FunctionPurpose
length(collection)Number of elements in list, map, or string
concat(lists…)Merge multiple lists into one
flatten(list)Collapse nested lists into a single list
distinct(list)Remove duplicate elements
compact(list)Remove null and empty string elements
sort(list)Sort a list of strings alphabetically
reverse(list)Reverse element order
slice(list, start, end)Extract a sublist
element(list, index)Get element at index (wraps around)
index(list, value)Find position of a value in a list
contains(list, value)Check if value is in a list
keys(map)Return list of map keys
values(map)Return list of map values
merge(maps…)Merge maps, later maps win on conflict
lookup(map, key, default)Safe map access with default
toset(list)Convert list to set (deduplicates)
tolist(set)Convert set to list
tomap(object)Convert object to map
zipmap(keys, values)Build a map from separate key and value lists
one(list)Return the single element or null if empty; error if multiple

Real-world examples:

locals {
  # Merge default tags with per-resource tags (resource tags win)
  common_tags = { env = var.environment, owner = var.team }
  instance_tags = merge(local.common_tags, { role = "web" })

  # Flatten a list-of-lists from a for_each result
  all_sg_ids = flatten([
    for az in var.availability_zones : [
      aws_security_group.az[az].id,
      aws_security_group.shared.id
    ]
  ])

  # Safe lookup with default
  instance_type = lookup(var.instance_types_by_env, var.environment, "t3.micro")
}

Numeric Functions

FunctionExampleResult
abs(n)abs(-5)5
ceil(n)ceil(1.2)2
floor(n)floor(1.9)1
max(nums…)max(1, 5, 3)5
min(nums…)min(1, 5, 3)1
pow(base, exp)pow(2, 8)256
log(n, base)log(100, 10)2
parseint(s, base)parseint("ff", 16)255

Encoding Functions

Encoding functions convert values between formats — frequently used for passing JSON configuration to resources and reading secrets:

FunctionPurpose
jsonencode(value)Encode any Terraform value as a JSON string
jsondecode(s)Parse a JSON string into a Terraform value
yamlencode(value)Encode as YAML
yamldecode(s)Parse YAML into a Terraform value
base64encode(s)Base64-encode a string
base64decode(s)Decode a base64 string
urlencode(s)URL-encode a string
sha256(s)SHA-256 hex digest of a string
md5(s)MD5 hex digest (use for change detection, not security)
uuid()Generate a random UUID (new value each plan — use with care)
resource "aws_ecs_task_definition" "app" {
  family = "app"

  # jsonencode turns a Terraform object into the JSON string ECS expects
  container_definitions = jsonencode([
    {
      name      = "app"
      image     = "${var.ecr_repo}:${var.image_tag}"
      essential = true
      portMappings = [{ containerPort = 8080, hostPort = 8080 }]
      environment = [
        { name = "ENV",    value = var.environment },
        { name = "REGION", value = var.aws_region }
      ]
    }
  ])
}

# Parse a JSON secret from AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
}

locals {
  db_creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

Filesystem Functions

Filesystem functions read files at plan time — useful for loading scripts, policies, and templates:

FunctionPurpose
file(path)Read a file as a string
filebase64(path)Read a file and return base64-encoded string
templatefile(path, vars)Read and render a template file with variable substitution
fileset(path, pattern)Return a set of file paths matching a glob pattern
filemd5(path)MD5 hash of a file — triggers resource updates when file changes
filesha256(path)SHA-256 hash of a file
pathexpand(path)Expand ~ to the home directory
abspath(path)Return absolute path
dirname(path)Directory component of a path
basename(path)Filename component of a path
# Inline IAM policy from a file
resource "aws_iam_policy" "app" {
  name   = "app-policy"
  policy = file("${path.module}/policies/app.json")
}

# User data script with variable substitution
resource "aws_instance" "app" {
  user_data = templatefile("${path.module}/scripts/user_data.sh.tpl", {
    environment = var.environment
    db_host     = aws_db_instance.main.endpoint
    bucket_name = aws_s3_bucket.app.id
  })
}

# Detect configuration file changes and trigger Lambda update
resource "aws_lambda_function" "processor" {
  source_code_hash = filesha256("${path.module}/lambda.zip")
  # ... triggers replacement when zip changes
}

IP & Network Functions

Network functions make VPC subnet math declarative — no manual CIDR calculation needed:

FunctionPurpose
cidrsubnet(prefix, newbits, netnum)Calculate a subnet CIDR within an address prefix
cidrsubnets(prefix, newbits…)Calculate multiple subnets at once
cidrhost(prefix, hostnum)Calculate a specific host address within a CIDR
cidrnetmask(prefix)Return the subnet mask for a CIDR
cidrcontains(range, addr)Check if an address is within a CIDR range

cidrsubnet(prefix, newbits, netnum) divides a prefix by adding newbits to the prefix length and selecting the netnum-th subnet:

# VPC: 10.0.0.0/16
# Create /24 subnets by adding 8 bits (16+8=24)
# netnum 0 = 10.0.0.0/24, netnum 1 = 10.0.1.0/24, etc.

variable "availability_zones" {
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_subnet" "public" {
  for_each = toset(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  availability_zone = each.key
  cidr_block = cidrsubnet(
    aws_vpc.main.cidr_block,
    8,
    index(var.availability_zones, each.key)
  )
}

cidrsubnets is cleaner when you need subnets of varying sizes:

locals {
  # From 10.0.0.0/16, carve out subnets:
  # +8 bits = /24, +4 bits = /20, +4 bits = /20
  subnets = cidrsubnets("10.0.0.0/16", 8, 4, 4)
  # ["10.0.0.0/24", "10.0.16.0/20", "10.0.32.0/20"]
}

Type Conversion

Terraform is strongly typed. When a function or resource expects a specific type, convert explicitly:

tostring(42)          # "42"
tonumber("42")        # 42
tobool("true")        # true
tolist(toset([1,1,2])) # [1, 2] — deduped and back to list
tomap({ a = 1 })      # { a = 1 } as map type

The try(expression, fallback) function evaluates an expression and returns a fallback if it errors — useful for safely accessing optional nested attributes:

locals {
  # If the object doesn't have a port key, default to 443
  port = try(var.config.port, 443)

  # Safe map lookup without error on missing key
  region = try(var.region_map[var.env], "us-east-1")
}

can(expression) returns true if the expression succeeds, false if it throws an error — useful in variable validation:

variable "image_tag" {
  type = string
  validation {
    condition     = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+$", var.image_tag))
    error_message = "image_tag must be a semver string like v1.2.3"
  }
}

Key Takeaways