Terraform’s expression and function system is the core “thinking engine” behind your configurations: expressions describe what value an argument should have, and functions are reusable tools you invoke inside those expressions to compute values dynamically.


1. What Is an Expression in Terraform?

An expression is any piece of HCL that Terraform can evaluate to a concrete value: a string, number, bool, list, map, or object. You use expressions in almost every place on the right-hand side of arguments, in locals, count, for_each, dynamic blocks, and more.

Common expression forms:

  • Literals: "hello", 5, true, null, ["a", "b"], { env = "dev" }.
  • References: var.region, local.tags, aws_instance.app.id, module.vpc.vpc_id.
  • Operators: arithmetic (+ - * / %), comparisons (< > <= >=), equality (== !=), logical (&& || !).
  • Conditionals: condition ? value_if_true : value_if_false.
  • for expressions: [for s in var.subnets : upper(s)] to transform collections.
  • Splat expressions: aws_instance.app[*].id to project attributes out of collections.

Rationale: Terraform must stay declarative (you describe the desired state), but real infrastructure is not static; expressions give you a minimal “language” to derive values from other values without dropping into a general-purpose programming language.


2. What Is a Function?

A function is a built‑in helper you call inside expressions to transform or combine values. The syntax is a function name followed by comma‑separated arguments in parentheses, for example max(5, 12, 9). Functions always return a value, so they can appear anywhere a normal expression is allowed.

Key properties:

  • Terraform ships many built‑in functions (string, numeric, collection, IP/network, crypto, time, type conversion, etc.).
  • You cannot define your own functions in HCL; you only use built‑ins, plus any provider-defined functions a provider may export.
  • Provider-defined functions are namespaced like provider::<local-name>::function_name(...) when used.

Examples of useful built‑in functions:

  • String: upper("dev"), lower(), format(), join("-", ["app", "dev"]).
  • Numeric: max(5, 12, 9), min(), ceil(), floor().
  • Collection: length(var.subnets), merge(local.tags, local.extra_tags), flatten().

Rationale: Functions cover the common transformation needs (naming, list/map manipulation, math) so that your Terraform remains expressive but compact, and you avoid copy‑pasting “string‑mangling” logic everywhere.


3. How Expressions and Functions Work Together

Terraform’s model is expression‑centric: on the right‑hand side of almost every argument, you write an expression, and function calls are just one kind of expression. You freely compose references, operators, conditionals, for expressions, and functions, as long as the input and output types match.

Typical composition patterns:

  • Use references (var.*, local.*, resource attributes) as the base inputs.
  • Apply operators and conditional expressions to make decisions (var.env == "prod" ? 3 : 1).
  • Use for expressions and collection functions to reshape data ([for s in var.subnets : upper(s)]).
  • Use string functions to build consistent resource names (format("%s-%s", var.app, var.env)).

From a mental-model perspective, a good way to think about this is: “Everything dynamic lives in expressions; functions are building blocks inside those expressions.”


4. A Small Example

Below is a minimal Terraform configuration that showcases expressions and functions together, and that you can actually run to observe evaluation results.

Example configuration (main.tf)

terraform {
  required_version = ">= 1.6"
}

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

variable "app_servers" {
  type    = list(string)
  default = ["app-1", "app-2", "app-3"]
}

locals {
  # Expression: equality operator -> bool
  is_prod = var.environment == "prod"

  # Literal map and reference
  base_tags = {
    app         = "payments"
    environment = var.environment
  }

  # For expression + string function
  uppercased_servers = [for s in var.app_servers : upper(s)]

  # Merge and format functions to compute a name once
  common_tags = merge(
    local.base_tags,
    {
      name = format(
        "%s-%s-%02d",
        local.base_tags.app,
        local.base_tags.environment,
        length(var.app_servers)
      )
    }
  )
}

output "summary" {
  value = {
    is_prod            = local.is_prod
    uppercased_servers = local.uppercased_servers
    common_tags        = local.common_tags
  }
}

What this demonstrates conceptually:

  • Expressions:
    • var.environment == "prod" produces a bool for local.is_prod.
    • The map in local.base_tags uses both literals and references.
    • The locals block itself is a way to give names to intermediate expressions.
  • Functions:
    • upper(s) transforms each server name to uppercase inside a for expression.
    • length(var.app_servers) computes the number of servers.
    • format("%s-%s-%02d", ...) builds a stable name string.
    • merge(...) combines two maps into a single tag map.

Rationale: This pattern—variables + locals + expressions + functions—is exactly how you avoid repetition and keep a production Terraform codebase readable as it grows.


5. How to Validate This Example

Terraform provides an expression console and standard workflow commands to validate that your expressions and functions behave as expected before they affect real infrastructure.

Option A: Run the configuration

  1. Save the example as main.tf in an empty directory.
  2. Run:
    • terraform init to set up the working directory.
    • terraform apply -auto-approve to evaluate and show outputs.
  3. Observe the summary output:
    • is_prod should be false (with the default environment dev).
    • uppercased_servers should be ["APP-1", "APP-2", "APP-3"].
    • common_tags.name should be payments-dev-03.

To see how expressions react to different inputs, run again with a different environment:

terraform apply -auto-approve -var 'environment=prod'

Now is_prod will be true, and the computed name will switch to payments-prod-03, even though you haven’t changed any resource definitions.

Option B: Experiment interactively with terraform console

Terraform’s console lets you test expressions and functions on the fly.

From the same directory:

terraform console

Then try:

> 1 + 2 * 3
> var.environment == "prod"
> [for s in var.app_servers : upper(s)]
> merge({a = 1}, {b = 2})
> format("%s-%s", "app", var.environment)

You will see the evaluated results immediately, which is ideal for teaching yourself how a particular expression or function behaves before embedding it into a real module.


6. Summary Table: Expressions vs Functions

Aspect Expressions Functions
Purpose Describe how to compute a value. Provide reusable operations used inside expressions.
Examples var.env == "prod", [for s in xs : x.id]. length(var.subnets), join("-", local.tags).
Defined by Terraform language syntax. Terraform’s built‑in and provider-defined function library.
Customization Composed via locals, variables, and blocks. No user-defined functions in HCL; only built‑ins/providers.
Typical usage domain Conditionals, loops, references, constructing structures. String formatting, math, collection manipulation, conversion.