Expressions and Functions
Every argument value in a Terraform configuration is an expression — even a plain string literal like "us-east-1" is technically the simplest possible expression. Expressions range from direct references (var.region, aws_vpc.main.id, local.name_prefix) to arithmetic and comparison operators, conditional (ternary) expressions, for expressions that transform collections, and calls into Terraform's large standard library of built-in functions. Because Terraform has no user-defined functions, the built-in library is what gives configurations real computational flexibility.
Cricket analogy: Even calling a shot 'defensive block' is technically a decision expression — from that simplest call, batting choices range from a straight bat block to a reverse-sweep, all evaluated in the moment the ball arrives, just like Terraform expressions from literals to functions.
References and Operators
References follow the pattern <TYPE>.<NAME>.<ATTRIBUTE> for resources, var.<name> for variables, local.<name> for locals, and module.<name>.<output> for child module outputs. Terraform supports the usual arithmetic (+, -, *, /, %), comparison (==, !=, <, >, <=, >=), and logical (&&, ||, !) operators, plus the conditional operator condition ? true_val : false_val for inline branching — commonly used to toggle a value based on an environment or feature flag.
Cricket analogy: A scorecard entry like team.batsman.runs mirrors Terraform's <TYPE>.<NAME>.<ATTRIBUTE> reference, and a captain's ternary call — rain forecast ? bat_first : bowl_first — is exactly the inline branching Terraform's conditional operator provides.
locals {
is_prod = var.environment == "production"
instance_type = local.is_prod ? "m5.large" : "t3.micro"
desired_count = local.is_prod ? 3 : 1
# for expression: transform a list into a map
subnet_by_az = {
for s in data.aws_subnets.selected.ids :
data.aws_subnet.each[s].availability_zone => s
}
# function calls
bucket_name = lower(replace("${var.project} Reports", " ", "-"))
cidr_block = cidrsubnet(var.vpc_cidr, 8, 2)
all_tags = merge(var.default_tags, { Name = local.bucket_name })
}for Expressions and splat
A for expression transforms one collection into another: [for x in list : upper(x)] produces a new list, and {for k, v in map : k => v} (or with a colon-separated key => value form) produces a new map. Optionally appending an if clause filters elements during the transformation. The splat operator, resource.*.attribute or more idiomatically resource[*].attribute, is a shorthand for extracting a given attribute from every instance of a resource created with count or for_each.
Cricket analogy: Converting a list of raw scores into strike rates — [for run in scores : run/balls*100] — is a for expression transforming a collection, and pulling every batsman's average with team.*.average is the splat operator's shorthand extraction.
Terraform's function library is organized by category — string functions (upper, lower, replace, format), collection functions (merge, concat, distinct, flatten), numeric functions (min, max, ceil), encoding functions (jsonencode, base64encode), and network functions (cidrsubnet, cidrhost) among others. The official documentation's function index is the fastest way to discover the right one rather than reinventing logic manually.
Functions like tostring(), tonumber(), tolist(), and tomap() perform explicit type conversion, useful when a value's inferred type doesn't match what a downstream argument expects. The coalesce() function returns the first non-null argument from a list, a common pattern for providing layered defaults, while try() attempts a sequence of expressions and returns the first one that doesn't produce an error — useful for gracefully handling optional or possibly-absent attributes.
Cricket analogy: Converting a scoreboard's string '287' to a number for run-rate math is exactly tonumber(), and coalesce() picking the first available score — live feed, else radio commentary, else scorecard — mirrors layered fallback defaults; try() gracefully handles a missing player stat.
for expressions and functions are pure and cannot have side effects or depend on execution order beyond normal dependency resolution — you cannot write imperative loops that mutate state across iterations. If you find yourself wanting genuinely procedural logic, that's usually a sign the work belongs in a provisioner, external script, or a purpose-built provider instead.
- Every argument value in HCL is an expression, from literals to function calls.
- References follow TYPE.NAME.ATTRIBUTE, var.NAME, local.NAME, and module.NAME.OUTPUT patterns.
- The conditional operator condition ? a : b enables inline branching.
- for expressions transform lists into lists or maps, optionally filtering with an if clause.
- The splat operator resource[*].attribute extracts an attribute across all instances of a multi-instance resource.
- Terraform provides no user-defined functions; complex logic composes the extensive built-in function library instead.
Practice what you learned
1. What does the conditional expression var.environment == "production" ? "m5.large" : "t3.micro" evaluate to when var.environment is "staging"?
2. What does a for expression like [for x in var.names : upper(x)] produce?
3. What does the splat operator aws_instance.web[*].id do when aws_instance.web was created with count = 3?
4. Which built-in function returns the first non-null value from a list of arguments?
5. Does Terraform support user-defined functions written directly in HCL?
Was this page helpful?
You May Also Like
HCL Syntax Fundamentals
HashiCorp Configuration Language (HCL) is the declarative syntax underlying every Terraform file. Mastering its blocks, arguments, and expressions is the foundation for writing correct configurations.
Variables and Variable Types
Input variables let Terraform configurations be parameterized and reused across environments. This topic covers declaring variables, type constraints, defaults, validation, and how to supply values.
count and for_each
Compare Terraform's two meta-arguments for creating multiple copies of a resource or module, and understand why for_each is usually the safer choice.
Outputs and Locals
Output values expose data from a module for use by callers or the CLI, while local values let you name and reuse computed expressions within a module. Together they make configurations readable and composable.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics