Terraform Expressions and Operators with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user; sudo only if Terraform is not installed yet
Scope Terraform expressions and operators — references, arithmetic, comparison, logical, conditional, for and splat syntax, string templates, collection access, unknown values, brief type conversion, terraform console testing, and common expression errors. Does not cover the full function catalog, count or for_each meta-arguments, dynamic blocks, HCL basics, or the complete type system.
Related guides Terraform variables
Terraform locals
Terraform data types
Terraform resource dependencies
Terraform Associate certification course

A Terraform expression turns inputs into the values your configuration needs. Instead of hard-coding every argument, you combine references, operators, and functions:

hcl
locals {
  full_name = "${var.environment}-${var.application}"
}

Resource arguments, output values, and local blocks all accept expressions — not only string literals. The rest of this guide walks through each operator family and expression form with terraform console on Terraform 1.15.8.

Most commands run in ~/terraform-labs/terraform-expressions/. The unknown-values demo uses a separate unknown-demo/ subdirectory so you do not delete state in the main lab.

NOTE
Use the Terraform lab environment on Ubuntu. Run terraform init in each directory before console or plan. Examples use the built-in terraform_data resource so you do not need cloud credentials.

How Terraform expressions work

An expression is any HCL value Terraform can evaluate: a literal, a reference, an operator chain, or a function call. The result must match the type the argument expects — a number for count, a string for a label, a list for an attribute that accepts collections.

Literal values vs expressions

Literals are fixed values written directly:

hcl
input = "dev-web"
replicas = 2
enabled  = true

Expressions compute values from other data:

hcl
input = "${var.environment}-${var.application}"
replicas = var.base_replicas * 2
enabled  = var.environment == "prod"

The distinction matters when you read plan output: literals appear exactly as written; expressions show the evaluated result after Terraform resolves references.

References in expressions

Expressions read values from elsewhere in the module:

text
var.<name>                    input variable
local.<name>                  local value
<resource_type>.<name>.<attr> managed resource attribute
data.<type>.<name>.<attr>     data source attribute

For example, var.environment comes from a Terraform variables block, local.full_name from Terraform locals, and terraform_data.seed.output from a managed resource. Resource and data references create dependency edges; see Terraform resource dependencies for graph behavior. This lesson focuses on syntax and evaluation, not dependency ordering.


Prepare the expressions lab

Create the working directory and write one variables.tf that supports every console demo in the main lab — operators, for expressions, templates, and collection access:

bash
mkdir -p ~/terraform-labs/terraform-expressions && cd ~/terraform-labs/terraform-expressions

Write variables.tf with the inputs every console demo in the main lab references:

bash
cat > variables.tf <<'EOF'
variable "environment" {
  type    = string
  default = "dev"
}

variable "application" {
  type    = string
  default = "web"
}

variable "replicas" {
  type    = number
  default = 2
}

variable "names" {
  type    = list(string)
  default = ["api", "worker", "api"]
}

variable "services" {
  type    = list(string)
  default = ["api", "worker"]
}

variable "settings" {
  type = map(string)
  default = {
    region = "us-east-1"
    tier   = "standard"
  }
}

variable "tags" {
  type = object({
    team = string
    cost = number
  })
  default = {
    team = "platform"
    cost = 100
  }
}
EOF

Add main.tf with locals that use string interpolation and a template directive:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

locals {
  full_name = "${var.environment}-${var.application}"

  service_block = <<-EOT
%{ for svc in var.services ~}
- ${svc}
%{ endfor ~}
EOT
}

resource "terraform_data" "example" {
  input = local.full_name
}

output "full_name" {
  value = local.full_name
}

output "service_block" {
  value = local.service_block
}
EOF

Initialize the directory:

bash
terraform init -input=false

Sample output:

output
Terraform has been successfully initialized!

Confirm the string template local evaluates as expected:

bash
echo 'local.full_name' | terraform console
output
"dev-web"

local.full_name combines two variable references with ${} string interpolation — the pattern most modules use for derived names.


Operators and conditional expressions

Terraform supports the arithmetic, comparison, and logical operators below. Test each one in terraform console before wiring it into a resource argument.

Arithmetic operators

HashiCorp documents arithmetic operators for number values. + is numeric addition only — not string concatenation.

Operator Meaning
+ Numeric addition
- Subtraction
* Multiplication
/ Division
% Remainder

Multiply the replica count:

bash
echo 'var.replicas * 3' | terraform console
output
6

The remainder operator works on whole numbers:

bash
echo '10 % 3' | terraform console
output
1

Combine strings with interpolation ("${var.environment}-${var.application}") or functions such as format() — not the + operator.

Comparison operators

Operator Meaning
==, != Equality / inequality
<, >, <=, >= Numeric comparison

Check whether the replica count meets a minimum:

bash
echo 'var.replicas >= 2' | terraform console
output
true

Compare the environment label to a literal with equality — not ordering operators:

bash
echo 'var.environment == "prod"' | terraform console
output
false

Comparison results are booleans you can feed into conditional expressions.

Logical operators

Operator Meaning
! Logical NOT
&& Logical AND
|| Logical OR

Combine a numeric guard with a non-empty string check:

bash
echo 'var.replicas > 0 && var.environment != ""' | terraform console
output
true

Negation flips a boolean:

bash
echo '!false' | terraform console
output
true

Operator precedence

Multiplication binds tighter than addition, just as in ordinary arithmetic. Without parentheses, 2 + 3 * 4 evaluates the product first:

bash
echo '2 + 3 * 4' | terraform console
output
14

Parentheses override the default order:

bash
echo '(2 + 3) * 4' | terraform console
output
20

When an expression mixes comparison, logical, and arithmetic operators, add parentheses until the intent is obvious — both for readers and for Terraform's parser.

Conditional expressions

A conditional expression picks one of two values based on a boolean condition:

hcl
condition ? true_value : false_value

The condition must evaluate to true or false. Both result branches must produce compatible types — Terraform rejects a list on one side and a string on the other.

Build an environment label from a comparison:

bash
echo 'var.environment == "prod" ? "production" : "non-production"' | terraform console
output
"non-production"

With var.environment defaulting to dev, the condition is false and the second branch wins.

Null handling often appears in conditionals. A common null check uses value != null as the boolean condition. If the value is null, that condition is false and Terraform selects the false branch:

bash
echo 'null != null ? "has value" : "missing"' | terraform console
output
"missing"

You can also return null from a branch when a value is optional:

bash
echo 'true ? null : "fallback"' | terraform console
output
tostring(null)

Mismatched structural types fail at evaluation time. This expression mixes a list with a string:

bash
echo 'true ? [] : "no"' | terraform console
output
Error: Inconsistent conditional result types

  on <console-input> line 1:
   (source code not available)

The true and false result expressions must have consistent types. The given
values are tuple and string, respectively.

Some primitive mixes may coerce in Terraform 1.15.8, but collection and object branches must agree. When in doubt, wrap both sides with tostring() or reshape them to the same type.


Collection expressions

for expressions

A for expression transforms or filters a collection. It is the idiomatic replacement for hand-written loops when building lists or maps from variables.

List transformation syntax:

hcl
[for <NAME> in <COLLECTION> : <TRANSFORM>]

Uppercase every service name, including duplicates:

bash
echo '[for n in var.names : upper(n)]' | terraform console
output
[
  "API",
  "WORKER",
  "API",
]

upper() is a built-in function; this lesson uses it only where it clarifies the transform. See Terraform functions for the full catalog.

Add an if clause to keep elements that match a condition:

bash
echo '[for n in var.names : upper(n) if n != "api"]' | terraform console
output
[
  "WORKER",
]

Only worker survives the filter because both api entries are excluded.

Brace syntax produces a map. You can rename keys or transform values:

hcl
{for <KEY>, <VALUE> in <MAP> : <NEW_KEY> => <NEW_VALUE>}

Uppercase every value while keeping the same keys:

bash
echo '{for k, v in var.settings : k => upper(v)}' | terraform console
output
{
  "region" = "US-EAST-1"
  "tier" = "STANDARD"
}

for expressions are usually clearer than splat syntax when you need filtering, key remapping, or per-element logic.

Splat expressions

A splat expression projects the same attribute from each element of a list-like value. A common Terraform use is collecting attributes from resource instances created with count:

hcl
<resource_type>.<name>[*].<attribute>

Resources created with for_each form a map rather than a list, so a for expression is usually clearer there.

Save the main lab configuration before replacing main.tf for the splat demo:

bash
cp main.tf main.tf.lab

Write a counted terraform_data module:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "svc" {
  count = length(var.services)
  input = var.services[count.index]
}

output "service_inputs" {
  value = terraform_data.svc[*].input
}
EOF

Apply so the resources exist, then read the splat result:

bash
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

service_inputs = [
  "api",
  "worker",
]

The splat terraform_data.svc[*].input collects each instance's input attribute into a list. For anything beyond a straight attribute projection — filtering, string formatting, key changes — prefer a for expression.

Remove the counted resources and restore the main lab module:

bash
terraform destroy -auto-approve -input=false
mv main.tf.lab main.tf

terraform destroy removes the two terraform_data instances; main.tf.lab puts the template locals back for the next section.


String templates and collection access

String interpolation

When the entire argument is a reference, write var.environment directly. When you need to combine an expression with surrounding text, use string interpolation such as "${var.environment}-web":

bash
echo '"${var.environment}-web"' | terraform console
output
"dev-web"

Inside a quoted string, ${ ... } is the interpolation syntax HashiCorp defines for embedding expressions in strings.

Template directives

The main lab main.tf already defines local.service_block with %{ for ... } directives inside a heredoc. Apply to render the template:

bash
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Outputs:

service_block = <<EOT
- api
- worker

EOT

The ~ strips extra whitespace around each directive. This lesson stops at inline templates; file-based templatefile() belongs in Terraform functions.

Index and attribute access

Access list/tuple elements by zero-based index:

bash
echo 'var.names[0]' | terraform console
output
"api"

Access map elements by key with bracket notation:

bash
echo 'var.settings["region"]' | terraform console
output
"us-east-1"

Access object attributes with dot notation:

bash
echo 'var.tags.team' | terraform console
output
"platform"

Bracket and dot syntax both work on objects when the attribute name is a valid identifier. For dynamic keys on maps, use brackets.


Unknown values and type conversion

Expressions with unknown values

Some attribute values do not exist until Terraform creates the resource during apply. Expressions that reference those attributes stay unknown at plan time.

Use a separate subdirectory so the main lab state is untouched:

bash
mkdir -p ~/terraform-labs/terraform-expressions/unknown-demo
cd ~/terraform-labs/terraform-expressions/unknown-demo

Write a minimal module that reads terraform_data.seed.output:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "seed" {
  input = "seed"
}

locals {
  derived = terraform_data.seed.output
}

output "derived" {
  value = local.derived
}
EOF

Initialize the subdirectory, then plan to see unknown attribute values on first create:

bash
terraform init -input=false
terraform plan -input=false -no-color

Sample output:

output
# terraform_data.seed will be created
  + resource "terraform_data" "seed" {
      + id     = (known after apply)
      + output = (known after apply)
    }

Changes to Outputs:
  + derived = (known after apply)

local.derived depends on terraform_data.seed.output, so the output stays unknown until apply even though the expression syntax is valid. After apply, the value becomes concrete ("seed" for terraform_data). Do not delete state files to force this behavior — use a fresh directory or a first-time plan instead.

Return to the main lab for type conversion examples:

bash
cd ~/terraform-labs/terraform-expressions

Brief type conversion

Terraform converts between compatible types automatically in many contexts. Explicit conversion functions make intent clear. Full type rules live in Terraform data types.

Parse a string digit sequence into a number:

bash
echo 'tonumber("42")' | terraform console
output
42

Format a number as a string:

bash
echo 'tostring(var.replicas)' | terraform console
output
"2"

Invalid conversions fail at evaluation time — for example tonumber("not-a-number") errors in console the same way a bad variable assignment would during plan.


Common Terraform expression errors

Save the main lab configuration before overwriting files for the error demos:

bash
cd ~/terraform-labs/terraform-expressions
cp main.tf main.tf.lab
cp variables.tf variables.tf.lab

The subsections below replace main.tf and variables.tf with minimal broken examples.

Invalid index

Point at an index beyond the list length:

bash
cd ~/terraform-labs/terraform-expressions
cat > variables.tf <<'EOF'
variable "names" {
  type    = list(string)
  default = ["api"]
}
EOF
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
output "x" {
  value = var.names[5]
}
EOF

Console surfaces the out-of-range index:

bash
echo 'var.names[5]' | terraform console
output
Error: Invalid index

  on main.tf line 5, in output "x":
   5:   value = var.names[5]
    ├────────────────
    │ var.names is list of string with 1 element

Index 5 is out of range for a one-element list. Bracket access to a missing map key reports the same Invalid index error.

Missing map key

Request a map key that does not exist:

bash
cat > variables.tf <<'EOF'
variable "settings" {
  type = map(string)
  default = {
    region = "us-east-1"
  }
}
EOF
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
output "x" {
  value = var.settings["zone"]
}
EOF

Bracket access to a missing key fails the same way in console:

bash
echo 'var.settings["zone"]' | terraform console
output
Error: Invalid index

  on main.tf line 5, in output "x":
   5:   value = var.settings["zone"]
    ├────────────────
    │ var.settings is map of string with 1 element

The key zone is not present. Use lookup() with a default or try() when a missing key should not fail the expression.

Invalid operand

Reset to a minimal module with no variables, then add a string to a number:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
EOF

The + operator rejects mixed operand types:

bash
echo '"hello" + 5' | terraform console
output
Error: Invalid operand

  on <console-input> line 1:
   (source code not available)

Unsuitable value for left operand: a number is required.

Use string interpolation or format() to combine strings with numbers.

Malformed for expression

A for expression needs a transform expression after the colon. Use a literal collection so the demo does not depend on variables left over from earlier error examples:

bash
echo '[for n in ["api", "worker"] :]' | terraform console
output
Error: Invalid expression

  on <console-input> line 1:
   (source code not available)

Expected the start of an expression, but found an invalid expression token.

Quick troubleshooting reference

Symptom Likely cause Fix
Invalid index List index out of range or map key absent Check length; use lookup() or try() when a missing key should not fail
Inconsistent conditional result types ? branches return different types Make both branches the same type or convert explicitly
Invalid operand Operator used on wrong type Convert operands or pick the correct operator
Invalid expression in for Incomplete for syntax Add transform after :; add if only after the transform
(known after apply) in plan Expression uses not-yet-created resource attribute Expected before first apply; re-plan after apply

Restore the main lab configuration before cleanup:

bash
mv main.tf.lab main.tf
mv variables.tf.lab variables.tf

Cleanup

Destroy resources in the main lab and the unknown-values subdirectory:

bash
cd ~/terraform-labs/terraform-expressions && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-expressions/unknown-demo && terraform destroy -auto-approve -input=false 2>/dev/null || true

References


Summary

Terraform expressions let you compute argument values instead of hard-coding literals. You started with local.full_name = "${var.environment}-${var.application}" and expanded into numeric arithmetic, equality and numeric comparison, logical operators, conditional ternary expressions, for transforms with optional if filters, and splat projection for list-like resource collections.

terraform console is the fastest way to validate an expression before you embed it in a resource block. Combine strings with ${} interpolation or format(), not the + operator. Resource references use <type>.<name>.<attr> — for example terraform_data.seed.output — without a resource. prefix.

Watch for unknown values: expressions that reference resource attributes not yet in state show (known after apply) during plan, which is normal on first create in a fresh directory. Type errors — invalid indexes, mismatched conditional branches, bad operands — surface in console the same way they would during plan. For conversion rules and constraint details, continue with Terraform data types; for wiring inputs into expressions, review Terraform variables and Terraform locals.


Frequently Asked Questions

1. What is a Terraform expression?

A Terraform expression is any value written in HCL that Terraform evaluates to produce a result, such as a string, number, list, or boolean. Expressions combine literals, references like var.name, operators, and functions to compute argument values dynamically instead of hard-coding literals in every block.

2. What is the difference between a conditional expression and a for expression?

A conditional expression chooses between two values based on a boolean condition using condition ? true_value : false_value. A for expression transforms or filters a collection, producing a new list or map from an input list or map.

3. When should I use a splat expression?

Use a splat such as terraform_data.svc[*].input when you need the same attribute projected from each element of a list-like value, commonly from resource instances created with count. Use a for expression when you need filtering, key remapping, or attributes from for_each resources, which form maps rather than lists.

4. How do I test Terraform expressions before apply?

Run terraform console in the working directory and type the expression at the REPL prompt, or pipe one expression per line with echo 'expression' | terraform console. Console evaluates against the loaded configuration and variable defaults without changing infrastructure.

5. What does known after apply mean in a Terraform expression?

It means Terraform cannot compute the value until a provider operation completes during apply. Expressions that reference resource attributes not yet created stay unknown during plan even when the surrounding syntax is valid.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)