Skip to main content

Terraform Built-in Functions — The Complete Reference with Examples

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

Terraform's HCL language has no user-defined functions, but it ships with a rich library of built-in functions that handle everything from string manipulation to CIDR math. Knowing these functions is the difference between clean, maintainable configurations and sprawling hacks with hardcoded values everywhere.

Testing Functions with terraform console

Before using a function in your configuration, test it interactively:

$ terraform console

> upper("hello terraform")
"HELLO TERRAFORM"

> length(["a", "b", "c"])
3

> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"

The console is your best friend when working with unfamiliar functions. Every example in this post can be tested there.

String Functions

String functions handle formatting, splitting, joining, and pattern matching.

# format — printf-style string formatting
> format("Hello, %s! You have %d instances.", "team", 5)
"Hello, team! You have 5 instances."

# join — concatenate list elements with a separator
> join(", ", ["us-east-1", "us-west-2", "eu-west-1"])
"us-east-1, us-west-2, eu-west-1"

# split — break a string into a list
> split(",", "a,b,c,d")
["a", "b", "c", "d"]

# replace — simple string replacement
> replace("hello-world-app", "-", "_")
"hello_world_app"

# regex — extract a match from a string
> regex("[0-9]+", "instance-42-prod")
"42"

# regexall — extract all matches
> regexall("[0-9]+", "port-8080-and-443")
["8080", "443"]

# trimspace — remove leading/trailing whitespace
> trimspace(" hello ")
"hello"

# lower / upper — case conversion
> lower("Production")
"production"
> upper("staging")
"STAGING"

# substr — extract a substring (offset, length)
> substr("us-east-1a", 0, 9)
"us-east-1"

Real-world usage — building a consistent naming convention:

locals {
name_prefix = lower(format("%s-%s", var.project, var.environment))
# "webapp-production"
}

resource "aws_instance" "web" {
tags = {
Name = "${local.name_prefix}-web"
}
}

Numeric Functions

# min / max — smallest or largest value
> min(5, 12, 3, 9)
3
> max(5, 12, 3, 9)
12

# ceil / floor — round up or down
> ceil(4.2)
5
> floor(4.9)
4

# abs — absolute value
> abs(-42)
42

# parseint — parse a string as an integer in a given base
> parseint("FF", 16)
255

Practical example — ensuring a minimum instance count:

variable "desired_count" {
type = number
default = 1
}

locals {
instance_count = max(var.desired_count, 2) # At least 2 for HA
}

Collection Functions

Collection functions are the workhorses of Terraform. They manipulate lists and maps, which are fundamental to writing DRY configurations.

# length — count elements
> length(["a", "b", "c"])
3
> length({ name = "web", env = "prod" })
2

# merge — combine maps (last wins on conflict)
> merge({ a = 1, b = 2 }, { b = 3, c = 4 })
{ "a" = 1, "b" = 3, "c" = 4 }

# lookup — get a map value with a default
> lookup({ dev = "t3.micro", prod = "t3.large" }, "staging", "t3.small")
"t3.small"

# keys / values — extract map keys or values
> keys({ name = "web", env = "prod" })
["env", "name"]
> values({ name = "web", env = "prod" })
["prod", "web"]

# flatten — collapse nested lists into a single list
> flatten([["a", "b"], ["c"], ["d", "e"]])
["a", "b", "c", "d", "e"]

# zipmap — create a map from two lists
> zipmap(["name", "env"], ["web-app", "production"])
{ "env" = "production", "name" = "web-app" }

# concat — combine multiple lists
> concat(["a", "b"], ["c", "d"])
["a", "b", "c", "d"]

# contains — check if a list contains a value
> contains(["us-east-1", "us-west-2"], "eu-west-1")
false

# distinct — remove duplicates from a list
> distinct(["a", "b", "a", "c", "b"])
["a", "b", "c"]

# element — get an element by index (wraps around)
> element(["a", "b", "c"], 4)
"b"

Real-world usage — merging default and custom tags:

locals {
default_tags = {
ManagedBy = "terraform"
Environment = var.environment
Project = var.project
}

resource_tags = merge(local.default_tags, var.extra_tags)
}

Using lookup for environment-specific sizing:

variable "instance_sizes" {
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}

resource "aws_instance" "web" {
instance_type = lookup(var.instance_sizes, var.environment, "t3.micro")
}

Encoding Functions

# jsonencode / jsondecode — convert to/from JSON
> jsonencode({ name = "web", ports = [80, 443] })
"{\"name\":\"web\",\"ports\":[80,443]}"

# yamlencode — convert to YAML
> yamlencode({ replicas = 3, image = "nginx:latest" })
"image: nginx:latest\nreplicas: 3\n"

# base64encode / base64decode
> base64encode("Hello, World!")
"SGVsbG8sIFdvcmxkIQ=="
> base64decode("SGVsbG8sIFdvcmxkIQ==")
"Hello, World!"

# urlencode — URL-encode a string
> urlencode("hello world&foo=bar")
"hello+world%26foo%3Dbar"

Practical example — passing structured data as user_data:

resource "aws_instance" "web" {
user_data = base64encode(jsonencode({
packages = ["nginx", "certbot"]
hostname = "web-${var.environment}"
config = { port = 80, workers = 4 }
}))
}

Filesystem Functions

# file — read a file as a string
> file("scripts/init.sh")
"#!/bin/bash\napt-get update\n..."

# fileexists — check if a file exists
> fileexists("scripts/init.sh")
true

# templatefile — read a file and substitute variables
> templatefile("templates/config.tpl", { db_host = "10.0.1.50", port = 5432 })

The templatefile function is extremely powerful for generating configuration files:

# templates/nginx.conf.tpl
server {
listen ${port};
server_name ${domain};

location / {
proxy_pass http://${backend_host}:${backend_port};
}
}
# main.tf
resource "aws_instance" "web" {
user_data = templatefile("templates/nginx.conf.tpl", {
port = 80
domain = var.domain_name
backend_host = aws_instance.app.private_ip
backend_port = 8080
})
}

Date and Time Functions

# timestamp — current UTC time in RFC 3339
> timestamp()
"2025-07-28T14:30:00Z"

# formatdate — format a timestamp
> formatdate("YYYY-MM-DD", "2025-07-28T14:30:00Z")
"2025-07-28"

> formatdate("DD MMM YYYY hh:mm", "2025-07-28T14:30:00Z")
"28 Jul 2025 14:30"

# timeadd — add a duration to a timestamp
> timeadd("2025-07-28T00:00:00Z", "72h")
"2025-07-31T00:00:00Z"

Practical example — setting resource expiration tags:

locals {
created_at = timestamp()
expires_at = timeadd(timestamp(), "720h") # 30 days
}

resource "aws_instance" "temp" {
tags = {
CreatedAt = local.created_at
ExpiresAt = local.expires_at
}
}

Hash and Crypto Functions

# md5 — MD5 hash (not for security, useful for change detection)
> md5("hello")
"5d41402abc4b2a76b9719d911017c592"

# sha256 — SHA-256 hash
> sha256("hello")
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

# bcrypt — generate a bcrypt hash (for passwords)
> bcrypt("my-password")
"$2a$10$..."

# uuid — generate a random UUID
> uuid()
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"

IP Network Functions

These are essential for network infrastructure — calculating subnets, host addresses, and CIDR ranges:

# cidrsubnet — calculate a subnet address
# cidrsubnet(prefix, newbits, netnum)
> cidrsubnet("10.0.0.0/16", 8, 0)
"10.0.0.0/24"
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
> cidrsubnet("10.0.0.0/16", 8, 255)
"10.0.255.0/24"

# cidrhost — calculate a host IP within a CIDR range
> cidrhost("10.0.1.0/24", 10)
"10.0.1.10"

# cidrnetmask — get the netmask for a CIDR prefix
> cidrnetmask("10.0.0.0/16")
"255.255.0.0"

Real-world usage — dynamically creating subnets across availability zones:

variable "vpc_cidr" {
default = "10.0.0.0/16"
}

variable "az_count" {
default = 3
}

resource "aws_subnet" "private" {
count = var.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]

tags = {
Name = "private-${count.index}"
}
}

resource "aws_subnet" "public" {
count = var.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 100)
availability_zone = data.aws_availability_zones.available.names[count.index]

tags = {
Name = "public-${count.index}"
}
}

Type Conversion Functions

# tostring / tonumber / tobool — explicit conversion
> tostring(42)
"42"
> tonumber("42")
42
> tobool("true")
true

# tolist / toset / tomap — convert collection types
> tolist(toset(["a", "b", "a", "c"]))
["a", "b", "c"]

# try — return the first expression that doesn't error
> try(tonumber("not-a-number"), 0)
0

# can — test if an expression is valid
> can(tonumber("42"))
true
> can(tonumber("hello"))
false

The try function is invaluable for handling optional values:

locals {
instance_type = try(var.override_instance_type, lookup(var.defaults, var.environment, "t3.micro"))
}

Function Categories Quick Reference

CategoryKey FunctionsCommon Use Case
Stringformat, join, split, replace, regexNaming conventions, string manipulation
Numericmin, max, ceil, floorCapacity planning, constraints
Collectionmerge, lookup, flatten, keys, valuesTag management, dynamic config
Encodingjsonencode, yamlencode, base64encodeUser data, policy documents
Filesystemfile, templatefile, fileexistsConfig templates, scripts
Date/Timetimestamp, formatdate, timeaddExpiration tags, audit trails
Hashmd5, sha256, uuidChange detection, unique IDs
IP Networkcidrsubnet, cidrhost, cidrnetmaskVPC and subnet design
Typetostring, tolist, try, canType coercion, error handling

Wrapping Up

Terraform's built-in functions turn HCL from a simple configuration language into something genuinely expressive. merge and lookup simplify tag management. cidrsubnet automates network math. templatefile replaces fragile heredocs with clean templates. And try handles edge cases that would otherwise require verbose conditional logic. Test functions in terraform console before using them in production code, and bookmark the official function reference — you will come back to it constantly.

Next, we will explore Terraform CI/CD — automating plan on pull requests, apply on merge, and scheduled drift detection with GitHub Actions, GitLab CI, and Atlantis.