How to Fix Terraform 'The condition expression must be boolean' Error
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: A
conditionblock inside avalidation,precondition, orpostconditionblock is returning a non-boolean value — Terraform requires a stricttrue/falseresult, nothing else. - How to fix it: Wrap the offending expression with a boolean-coercing function like
can(),contains(),length(...) > 0, or a direct comparison operator so the expression evaluates totrueorfalse. - Fast path: Drop your failing
.tffile into the Client-Side Sandbox above to auto-refactor the condition expression without sending your config anywhere.
The Incident (What Does the Error Mean?)
Raw error output:
Error: The condition expression must be boolean
on main.tf line 12, in variable "instance_type":
12: condition = var.instance_type
The condition expression must return a boolean (true or false) value, not a string.
Terraform's validation and lifecycle condition system is strict: the condition argument accepts exactly one boolean expression. If your expression resolves to a string, number, list, object, tuple, or null, Terraform aborts at plan time — before a single resource is created, modified, or destroyed. Every terraform plan and terraform apply in your pipeline is dead until this is resolved.
The Attack Vector / Blast Radius
This error surfaces in three distinct Terraform constructs, each with its own blast radius:
1. variable validation blocks — Broken immediately on terraform plan. No infrastructure runs. CI/CD pipeline fails at the gate.
2. precondition blocks (Terraform 1.2+) — Resource provisioning is blocked. In a module chain, this can cascade: a failed precondition on a data source or resource prevents all dependent resources in the graph from being evaluated.
3. postcondition blocks — The resource has already been provisioned by the time this fires. A non-boolean postcondition crashes the apply mid-run, leaving infrastructure in a partial deployment state — the worst possible outcome in production.
The secondary risk: engineers under pressure will comment out the condition entirely to unblock the pipeline. That removes your guardrail permanently and the underlying misconfiguration ships to prod silently.
How to Fix It (The Solution)
Basic Fix — Direct Boolean Comparison
The most common cause: passing a variable or expression directly instead of evaluating it.
variable "instance_type" {
type = string
validation {
- condition = var.instance_type
+ condition = contains(["t3.micro", "t3.small", "m5.large"], var.instance_type)
error_message = "instance_type must be one of: t3.micro, t3.small, m5.large."
}
}
Common Non-Boolean Patterns and Their Fixes
# Pattern 1: length() returns a number, not a boolean
- condition = length(var.allowed_cidrs)
+ condition = length(var.allowed_cidrs) > 0
# Pattern 2: regex() returns a string match, not a boolean
- condition = regex("^arn:aws:", var.role_arn)
+ condition = can(regex("^arn:aws:", var.role_arn))
# Pattern 3: try() returns the value, not a boolean
- condition = try(var.tags["Environment"], null)
+ condition = try(var.tags["Environment"], null) != null
# Pattern 4: lookup() returns a string
- condition = lookup(var.config, "enabled", "false")
+ condition = lookup(var.config, "enabled", "false") == "true"
Enterprise Best Practice — Precondition with can() and alltrue()
In complex modules, validate multiple constraints atomically. Use alltrue() to compose boolean checks and can() to safely probe expressions that might throw errors:
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
precondition {
- condition = var.ami_id && var.instance_type
+ condition = alltrue([
+ can(regex("^ami-[0-9a-f]{8,17}$", var.ami_id)),
+ contains(["t3.micro", "t3.small", "m5.large"], var.instance_type)
+ ])
error_message = "ami_id must be a valid AMI ID and instance_type must be an approved size."
}
}
}
Key functions for boolean-safe conditions:
| Function | Use Case |
|---|---|
can(expr) |
Returns true if expr evaluates without error, false otherwise |
contains(list, val) |
Membership check — always boolean |
alltrue(list) |
AND across multiple boolean expressions |
anytrue(list) |
OR across multiple boolean expressions |
startswith(str, prefix) |
String prefix check — always boolean (Terraform 1.3+) |
endswith(str, suffix) |
String suffix check — always boolean (Terraform 1.3+) |
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This error should never reach a human engineer's terminal. Catch it in the pipeline.
1. terraform validate as a pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_validate
terraform validate catches boolean type mismatches before any plan is run. Zero cost. Zero excuse not to have it.
2. TFLint with the AWS ruleset
tflint --init
tflint --call-module-type=all
TFLint performs static type analysis and will flag expressions in condition blocks that cannot resolve to boolean.
3. Checkov policy scan
checkov -d . --framework terraform
Checkov's Terraform runner validates HCL structure including validation block integrity as part of its static analysis pass.
4. OPA/Conftest for custom boolean expression enforcement
# policy/condition_must_be_bool.rego
package terraform.validation
deny[msg] {
block := input.resource.aws_instance[_].lifecycle[_].precondition[_]
not is_boolean(block.condition)
msg := sprintf("precondition condition must be a boolean expression, got: %v", [block.condition])
}
Enforce this in your Atlantis or Spacelift pipeline as a mandatory policy gate. Merge is blocked if the condition expression type cannot be statically verified as boolean.