Chapter 6: Iteration and Conditionals
Topic 39

Conditional Expressions

Expressions

The conditional expression — the ternary — condition ? true_value : false_value is how HCL chooses between two values inline: a smaller bucket in dev, a larger one in prod, a name with or without a suffix. Its most consequential use is the "create this resource or not" pattern, expressed either as count = var.enabled ? 1 : 0 or as for_each over an empty set, and the two forms leave a different footprint in state.

For the Hatch pipeline the optional resource shows up constantly: a dead-letter topic that only some environments want, a monitoring export that staging skips, a per-tenant dataset created only when the tenant opts in. Each is a conditional creation, and which construct you reach for decides whether toggling it later renumbers your state or leaves it stable. That choice is the heart of this topic.

The Ternary

var.is_prod ? "US" : "us-central1" picks one of two values. Both branches must be the same type — a string and a string, a list and a list — because HCL evaluates the expression to a single typed result and will not unify a string with a list. The expression is evaluated, not short-circuited around type errors: both arms are type-checked, so a type mismatch is a plan-time failure, not a runtime surprise on the branch you happened to take.

Conditional Resource Creation via count

count = var.create_dlq ? 1 : 0 creates the dead-letter topic in environments that want it and zero instances elsewhere. The resource becomes a list of length 0 or 1, addressed google_pubsub_topic.dlq[0] when it exists and absent entirely when it does not. This is the classic conditional-creation idiom, and it works — with one sharp edge covered two sections down.

Conditional creation, the count form
resource "google_pubsub_topic" "dlq" {
  count = var.create_dlq ? 1 : 0
  name  = "hatch-events-dlq"
}

Conditional Creation via for_each

for_each = var.create_dlq ? toset(["dlq"]) : toset([]) does the same thing — one instance when the condition holds, none when it does not — but keys the instance by "dlq" instead of [0]. The resource is addressed google_pubsub_topic.dlq["dlq"], which reads better in state and, crucially, interacts cleanly with other for_each resources. Toggling it on and off adds or removes the "dlq" key without disturbing anything else.

Conditional creation, the for_each form
resource "google_pubsub_topic" "dlq" {
  for_each = var.create_dlq ? toset(["dlq"]) : toset([])
  name     = "hatch-events-dlq"
}
Two ways to make a resource optional
conditional count
count = cond ? 1 : 0 — addressed [0] when present. Existence is tied to a position, so every reference must index [0] and a flip can break it.
conditional for_each
for_each = cond ? toset(["dlq"]) : toset([]) — addressed by key. Toggling adds or removes the key, never renumbers. Prefer when referenced elsewhere.

The count-flip Index Gotcha

Toggling count between 0 and 1 changes the resource from [0] existing to not existing. If other resources reference ...[0] — a subscription pointing at google_pubsub_topic.dlq[0].id, say — flipping the condition off makes that reference point at a non-existent index and the plan fails. Worse is combining a conditional count with a list count in one expression: now the index encodes both "does it exist?" and "which one is it?", and any change churns the lot. Keep "whether" and "which" out of the same count.

Defaulting and Coalescing

Not every conditional is a create-or-not. For "use this value if set, otherwise fall back," coalesce() returns its first non-null argument and try() returns the first argument that does not error — both express null-handling in one line where a nested ternary would sprawl. coalesce(var.bucket_location, "US") reads more cleanly than var.bucket_location != null ? var.bucket_location : "US", and the intent — default this optional variable — is legible at a glance.

conditional count vs conditional for_each

count = cond ? 1 : 0 — addresses the optional resource as [0], which forces every reference to index [0] and entangles existence with position. Toggling it off breaks any reference that assumed the index existed. Acceptable only when nothing else references the resource.

for_each = cond ? toset(["x"]) : toset([]) — addresses it by a named key, so toggling it never renumbers anything and references stay stable across the flip. Prefer this form whenever the resource is referenced elsewhere, which is most of the time.

Common Mistakes
  • Returning different types from the two ternary branches (a string versus a list) and getting an "inconsistent conditional result types" error — both arms must match.
  • Referencing a conditionally-created resource as ...[0] without guarding for the count-zero case, so when the condition is false the reference points at a non-existent index and the plan fails.
  • Toggling a conditional count from 1 back to 0 to "pause" a resource and discovering it is destroyed, not suspended — index 0 ceasing to exist is a delete.
  • Combining conditional creation and iteration in one count expression (var.enabled ? length(var.x) : 0), so the index now encodes both "whether" and "which," and any change churns the lot.
  • Using a deeply nested ternary for null-defaulting where coalesce() or try() would read in one line.
  • Assuming the ternary short-circuits like a guard, then hitting a type error on the branch that was never meant to run — both arms are type-checked regardless.
Best Practices
  • Use the for_each-on-empty-set form for optional resources that other resources reference, so toggling never renumbers state.
  • Keep both ternary branches the same type, and reach for coalesce() or try() instead of nested ternaries for fallbacks.
  • Guard references to conditionally-created resources with one(google_pubsub_topic.dlq[*].id) or a length check instead of hardcoding [0].
  • Keep "whether to create" and "how many to create" in separate expressions, never fused into one count.
  • Default optional variables with coalesce() at the point of use so a missing input has one obvious fallback.
Comparable approaches Pulumi native if/else in a real language, no resource-count trick needed Deployment Manager Jinja conditionals Ansible the when: clause HCL coalesce and try as the null-handling analog

Knowledge Check

Why must both branches of a ternary return the same type?

  • HCL evaluates the expression to a single typed result and type-checks both arms, so a mismatch fails the plan
  • The branch not taken is discarded at runtime, so only the type of the branch the condition actually selects ever matters
  • Terraform silently converts both branches to strings, so any mix of types is allowed
  • The condition itself must be one of the two branch types in order for the ternary to evaluate at all

How do conditional count and conditional for_each differ in state for an optional resource?

  • count addresses it as [0], entangling existence with position; for_each keys it by name, so toggling never renumbers
  • They are identical in state; only the surface syntax of the meta-argument differs
  • for_each stores the resource under two separate keys at once so a destroy of one still leaves the other in place for redundancy
  • count retains the resource in state even at zero, while for_each removes it entirely

You toggle a conditional count from 1 back to 0 to pause a topic. What actually happens?

  • The topic is destroyed — index 0 ceasing to exist is a delete, not a suspension
  • The topic is preserved in state at index 0 but quietly stops receiving and delivering messages
  • Nothing changes in state or in the plan until some later apply sets count back to 1
  • The topic is archived and restored automatically when count returns to 1

When is coalesce() a better choice than a nested ternary?

  • For null-defaulting an optional value — it returns the first non-null argument in one readable line
  • When you need to create or skip a whole resource conditionally based on the value of a boolean flag
  • When the candidate values are different types and must be unified into one
  • Whenever the condition itself depends on a resource's computed attribute

You got correct