Terraform

Terraform Conditional Expressions: Practical Patterns for Conditional Resources (Associate Prep)

2026-04-19
NicheeLab Editorial Team

A Terraform conditional expression alone cannot suppress resource creation. To toggle whether a resource exists, use count or for_each.

For attribute-level on/off, returning null from a conditional expression is the idiomatic approach. Behavior under plan-time unknown values and the type-compatibility rules are also frequent exam topics.

Conditional Expression Basics and Type Rules

A Terraform conditional expression has the form condition ? true_val : false_val. If the condition is true, true_val is evaluated; if false, false_val is evaluated. The unselected branch is not evaluated, but the two branches must still have compatible types.

From an exam perspective, the two points that come up the most are type compatibility and the fact that an unselected branch is allowed to reference things that do not exist. If the condition is unknown at plan time, the result may also be unknown. Passing an unknown value into the condition for count/for_each is not allowed — it must be decidable at plan time.

  • Both branches must be the same type or implicitly convertible to a common type
  • The unselected branch is not evaluated, so referencing a non-existent resource there is fine
  • When the condition is unknown the result is also unknown — and unknown values cannot drive count/for_each
  • Type mismatches across strings, numbers, and collections often surface as plan-time errors

Example: type compatibility in conditional expressions

variable "env" { type = string }

locals {
  is_prod   = var.env == "prod"
  # OK: number and number
  replicas  = local.is_prod ? 3 : 1
  # OK: list and list (both list(string))
  cidrs     = local.is_prod ? ["10.0.0.0/8"] : ["192.168.0.0/16"]
  # NG example (type mismatch): number and string → plan error
  # bad = local.is_prod ? 1 : "1"
}

Conditional Resource Creation: Canonical count and for_each Patterns

You cannot wrap a Terraform resource block itself in a conditional expression. To toggle whether a resource is created, use the count or for_each meta-arguments. Use count to toggle a single resource on/off, and for_each when you need to create multiple resources with stable keys.

The expression for count/for_each must be decidable at plan time. For example, using a value that depends on a data source and is only known at apply time will cause an error.

  • For a single instance, use count = var.create ? 1 : 0
  • For multiple instances or stable keys, use for_each = var.create ? {"main"=true} : {}
  • Guard [0] references with try(...) or a conditional expression
  • count/for_each values must be known at plan time
MechanismPrimary useCaveats / downsidesTypical code
Conditional onlySwitching values or expressions (attributes/variables)Cannot toggle the existence of a resourcevar.enabled ? "on" : "off"
countToggle a single resource on/offIndex references need to be guardedcount = var.create ? 1 : 0
for_eachMultiple instances with stable keysChanging keys cause destroy/create cyclesfor_each = var.create ? {"main"=true} : {}
Return nullDisabling an attribute (treated as unspecified)Blocks need dynamic to toggletags = var.add_tags ? local.tags : null

Conditional creation flow with count

var.create
    |
    v
condition ? 1 : 0  --->  count = 1  ---->  resource.example[0] is created
                 \
                  \->  count = 0  ---->  0 instances (nothing created)

Toggling resources with count/for_each and guarding references safely

variable "create" { type = bool }

# A single on/off is best handled with count
resource "null_resource" "one" {
  count = var.create ? 1 : 0
  triggers = {
    purpose = "demo"
  }
}

# Guard references with a conditional or try
output "one_id" {
  value = var.create ? null_resource.one[0].id : null
}

output "one_id_try" {
  value = try(null_resource.one[0].id, null)
}

# When stable keys matter, prefer for_each
resource "null_resource" "named" {
  for_each = var.create ? { main = true } : {}
  triggers = {
    name = each.key
  }
}

output "named_keys" {
  value = keys(null_resource.named)
}

Toggle Attributes On/Off by Returning null

For most arguments, passing null is equivalent to leaving the argument unspecified. When you want to conditionally omit something like tags or user_data, returning null from a conditional expression is the safe approach.

To toggle a block (such as versioning) on/off, combine a dynamic block with a conditional / for_each expression.

  • null on an argument means the argument is treated as unspecified
  • For blocks, use dynamic + for_each = condition ? [1] : []
  • Combine with merge / map operations for flexible toggling

Example: conditionally omitting arguments and blocks

# Omit an attribute by returning null
variable "attach_user_data" { type = bool }

resource "aws_instance" "web" {
  ami           = "ami-xxxxxxxx"
  instance_type = "t3.micro"
  user_data     = var.attach_user_data ? file("${path.module}/init.sh") : null
}

# Toggle a block on/off with dynamic
variable "enable_versioning" { type = bool }

resource "aws_s3_bucket" "b" {
  bucket = "example-bucket-12345"

  dynamic "versioning" {
    for_each = var.enable_versioning ? [1] : []
    content {
      enabled = true
    }
  }
}

Conditional Branching with for_each and Key Stability

Because for_each manages resources by key, when you filter the set with a conditional expression you must design the keys to be stable. Changing keys triggers destroy/create.

Subset creation driven by an enabled flag fits naturally into a for-expression with an if clause. To completely disable everything, wrap it in a conditional that returns an empty map {}.

  • Use durable identifiers (e.g., user ID) for map keys
  • Use an if clause to skip individual entries and return {} for a master switch
  • Verify the impact of key-set changes in plan output

Subset creation with an enabled flag plus a master switch

variable "manage_users" { type = bool }
variable "users" {
  type = map(object({ enabled = bool, email = string }))
}

locals {
  # Pick only the enabled ones
  enabled_users = { for k, v in var.users : k => v if v.enabled }
}

resource "example_user" "this" {
  # Master switch: empty map => 0 instances
  for_each = var.manage_users ? local.enabled_users : {}
  name  = each.key
  email = each.value.email
}

Pitfalls Around Unknown Values and Evaluation Order

A conditional expression evaluates only the selected branch. So if a resource is referenced only from the true branch and is not defined when the condition is false, that is not an error. On the other hand, the count/for_each expression must be known at plan time — an unknown value here causes an error.

Guard optional resource references with a conditional or try(). try() evaluates its arguments in order and returns the first one that does not error.

  • A conditional does not evaluate the unselected branch
  • count/for_each must be known at plan time
  • Safe references: var.create ? res[0].id : null or try(res[0].id, null)

Safe references with conditionals and try()

variable "create" { type = bool }

resource "null_resource" "maybe" {
  count = var.create ? 1 : 0
}

# Guard [0] with a conditional
output "maybe_id" {
  value = var.create ? null_resource.maybe[0].id : null
}

# Use try() to attempt in order (safe even when count=0)
output "maybe_id_try" {
  value = try(null_resource.maybe[0].id, null)
}

Summary, Anti-Patterns, and Checklist

Use conditionals to switch values, count/for_each to control resource presence and count, and null to leave attributes unspecified. Keeping these roles separate makes plans readable and safe.

On the exam, lock in three points: you cannot wrap a resource block in a conditional, count/for_each must be known at plan time, and the unselected branch is not evaluated.

  • Do: toggle a single resource on/off with count = var.flag ? 1 : 0
  • Do: use a conditional for_each when you need multiple instances with stable keys
  • Do: omit an attribute by returning null from a conditional
  • Don't: wrap the resource block itself in a conditional (not allowed by the syntax)
  • Don't: pass an unknown-at-plan-time value into count/for_each
  • Don't: use volatile keys for for_each

Minimal pattern for toggling a module

variable "create_subnet" { type = bool }

module "subnet" {
  source = "./modules/subnet"
  count  = var.create_subnet ? 1 : 0
  # Reference module outputs safely
}

output "subnet_id" {
  value = try(module.subnet[0].id, null)
}

Check Your Understanding

Associate

問題 1

Which is the most appropriate way to skip creating an S3 bucket when var.create_bucket is false, while still referencing the bucket ID safely from an output?

  1. Set count = var.create_bucket ? 1 : 0 on the resource and use try(aws_s3_bucket.main[0].id, null) in the output
  2. Branch the whole resource block as var.create_bucket ? resource {} : null
  3. Set create_before_destroy = false in lifecycle
  4. Reference aws_s3_bucket.main[0].id directly in the output and ignore any plan errors

正解: A

Control the presence of a resource with count/for_each. Guard references with a conditional or try(). You cannot wrap a resource block itself in a conditional, and lifecycle does not control whether the resource is created.

Frequently Asked Questions

Does a conditional expression evaluate both branches? What happens if the unselected branch references a resource that does not exist?

Only the selected branch is evaluated. Because the unselected branch is not evaluated, referencing a resource whose count is 0 there will not cause an error. However, the two branches must still have compatible types.

Can I drive count/for_each with a value from a data source that is unknown at plan time?

No. count/for_each requires values that are known at plan time. Passing an unknown value triggers a plan-time error. Use variables or locals whose values are decidable at plan time.

What happens to an attribute when a conditional expression returns null?

For most arguments, null is treated the same as if the argument were not specified at all, and the provider will not send that attribute. To toggle whole blocks on/off, combine dynamic blocks with for_each.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.