Chapter 5: Iteration and Conditionals
Topic 33

Conditional Expressions

HCL

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.

select an instance size by environment
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.

create the resource only when enabled
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.

a lookup map beats a nested ternary
locals {
  instance_size = {
    dev     = "t3.micro"
    staging = "t3.medium"
    prod    = "m5.large"
  }
  size = lookup(local.instance_size, var.environment, "t3.micro")
}
Common Mistakes
  • 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 lookup map keyed by the deciding value would be plain.
  • Using a ternary where coalesce or try expresses "first non-null" or "fall back on error" far more directly.
  • Branching on a value not known at plan time when the result drives count or for_each, which both need plan-time knowns.
  • Expecting an if statement to skip a resource block, instead of driving its count to zero.
Best Practices
  • Keep ternaries to one level; replace nested conditionals with a lookup map keyed by the deciding value.
  • Use coalesce for null-fallback and try for 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 count or for_each and a boolean variable.
  • Branch only on plan-time-known values when the result feeds count or for_each.
Comparable tools Pulumi host-language 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 count to zero: count = var.enabled ? 1 : 0
  • Wrap the resource block in an if { } guard
  • Set the resource's enabled meta-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 argument
  • var.name == "" ? "default" : var.name — the empty-string ternary check
  • lookup(var, "name", "default") — keying into var with a fallback
  • merge(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 lookup is unable to supply a fallback default value

You got correct