Built-in Functions & Expressions
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.
| Function | Example | Result |
|---|---|---|
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.
| Function | Purpose |
|---|---|
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
| Function | Example | Result |
|---|---|---|
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:
| Function | Purpose |
|---|---|
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:
| Function | Purpose |
|---|---|
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:
| Function | Purpose |
|---|---|
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
- Use
terraform consoleto test functions interactively against your actual variables and state — it's faster than plan/apply cycles. - String functions (
lower,replace,format,trimspace) handle naming normalization.regexandregexallparse structured strings. - Collection functions transform inputs:
mergecombines maps with last-wins,flattencollapses nested lists,compactremoves nulls,distinctdeduplicates. jsonencode/jsondecodeandtemplatefilebridge Terraform's type system to resources that expect raw strings.- Network functions (
cidrsubnet,cidrsubnets,cidrhost) make VPC design declarative — drive them from a single VPC CIDR variable and index math. try(expr, fallback)safely handles optional attributes.can(expr)turns errors into booleans for validation rules.