Skip to content
Infrastructure as Code iac hcl 5 min read

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.

PrefixRefers toExample
var.<name>An input variablevar.region
local.<name>A local valuelocal.common_tags
<type>.<name>.<attr>A managed resource attributeaws_vpc.main.id
data.<type>.<name>.<attr>A data source attributedata.aws_ami.ubuntu.id
module.<name>.<output>A child module outputmodule.network.vpc_id
each.key / each.valueThe current for_each itemeach.value.cidr
count.indexThe current count indexcount.index
path.moduleFilesystem 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.id not 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
}
OperatorMeaningExampleResult
+Addition2 + 35
-Subtraction / unary5 - 23
*Multiplication4 * 28
/Division9 / 24.5
%Modulo7 % 31

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
}
OperatorMeaning
==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"
}
OperatorMeaning
&&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, and module. 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 from count-based resources instead of writing manual for loops.
  • Remember integer division yields a float (10 / 4 is 2.5); use the floor/ceil/floordiv functions 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 locals so each expression is named, testable, and reused rather than repeated.
Last updated June 14, 2026
Was this helpful?