| 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:
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.
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:
input = "dev-web"
replicas = 2
enabled = trueExpressions compute values from other data:
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:
var.<name> input variable
local.<name> local value
<resource_type>.<name>.<attr> managed resource attribute
data.<type>.<name>.<attr> data source attributeFor 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:
mkdir -p ~/terraform-labs/terraform-expressions && cd ~/terraform-labs/terraform-expressionsWrite variables.tf with the inputs every console demo in the main lab references:
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
}
}
EOFAdd main.tf with locals that use string interpolation and a template directive:
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
}
EOFInitialize the directory:
terraform init -input=falseSample output:
Terraform has been successfully initialized!Confirm the string template local evaluates as expected:
echo 'local.full_name' | terraform console"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:
echo 'var.replicas * 3' | terraform console6The remainder operator works on whole numbers:
echo '10 % 3' | terraform console1Combine 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:
echo 'var.replicas >= 2' | terraform consoletrueCompare the environment label to a literal with equality — not ordering operators:
echo 'var.environment == "prod"' | terraform consolefalseComparison 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:
echo 'var.replicas > 0 && var.environment != ""' | terraform consoletrueNegation flips a boolean:
echo '!false' | terraform consoletrueOperator precedence
Multiplication binds tighter than addition, just as in ordinary arithmetic. Without parentheses, 2 + 3 * 4 evaluates the product first:
echo '2 + 3 * 4' | terraform console14Parentheses override the default order:
echo '(2 + 3) * 4' | terraform console20When 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:
condition ? true_value : false_valueThe 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:
echo 'var.environment == "prod" ? "production" : "non-production"' | terraform console"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:
echo 'null != null ? "has value" : "missing"' | terraform console"missing"You can also return null from a branch when a value is optional:
echo 'true ? null : "fallback"' | terraform consoletostring(null)Mismatched structural types fail at evaluation time. This expression mixes a list with a string:
echo 'true ? [] : "no"' | terraform consoleError: 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:
[for <NAME> in <COLLECTION> : <TRANSFORM>]Uppercase every service name, including duplicates:
echo '[for n in var.names : upper(n)]' | terraform console[
"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:
echo '[for n in var.names : upper(n) if n != "api"]' | terraform console[
"WORKER",
]Only worker survives the filter because both api entries are excluded.
Brace syntax produces a map. You can rename keys or transform values:
{for <KEY>, <VALUE> in <MAP> : <NEW_KEY> => <NEW_VALUE>}Uppercase every value while keeping the same keys:
echo '{for k, v in var.settings : k => upper(v)}' | terraform console{
"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:
<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:
cp main.tf main.tf.labWrite a counted terraform_data module:
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
}
EOFApply so the resources exist, then read the splat result:
terraform apply -auto-approve -input=false -no-colorSample 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:
terraform destroy -auto-approve -input=false
mv main.tf.lab main.tfterraform 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":
echo '"${var.environment}-web"' | terraform console"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:
terraform apply -auto-approve -input=false -no-colorSample output:
Outputs:
service_block = <<EOT
- api
- worker
EOTThe ~ 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:
echo 'var.names[0]' | terraform console"api"Access map elements by key with bracket notation:
echo 'var.settings["region"]' | terraform console"us-east-1"Access object attributes with dot notation:
echo 'var.tags.team' | terraform console"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:
mkdir -p ~/terraform-labs/terraform-expressions/unknown-demo
cd ~/terraform-labs/terraform-expressions/unknown-demoWrite a minimal module that reads terraform_data.seed.output:
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
}
EOFInitialize the subdirectory, then plan to see unknown attribute values on first create:
terraform init -input=false
terraform plan -input=false -no-colorSample 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:
cd ~/terraform-labs/terraform-expressionsBrief 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:
echo 'tonumber("42")' | terraform console42Format a number as a string:
echo 'tostring(var.replicas)' | terraform console"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:
cd ~/terraform-labs/terraform-expressions
cp main.tf main.tf.lab
cp variables.tf variables.tf.labThe subsections below replace main.tf and variables.tf with minimal broken examples.
Invalid index
Point at an index beyond the list length:
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]
}
EOFConsole surfaces the out-of-range index:
echo 'var.names[5]' | terraform consoleError: Invalid index
on main.tf line 5, in output "x":
5: value = var.names[5]
├────────────────
│ var.names is list of string with 1 elementIndex 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:
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"]
}
EOFBracket access to a missing key fails the same way in console:
echo 'var.settings["zone"]' | terraform consoleError: Invalid index
on main.tf line 5, in output "x":
5: value = var.settings["zone"]
├────────────────
│ var.settings is map of string with 1 elementThe 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:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
EOFThe + operator rejects mixed operand types:
echo '"hello" + 5' | terraform consoleError: 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:
echo '[for n in ["api", "worker"] :]' | terraform consoleError: 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:
mv main.tf.lab main.tf
mv variables.tf.lab variables.tfCleanup
Destroy resources in the main lab and the unknown-values subdirectory:
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 || trueReferences
- Expressions — Terraform configuration language
- Types and values — Terraform configuration language
- terraform console command — Terraform CLI
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.

