skip to content

In Terraform, what does the idiom count = var.enabled ? 1 : 0 on a resource block do, and how do you then reference that resource's attributes elsewhere in the configuration?

level: juniorimportance: should knowfreq 62%

answer

  1. zero is a legal count
  2. expansion happens either way
  3. the reference needs an index
  4. splat then collapse the list
  5. null, not an empty string

basics

~20 s

It makes the resource conditional: one instance when the flag is true, zero when false. Because the block is still expanded by count, every reference needs an index — aws_s3_bucket.logs[0].id — or a splat wrapped in one() to yield null when the resource is absent.

solid answer

~40 s

`count` accepts any whole number, and zero is legal, so `count = var.enabled ? 1 : 0` is Terraform's standard way to say "create this resource only sometimes". The cost is addressing: the resource is now a counted resource whether or not the flag is set, so it is no longer `aws_s3_bucket.logs.id` but `aws_s3_bucket.logs[0].id`, and that reference errors when the count is zero. The safe form is a splat plus `one()` — `one(aws_s3_bucket.logs[*].id)` — which returns the single id when the resource exists and `null` when it does not, so downstream expressions can use `coalesce` or a conditional instead of blowing up. The other trap is that the condition must be known at plan time; if it depends on an attribute that only exists after apply, the plan fails rather than guessing.

code

hcl · 14 lines
hcl
variable "create_bucket" {
  type    = bool
  default = false
}

resource "aws_s3_bucket" "logs" {
  count  = var.create_bucket ? 1 : 0
  bucket = "app-logs-example"
}

output "bucket_id" {
  # null when the bucket was not created
  value = one(aws_s3_bucket.logs[*].id)
}

go deeper

for a junior

Recognise the ternary idiom and be able to write it. Know that once count is set, references need an index like [0] rather than the bare resource name.

for a middle

Explain why expansion forces indexed addressing even at count 0, and show the splat-plus-one() pattern that returns null instead of erroring when the resource is absent.

for a senior

Treat the toggle as a destroy in review: point out that flipping the flag off deletes a real object, and prefer an empty-collection for_each when the optional thing is a group rather than a single resource.

for a principal

Decide where optionality lives at all. Argue whether a module should carry feature flags or whether the estate is better served by separate composition, since each flag multiplies the untested configurations the team must reason about.

## Why a meta-argument for multiplicity is used as an if-statement HCL has no `if` at the block level. You cannot write "declare this resource only when the flag is on". What you can do is declare it always and give it a multiplicity of zero, because `count` accepts any whole number including `0`, and a resource with zero instances creates nothing: ```hcl variable "create_bucket" { type = bool default = false } resource "aws_s3_bucket" "logs" { count = var.create_bucket ? 1 : 0 bucket = "app-logs-example" } ``` The expression is a normal conditional: `condition ? true_value : false_value`. With `create_bucket = false` the plan contains no bucket at all; flip it to `true` and the plan creates exactly one. This is the accepted idiom, and it is why `count` remains in everyday use even in codebases that otherwise prefer `for_each`. ## The price: the resource is now indexed forever Expansion happens whether the number is 0, 1 or 50. The moment a block carries `count`, its instances live at indexed addresses, so a reference to the whole resource is no longer valid: ```hcl # Error: because aws_s3_bucket.logs has count set, # its attributes must be accessed on specific instances output "id" { value = aws_s3_bucket.logs.id } # Valid, but crashes when count is 0 output "id" { value = aws_s3_bucket.logs[0].id } ``` `[0]` is fine inside a branch you know is taken, but any module that might be configured with the flag off will fail on index-out-of-range. This is the "ugly `[0]`" that spreads through a codebase once one optional resource appears. ## The splat plus one() pattern The splat operator on a counted resource yields a list of that attribute across all instances — length 1 when enabled, length 0 when not. `one()` collapses a zero-or-one-element list to either the element or `null`: ```hcl output "bucket_id" { value = one(aws_s3_bucket.logs[*].id) } ``` Now the output is the id when the bucket exists and `null` when it does not, and consumers can handle absence explicitly: ```hcl bucket = coalesce(one(aws_s3_bucket.logs[*].id), var.existing_bucket_id) ``` `one()` is available from Terraform 0.15 onward; before that people wrote `join("", aws_s3_bucket.logs[*].id)` to squash the list into an empty string, which is why that odd-looking `join` still shows up in older modules. Prefer `one()`: `null` is a real absence, an empty string is a value that will happily be passed somewhere it does not belong. ## The condition must be known at plan time Terraform must know how many instances exist before it can produce a diff, so the count expression cannot depend on something only computable during apply. Deriving it from a variable, a local, or a data source read at plan is fine; deriving it from an attribute of a resource being created in the same run is not, and the plan fails rather than guessing. Push conditionals up to inputs whenever you can. ## Toggling the flag is create/destroy, not pause Setting the flag to `false` on a live resource does not disable it — it removes the instance from the configuration, so the plan destroys the real object. That is the intended behaviour, but it is worth saying out loud in review: an innocuous-looking `enabled = false` in a tfvars file is a delete. For anything holding data, pair the pattern with an explicit review step, and remember that the resource comes back empty when the flag is flipped again. ## When for_each is the cleaner conditional For an optional *set* of resources rather than an optional single one, conditioning the collection reads better and keeps key addressing: ```hcl resource "aws_subnet" "extra" { for_each = var.enabled ? var.extra_subnets : {} # ... } ``` An empty map produces zero instances, exactly like `count = 0`, but the surviving instances stay key-addressed and no `[0]` is required anywhere. Use `count` for the single optional resource, and an empty-collection `for_each` when the thing being toggled is a group.

  • What breaks if you write aws_s3_bucket.logs[0].id in a module whose flag can be false?
    The expression is evaluated even when zero instances exist, so Terraform fails with an index-out-of-range error during plan. Use `one(aws_s3_bucket.logs[*].id)` instead: the splat produces an empty list and `one()` returns null, letting callers decide what absence means rather than aborting the run.
  • Can the count expression depend on an attribute of another resource in the same configuration?
    Only if that attribute is already known at plan time. Terraform must expand the resource before diffing, so a count derived from a value that is unknown until apply fails the plan outright. Move the condition to a variable, a local, or a data source that resolves during plan.

saying these in an interview costs you the question

  • Thinks count = 0 disables the resource without destroying it
  • Says the resource can still be referenced without an index
  • Uses [0] unconditionally in a module the flag can turn off
  • Believes join("",...) and one() are equally safe
  • Assumes the flag may be computed during apply

context