Expressions & Operators
Expressions are how you compute and refer to values in HCL. Every argument value — whether it is a literal, a reference to another resource, or the result of arithmetic — is an expression that Terraform evaluates while building its dependency graph. Mastering expressions is what turns static configuration into reusable, dynamic infrastructure, because references between objects are also what let Terraform figure out the correct order to create things. Everything here works identically in Terraform 1.5+ and OpenTofu, which share the HCL2 grammar.
References
The most important kind of expression is a reference to a named value elsewhere in your configuration. References create implicit dependencies, so the referenced object is always created or read before the one using it. There are several reference namespaces, each addressed by a prefix.
resource "aws_instance" "web" {
ami = var.ami_id # input variable
instance_type = local.instance_type # local value
subnet_id = aws_subnet.app.id # another resource's attribute
key_name = module.keys.key_name # a module output
}
The table summarizes the reference forms you will use daily.
| Prefix | Refers to | Example |
|---|---|---|
var.<name> | An input variable | var.region |
local.<name> | A local value | local.common_tags |
<type>.<name>.<attr> | A managed resource attribute | aws_vpc.main.id |
data.<type>.<name>.<attr> | A data source attribute | data.aws_ami.ubuntu.id |
module.<name>.<output> | A child module output | module.network.vpc_id |
each.key / each.value | The current for_each item | each.value.cidr |
count.index | The current count index | count.index |
path.module | Filesystem path of the module | "${path.module}/init.sh" |
Resource attributes whose values are only known after the resource is created appear as (known after apply) in the plan — Terraform tracks these as unknown values and resolves them during apply.
Tip: Prefer references over hard-coded IDs. A reference like
aws_subnet.app.idnot only stays correct when the underlying ID changes, it also tells Terraform about the dependency so it never tries to create the instance before the subnet.
Arithmetic operators
HCL supports the standard numeric operators. Operands are coerced to numbers where possible, and division of integers still produces a floating-point result.
locals {
base_count = 3
total_count = local.base_count * 2 + 1 # 7
half = 10 / 4 # 2.5
remainder = 10 % 3 # 1
negated = -local.base_count # -3
}
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ | Addition | 2 + 3 | 5 |
- | Subtraction / unary | 5 - 2 | 3 |
* | Multiplication | 4 * 2 | 8 |
/ | Division | 9 / 2 | 4.5 |
% | Modulo | 7 % 3 | 1 |
Comparison and equality operators
Comparison operators return a bool. Equality works across any type, including objects and lists, which are compared structurally.
locals {
is_prod = var.environment == "production"
has_capacity = var.instance_count > 0
not_empty = length(var.subnets) != 0
}
| Operator | Meaning |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
Logical operators
Logical operators combine boolean values. They are most useful inside conditional expressions and validation rules.
locals {
multi_az = var.environment == "production" && var.instance_count > 1
needs_review = !var.auto_approve
allow_public = var.is_demo || var.environment == "staging"
}
| Operator | Meaning |
|---|---|
&& | Logical AND |
|| | Logical OR |
! | Logical NOT |
Parentheses and precedence
Operators follow conventional precedence: unary !/- first, then * / %, then + -, then comparisons, then &&, then ||. Use parentheses to make intent explicit and override the default grouping.
locals {
# Without parens, && binds tighter than ||
result = (var.a || var.b) && var.c
}
When in doubt, add parentheses — they cost nothing and remove ambiguity for the next reader.
Splat expressions
A splat expression extracts an attribute from every element of a list in one concise form, using the [*] operator. It is the idiomatic way to collect attributes across resources created with count or for_each.
resource "aws_instance" "web" {
count = 3
ami = var.ami_id
instance_type = "t3.micro"
}
output "private_ips" {
value = aws_instance.web[*].private_ip # list of all private IPs
}
output "first_id" {
value = aws_instance.web[0].id # index a single element
}
The splat aws_instance.web[*].private_ip is shorthand for a for expression over the list. When applied to a single (non-list) value, the splat wraps it in a one-element list — handy for normalizing optional resources. For for_each resources, which produce a map rather than a list, splat works on values(aws_instance.web)[*].id.
Output:
Changes to Outputs:
+ first_id = (known after apply)
+ private_ips = [
+ (known after apply),
+ (known after apply),
+ (known after apply),
]
The three (known after apply) entries confirm the splat produced a list with one entry per instance, even before the IPs are assigned.
Best Practices
- Reference values with
var.,local., resource attributes, andmodule.rather than duplicating literals, so dependencies are tracked automatically. - Use parentheses to make operator precedence explicit, especially when mixing
&&and||. - Reach for splat expressions (
[*]) to gather attributes fromcount-based resources instead of writing manualforloops. - Remember integer division yields a float (
10 / 4is2.5); use thefloor/ceil/floordivfunctions when you need integer math. - Lean on equality (
==,!=) for structural comparison of lists, maps, and objects — there is no need to compare elements manually. - Keep complex computed values in
localsso each expression is named, testable, and reused rather than repeated.