Conditional Expressions
The ternary condition ? true_value : false_value is HCL's only conditional expression, and it does a lot of work: toggling resource creation with count, selecting a value per environment, supplying a default. There is no if statement — every conditional is an expression that produces a value, which keeps configurations declarative.
That single rule trips up people arriving from imperative languages who expect to wrap a block in an if. In HCL you do not branch control flow; you compute a value and let it flow into an argument. A resource becomes optional not by skipping a block but by making its count evaluate to zero.
The Ternary Operator
var.is_prod ? "m5.large" : "t3.micro" picks a value based on a boolean. The condition must be a boolean; both branches are evaluated lazily so only the chosen one matters at runtime. Nesting is legal but should be rare — one level reads cleanly, three levels do not.
resource "aws_instance" "app" { ami = var.ami_id instance_type = var.is_prod ? "m5.large" : "t3.micro" }
Conditional Resource Creation
The most common use of the ternary is making a resource present or absent. count = var.enabled ? 1 : 0 creates one instance or none; for_each = var.enabled ? {...} : {} does the same for a keyed set, creating the members or nothing. This is how you express "build this only in production" or "only when the feature flag is on" without an if block.
resource "aws_flow_log" "vpc" { count = var.enable_flow_logs ? 1 : 0 vpc_id = aws_vpc.main.id traffic_type = "ALL" log_destination = aws_s3_bucket.logs.arn }
Defaults and Fallbacks
For "use this unless it is null or missing," reach for coalesce and try rather than hand-rolling a ternary. coalesce(var.name, "default") returns the first non-null argument; try(local.parsed.id, null) returns its first argument unless evaluating it errors. Both express fallback intent more directly than var.name != null ? var.name : "default".
Type Consistency
Both branches of a ternary must produce compatible types, and mismatches are a frequent error. var.x ? "a" : 5 mixes a string and a number; Terraform will try to converge them and either coerce surprisingly or fail. Keep both arms returning the same type — two strings, two lists, two objects with the same attributes — so the result type is unambiguous.
Readability Limits
Nested ternaries past one level become unreadable fast. When you are selecting one of several values by a key, a lookup map is clearer than a chain of a ? x : b ? y : z. Map the deciding value to its result in a local and look it up — the logic becomes a table anyone can scan instead of a right-leaning ladder.
locals { instance_size = { dev = "t3.micro" staging = "t3.medium" prod = "m5.large" } size = lookup(local.instance_size, var.environment, "t3.micro") }
- Returning different types from the two branches of a ternary, causing a type-mismatch error or a surprising coercion.
- Nesting ternaries three deep until the logic is unreadable, where a
lookupmap keyed by the deciding value would be plain. - Using a ternary where
coalesceortryexpresses "first non-null" or "fall back on error" far more directly. - Branching on a value not known at plan time when the result drives
countorfor_each, which both need plan-time knowns. - Expecting an
ifstatement to skip a resource block, instead of driving itscountto zero.
- Keep ternaries to one level; replace nested conditionals with a
lookupmap keyed by the deciding value. - Use
coalescefor null-fallback andtryfor error-fallback rather than hand-rolled ternaries. - Ensure both branches of a ternary return the same type so the result type is unambiguous.
- Drive conditional resource creation with
countorfor_eachand a boolean variable. - Branch only on plan-time-known values when the result feeds
countorfor_each.
if and conditionals
CloudFormation Conditions blocks
Terraform a single ternary, deliberately minimal
Knowledge Check
How do you make a resource optional in HCL, given there is no if statement?
- Drive its
countto zero:count = var.enabled ? 1 : 0 - Wrap the resource block in an
if { }guard - Set the resource's
enabledmeta-argument to false - Comment the block out behind a preprocessor directive
Why must both branches of a ternary return the same type?
- The expression has one result type, so mixed branches cause a type mismatch or a surprising coercion
- Terraform evaluates both arms eagerly and then concatenates their two values into one combined result string
- It is only a style preference and has no effect on the result at runtime
- The boolean condition is itself recomputed from the two branch result types
You need "use var.name unless it is null, then a default." What is the cleanest expression?
coalesce(var.name, "default")— returns the first non-null argumentvar.name == "" ? "default" : var.name— the empty-string ternary checklookup(var, "name", "default")— keying into var with a fallbackmerge(var.name, "default")— merging the value with the default
When is a lookup map preferable to a nested ternary?
- When selecting one of several values by a key, where a chain of ternaries would be hard to read
- Always, since ternary expressions are deprecated in modern Terraform releases
- Only when the deciding condition becomes known after apply rather than during the initial plan phase
- Never, because
lookupis unable to supply a fallback default value
You got correct