Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terraform Functions You Should Know

By Kokil Thapa | Last reviewed: September 2026

Terraform functions you should know are the built-in helpers in HashiCorp Configuration Language (HCL) that turn static infrastructure files into reusable logic. You declare resources in .tf files, but functions handle naming, tagging, CIDR math, JSON payloads, and conditional values at plan time. On production stacks I maintain alongside infrastructure as code with Terraform, the same ten functions appear in almost every module. This guide walks through each category with copy-paste examples you can drop into real projects today.

What are Terraform functions and why do they matter?

Functions in Terraform are expressions evaluated during planning, not at apply time on the remote server. They live inside ${...} or bare parentheses in HCL2. You cannot define custom functions in pure Terraform; you compose built-ins inside locals, variable defaults, resource arguments, and output blocks.

That limitation shapes how teams structure modules. Complex logic belongs in Terraform variables, locals, and outputs, not scattered across dozens of resource files. Functions keep module interfaces small while behaviour stays flexible.

Terraform Function CategoriesInput: variables + localsstrings, maps, lists, numbersStringformat, replaceCollectionmerge, forEncodingjsonencodeNetworkcidrsubnetResource arguments at plan timenames, tags, policies, user_dataterraform plan / apply
Terraform functions you should know grouped by category—from input variables to final resource arguments evaluated at plan time

Official reference lives in the Terraform functions documentation. Bookmark it. Autocomplete in VS Code with the HashiCorp extension catches many names, but understanding behaviour prevents subtle plan diffs.

How Terraform evaluates function expressions

Terraform builds a dependency graph before planning. Function calls that reference attributes from other resources create implicit edges. A format() call using aws_vpc.main.id waits until that VPC is known. Pure functions on variables resolve immediately.

Keep that in mind when mixing functions with Terraform for_each vs count. Both meta-arguments accept expressions, but for_each requires a map or set of strings. Collection functions often build that map upstream.

Which string functions should you use in Terraform modules?

String functions shape resource names, DNS labels, and IAM paths. Three functions cover most daily work: format, replace, and lower.

format() works like printf. It keeps naming conventions consistent across environments:

locals {
  name_prefix = format("%s-%s", var.project, var.environment)
  bucket_name = format("%s-assets-%s", local.name_prefix, var.region)
}

replace() supports literal or regex replacement. Use it to sanitise user-supplied strings before they become cloud resource names:

locals {
  safe_slug = replace(lower(var.site_name), " ", "-")
  dns_label = replace(local.safe_slug, "[^a-z0-9-]", "")
}

join() and split() round-trip list data through delimited strings. They pair well with tag builders and security group rule descriptions.

format, replace, and regex workflow

On a VPS provisioning stack built with guidance from Terraform for VPS provisioning, hostname generation often chains three calls:

  1. lower() normalises input.
  2. replace() strips illegal characters.
  3. format() appends environment suffix and index.

regex() and regexall() extract capture groups. They are easy to misuse. Test patterns in the site regex tester tool before pasting them into production modules. A bad pattern fails the entire plan.

String Function PipelineRaw input"Prod API"lower()"prod api"replace()"prod-api"format()prod-api-stgResult: valid DNS label and AWS resource nametrim / trimspacestrip whitespace from varssubstr / strrevtruncate long identifiersCommon mistake: regex without anchoringpartial matches silently pass bad values
Chaining lower, replace, and format is a core pattern among Terraform functions you should know for safe resource naming

indent() helps when embedding multi-line strings inside YAML or JSON templates. It is less common but saves manual spacing in policy documents.

How do collection functions make Terraform configuration DRY?

Collection functions transform maps and lists—the data structures behind reusable Terraform modules. Start with merge, lookup, and for.

merge() combines maps. Later keys override earlier ones. Standard pattern for default tags plus environment overrides:

locals {
  default_tags = {
    managed_by = "terraform"
    project    = var.project
  }
  tags = merge(local.default_tags, var.extra_tags)
}

lookup(map, key, default) avoids errors when a key might be absent. Use it for optional feature flags in shared modules:

instance_type = lookup(var.instance_overrides, var.environment, "t3.small")

for expressions are the most powerful collection tool. They build new lists or maps from existing ones:

locals {
  private_subnet_ids = [for s in aws_subnet.private : s.id]
  name_to_cidr = {
    for k, v in var.subnets : k => v.cidr
  }
}

flatten, distinct, and set operations

flatten() collapses nested lists—common after generating rules per security group. distinct() removes duplicates before passing values to for_each.

setsubtract(), setintersection(), and setunion() compare sets of strings. They help when validating CIDR allow lists or computing role membership diffs in policy modules scanned by Checkov for Terraform misconfigurations.

zipmap() pairs two lists into a map. It is useful when legacy variable design exposes parallel lists instead of a single map object. Refactor to a map when you control the interface.

FunctionInput typesTypical useGotcha
mergemapsLayer default and override tagsOnly maps—not lists
lookupmap + keyEnvironment-specific defaultsDefault must match value type
forlist or mapTransform collectionsOutput type must match context
concatlistsCombine SG rules or routesAll elements must share type
tosetlistFeed for_eachLists must contain only strings
tryexpressionsSafe fallback on missing attrsCan hide real config bugs

How do encoding and type conversion functions prevent runtime errors?

Cloud APIs expect JSON strings, booleans, and numbers—not HCL types. Encoding and conversion functions bridge that gap.

jsonencode() serialises Terraform values into JSON. It appears in IAM policies, ECS task definitions, and Lambda environment payloads. The official jsonencode reference documents escaping rules. Pair output review with the JSON formatter tool during development.

resource "aws_iam_role_policy" "app" {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = "${aws_s3_bucket.assets.arn}/*"
    }]
  })
}

jsondecode() parses JSON strings back into Terraform types. It helps when a data source returns a JSON blob in a single attribute.

base64encode() and base64decode() wrap user-data scripts and certificate bodies. The site Base64 encoder and decoder helps verify payloads before apply. urlencode() handles query string segments in API Gateway or CDN rules.

Type conversion and null handling

Explicit converters—tostring, tonumber, tobool, tolist, toset, tomap— satisfy strict provider schemas. A provider rejecting "443" as a string port when it expects a number is a classic fix-with-tonumber moment.

coalesce() returns the first non-null argument. coalescelist() does the same for lists. They replace long condition ? a : b chains when defaults cascade.

try() catches evaluation errors and returns a fallback:

vpc_id = try(data.aws_vpc.existing[0].id, aws_vpc.new.id)

Use try() sparingly. It can mask typos in attribute names until a downstream resource fails mysteriously. Prefer explicit count or for_each guards when structure is known.

Type Conversion DecisionsProvider expects type Xcheck schema docs firstHCL typematches?yesPass throughnojsonencodefor policy stringstonumberfor numeric portstoset / tolistfor for_each inputRun terraform validate before CI
Encoding and type conversion functions among Terraform functions you should know—match HCL types to provider schemas before plan

Which Terraform file and network functions do production stacks rely on?

File functions read content from disk at plan time. Network functions compute CIDR layouts without a spreadsheet.

file, templatefile, and fileset

file() loads a static file as a string. Common for SSH public keys and small JSON snippets:

resource "aws_key_pair" "deploy" {
  public_key = file("${path.module}/keys/deploy.pub")
}

templatefile() renders templates with variable injection—ideal for cloud-init and user_data scripts:

locals {
  cloud_init = templatefile("${path.module}/templates/cloud-init.tpl", {
    hostname = local.hostname
    admin_ip = var.admin_cidr
  })
}

fileset() discovers files matching a glob inside a module. Dynamic blocks in Terraform dynamic blocks often iterate over fileset() results to attach one block per config fragment.

fileexists() returns a boolean without reading content. It supports conditional module behaviour, though explicit variables are usually clearer.

cidrsubnet, cidrhost, and cidrsubnets

cidrsubnet(prefix, newbits, netnum) splits address space. It is essential for VPC modules:

locals {
  public_cidrs = [
    for i in range(var.az_count) :
    cidrsubnet(var.vpc_cidr, 4, i)
  ]
  private_cidrs = [
    for i in range(var.az_count) :
    cidrsubnet(var.vpc_cidr, 4, i + var.az_count)
  ]
}

cidrhost() picks a host IP inside a subnet—often the .1 gateway address. cidrsubnets() splits into multiple subnets in one call when bit allocations vary.

I rely on these functions when provisioning Linux servers through workflows similar to our Linux system administration service. Correct CIDR math at plan time prevents overlapping subnets that only surface after apply.

timestamp, formatdate, and timeadd

Time functions generate ISO timestamps for tags or rotation schedules:

tags = {
  deployed_at = formatdate("YYYY-MM-DD'T'hh:mm:ssZ", timestamp())
}

timestamp() changes every plan, which can cause perpetual drift. Use it only where constant replacement is acceptable, or omit it from resources that force recreation. timeadd() and timecmp() help with certificate expiry comparisons.

Production Module Patternvariables: vpc_cidr, environment, app_configcidrsubnetpublic + private netsmerge + lookupstandard tagstemplatefilecloud-init scriptaws_instance + aws_security_groupjsonencode on ingress policyOutputs: subnet ids, fqdnconsumed by CI via remote state
Real-world stack combining Terraform functions you should know—network, tags, templates, and JSON policies in one module

Functions inside CI and remote state workflows

Functions do not run inside Terraform Cloud or GitHub Actions themselves. They execute when CI invokes terraform plan. Keep function-heavy logic in modules versioned and pinned like any provider.

Remote state consumers read outputs—often built with functions upstream. Patterns from managing Terraform state safely and remote backend configuration apply unchanged. Functions affect what lands in state, not where state lives.

Wrap plans in Terraform CI/CD with GitHub Actions or GitLab pipelines—the same approach I use on sister-site deployments with Deployer on EC2. Run terraform fmt -check and validate before merge. Long one-line function chains fail readability reviews faster than they fail syntax checks.

abspath, dirname, basename, and path.module keep file references portable across Terraform workspaces and environments. Never hard-code absolute paths from your laptop.

When logic grows beyond what functions express cleanly, reach for Terragrunt to keep Terraform DRY or split into child modules. Functions are not a substitute for structure.

For comparison with post-provision configuration, see Terraform vs Ansible. Terraform functions shape infrastructure declarations; Ansible templates handle machine state after boot.

A booking platform like Adventure Third Pole Trek might use exactly this module pattern: cidrsubnet for network isolation, merge for cost-allocation tags, and templatefile for app server bootstrap. Functions stay identical whether the workload serves trek bookings or legal document portals.

The HCL expression syntax guide on HashiCorp's expressions page documents operator precedence and nesting rules. Read it once when moving from copy-paste to authoring complex locals.

Key Takeaways

  • Learn format, merge, lookup, and for first—they appear in nearly every production module.
  • Use jsonencode for IAM and API JSON strings; validate output with a formatter before apply.
  • Chain lower and replace to sanitise names; test regex patterns outside Terraform.
  • Reach for cidrsubnet instead of manual IP spreadsheets when splitting VPC address space.
  • Treat try as a last resort—it can hide attribute typos until apply fails downstream.
  • Run terraform validate in CI after changing locals that depend on functions.

People Also Ask

Can you create custom functions in Terraform?

No. Terraform only supports built-in functions plus provider-specific functions documented per provider. Teams that need custom logic use modules, external data sources, or preprocess with Terragrunt. Keep complex business rules in application code, not HCL.

What is the difference between functions and locals in Terraform?

Functions transform values; locals assign names to expressions. You almost always call functions inside locals blocks to keep resource declarations readable. A local can reference other locals, building a pipeline of function results.

Do Terraform functions work with OpenTofu?

Yes. OpenTofu maintains compatibility with Terraform's function set for the fork versions in active use during 2026. Syntax and behaviour match for core functions like merge and jsonencode. Always pin versions in production regardless of distribution.

Why does my jsonencode plan show escaped characters?

jsonencode produces a valid JSON string with required escaping for HCL display. The provider receives correct JSON at apply. Inspect rendered values with terraform console or a temporary output block during development—not by reading the raw plan diff alone.

Put these Terraform functions to work on your stack

The Terraform functions you should know are not exotic language features. They are the everyday tools that keep modules short, names safe, and policies valid. Start by refactoring one repeated string concat into format, one tag map into merge, and one hand-written JSON blob into jsonencode. Run plan in a branch before touching production state.

If you want help structuring modules, CI pipelines, or VPS provisioning for a Nepal or remote team, contact us or explore enterprise application development options. Solid function usage in HCL is a small detail that pays off every time infrastructure scales.

Frequently Asked Questions

Built-in helpers in HashiCorp Configuration Language that evaluate during terraform plan, not on remote servers at apply time. They transform strings, maps, lists, JSON, CIDR blocks, and file content inside locals, variables, resource arguments, and outputs.

No. Terraform only supports built-in functions plus provider-specific ones. Teams use modules, external data sources, Terragrunt preprocessing, or application code for custom logic instead.

Functions transform values; locals assign names to expressions. You almost always call functions inside locals blocks so resource declarations stay readable. Locals can reference other locals, building a pipeline of function results evaluated at plan time.

Yes. OpenTofu maintains compatibility with Terraform's core function set for active 2026 fork versions. Syntax and behaviour match for functions like merge, jsonencode, and cidrsubnet. Pin versions in production regardless of distribution.

Start with format, merge, lookup, and for—they appear in nearly every production module I maintain. Add jsonencode for IAM and API JSON strings, cidrsubnet for VPC subnet math, templatefile for cloud-init scripts, and file for loading SSH keys. Coalesce and try handle null fallbacks but try should be a last resort because it can hide attribute typos until downstream resources fail mysteriously during apply.

Chain lower() to normalise input, replace() to strip illegal characters from user-supplied strings, then format() to append environment suffixes and indices. On VPS provisioning stacks, this hostname pattern prevents cloud API rejections. join() and split() round-trip list data through delimited strings for tag builders. Test regex() and regexall() patterns outside Terraform first—a bad pattern fails the entire plan before any resource is created.

merge() combines maps where later keys override earlier ones. The standard pattern layers default tags such as managed_by and project with environment-specific overrides from var.extra_tags. Use lookup() when a key might be absent—it returns a default instead of erroring, which suits optional feature flags like instance_type = lookup(var.instance_overrides, var.environment, "t3.small") in shared modules consumed across staging and production.

for builds new lists or maps from existing ones at plan time. Common patterns include extracting resource IDs with [for s in aws_subnet.private : s.id] or transforming variable maps with { for k, v in var.subnets : k => v.cidr }. Pair for with flatten() after generating nested rule lists, distinct() before for_each, and setsubtract() when validating CIDR allow lists. Output type must match the context—lists and maps are not interchangeable without explicit conversion.

Cloud APIs expect JSON strings, not HCL types. jsonencode() serialises Terraform values into valid JSON for IAM policies, ECS task definitions, and Lambda environment payloads. During development, review output with terraform console, a temporary output block, or a JSON formatter—plan diffs show escaped characters, but the provider receives correct JSON at apply. jsondecode() parses JSON blobs returned by data sources back into Terraform types when a single attribute holds encoded content.

jsonencode produces a valid JSON string with required escaping for HCL display in plan output. The provider receives correct JSON at apply time. Inspect rendered values with terraform console or a temporary output block during development—not by reading the raw plan diff alone, which always shows backslashes and quotes that look wrong but are syntactically correct for the target API.

cidrsubnet(prefix, newbits, netnum) splits address space without manual IP spreadsheets. A typical VPC module generates public and private CIDRs per availability zone using for with range(var.az_count). cidrhost() picks gateway addresses like .1 inside a subnet. cidrsubnets() splits into multiple subnets when bit allocations vary. Correct CIDR math at plan time prevents overlapping subnets that only surface after apply, which I rely on when provisioning Linux servers through infrastructure-as-code workflows.

file() loads static content as a string—common for SSH public keys and small JSON snippets from path.module paths. templatefile() renders templates with variable injection, ideal for cloud-init and user_data scripts where hostname and admin_ip values change per environment. fileset() discovers files matching a glob for dynamic blocks that attach one block per config fragment. fileexists() returns a boolean for conditional behaviour, though explicit variables are usually clearer than filesystem checks.

coalesce() returns the first non-null argument, replacing long conditional chains when defaults cascade. try() catches evaluation errors and returns a fallback, such as vpc_id = try(data.aws_vpc.existing[0].id, aws_vpc.new.id). Use try() sparingly—it can mask typos in attribute names until a downstream resource fails mysteriously. Prefer explicit count or for_each guards when structure is known. Explicit converters like tonumber fix provider schema mismatches, such as ports expecting numbers not strings.

timestamp() generates a new ISO value on every plan because functions evaluate at plan time, not once at apply. Tags like deployed_at = formatdate("YYYY-MM-DD'T'hh:mm:ssZ", timestamp()) force constant replacement on resources that trigger recreation when tags change. Use timestamp() only where constant replacement is acceptable, or omit it from resources that force recreation. timeadd() and timecmp() suit certificate expiry comparisons where the comparison logic matters more than a live clock value.

Functions execute when CI invokes terraform plan on the runner, not inside Terraform Cloud, GitHub Actions themselves, or on provisioned servers at apply time. Keep function-heavy logic in versioned, pinned modules like any provider. Remote state consumers read outputs often built with functions upstream—functions affect what lands in state, not where state lives. Run terraform fmt -check and validate before merge; long one-line function chains fail readability reviews faster than syntax checks.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: