Expressions and Operators
HCL expressions are how you compute values — arithmetic, comparison, logical operators, string templates, and references. They are the connective tissue between resources: an expression reads one resource's attribute, transforms it, and feeds it to another. Knowing what's available — and what isn't, because HCL is expression-based rather than a full language — keeps configs clean.
The mistake worth heading off early is building structured text by hand. The temptation to construct an IAM policy or a JSON document with string concatenation ends in invalid output the first time a value contains a quote or a special character. Expressions are for computing values; structured documents belong to the encoding functions covered next.
Operators and Precedence
HCL has arithmetic operators (+, -, *, /, %), comparison operators (==, !=, <, >), and logical operators (&&, ||, !). They follow conventional precedence — multiplication before addition, comparison before logical-and — but a compound boolean reads more clearly when you parenthesize intent rather than trusting a reader to recall the table.
locals { # parenthesize compound booleans for explicit intent is_prod_ha = (var.environment == "prod") && (var.replica_count > 1) subnet_bits = var.large_network ? 8 : 4 }
String Templates and Directives
Interpolation with ${...} embeds a value inside a string, and template directives %{if} and %{for} add inline conditionals and loops within a string. Directives are handy for a one-line user-data tweak, but building a whole collection inside a string template is harder to read than a for expression that produces the structure directly.
locals { bucket_name = "${var.project}-logs-${var.environment}" greeting = "Hosts: %{ for h in var.hosts }${h} %{ endfor }" }
References and Traversal
A reference walks into a value: aws_instance.web.private_ip reads an attribute, var.subnets[0] indexes a list, and var.config["region"] indexes a map by key. List indexing is positional and errors on an out-of-range index; map indexing is by key and errors on a missing key unless you use lookup with a default. The distinction matters the moment a collection might not contain what you assume.
Type Behavior and null
Operators coerce compatible types — a number used in a string context becomes its string form — and null threads through expressions in ways that surprise people. A null argument means "use the provider default" rather than "set to empty", and a null flowing into an operator often produces a confusing error rather than a sensible fallback. When a value can be null, handle it explicitly with coalesce or a conditional rather than letting it propagate.
Heredocs for Multi-line Strings
A heredoc with <<-EOT holds a multi-line string — a user-data script, a templated policy document — without escaping every newline. The dash form strips leading indentation so the block can sit indented in the config and still produce a clean string. Heredocs are for literal multi-line text; for structured JSON, reach for jsonencode instead of writing the braces by hand.
resource "aws_instance" "web" { ami = "ami-0abc123" instance_type = "t3.micro" user_data = <<-EOT #!/bin/bash echo "env=${var.environment}" > /etc/app.env systemctl restart app EOT }
- Building an IAM policy by hand with string templates instead of
jsonencode, producing invalid JSON the first time a value contains a quote. - Misjudging operator precedence in a compound boolean and getting the wrong result for want of parentheses to pin the intent.
- Overusing
%{for}template directives to assemble a collection aforexpression would build more clearly. - Letting
nullpropagate through an expression and hitting a confusing "null value" error instead of handling it withcoalesce. - Indexing a list with
[0]when it might be empty, or a map with a key that might be absent, and erroring instead of falling back.
- Build JSON with
jsonencodeand YAML withyamlencoderather than concatenating strings. - Parenthesize compound boolean and arithmetic expressions so precedence is explicit to the next reader.
- Use heredocs (
<<-EOT) for multi-line user-data and templated documents instead of escaped one-liners. - Prefer a
forexpression over a%{for}template directive when the output is a collection rather than a string. - Handle a possibly-null value explicitly with
coalesceor a conditional rather than letting it thread through an operator.
Knowledge Check
Why build an IAM policy document with jsonencode instead of a hand-written string template?
jsonencodeescapes special characters, so a quote or backslash in a value can't produce invalid JSON- Hand-written string templates aren't permitted at all inside a resource argument that expects a JSON document
jsonencodeencrypts the rendered policy document so it never appears in the state file- String templates can only embed literals and cannot reference variables at all
How does indexing a list differ from indexing a map when the element might be missing?
- A list index is positional and an out-of-range index errors; a map is keyed and a missing key errors unless you
lookupwith a default - Both a list and a map silently return an empty string when the requested element is absent at plan time, rather than ever raising an error
- A list returns null for any out-of-range index, while a map raises an error for any key you look up
- Maps can only be indexed from inside a
forexpression and never directly with bracket syntax
When is a heredoc (<<-EOT) the right tool?
- For literal multi-line text like a user-data script, where escaping every newline would be noise
- For building a structured JSON policy document by hand, used in place of calling
jsonencodeon a map - For any string that needs to reference a variable through interpolation
- For encrypting a multi-line value before it is written into the state file
A null value flows into a comparison and the plan errors. What is the cleaner approach?
- Handle the null explicitly with
coalesceor a conditional before it reaches the operator - Wrap the whole comparison expression in a heredoc string literal to suppress the plan error
- Cast the null to an empty string by passing it through
jsonencodefirst - Nulls are always coerced to false in a comparison, so the error is a Terraform bug
You got correct