Conditional Expressions
Real infrastructure is rarely one-size-fits-all. A production environment needs a larger instance than a development sandbox, a public-facing service may or may not attach a load balancer, and the number of replicas often depends on a flag. HCL gives you a single, compact tool for these decisions: the conditional (ternary) expression. It lets you choose between two values based on a boolean condition without resorting to duplicated blocks or external templating.
The ternary expression
The conditional expression has exactly one form:
condition ? true_value : false_value
condition must evaluate to a boolean. If it is true, the whole expression evaluates to true_value; otherwise it evaluates to false_value. Both result values must be convertible to the same type, because Terraform has to know the type of the expression regardless of which branch is taken.
variable "environment" {
type = string
default = "dev"
}
locals {
instance_type = var.environment == "prod" ? "m6i.xlarge" : "t3.micro"
}
Here local.instance_type resolves to "m6i.xlarge" only when environment is "prod", and "t3.micro" for every other value. This works identically in OpenTofu, which shares the HCL2 expression language with Terraform.
Environment-based values
The most common use is selecting configuration per environment. Combine the conditional with input variables so the choice is driven by data rather than hard-coded:
variable "is_production" {
type = bool
default = false
}
resource "aws_instance" "app" {
ami = "ami-0c02fb55956c7d316"
instance_type = var.is_production ? "m6i.large" : "t3.small"
root_block_device {
volume_size = var.is_production ? 100 : 20
volume_type = "gp3"
}
tags = {
Name = var.is_production ? "app-prod" : "app-dev"
}
}
A plan for the non-production case makes the chosen values explicit:
Output:
# aws_instance.app will be created
+ resource "aws_instance" "app" {
+ ami = "ami-0c02fb55956c7d316"
+ instance_type = "t3.small"
+ tags = {
+ "Name" = "app-dev"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Toggling a resource argument
Conditionals shine when an argument should sometimes be unset. Pairing a ternary with null tells Terraform to omit the argument entirely and fall back to the provider default:
variable "enable_logging" {
type = bool
default = true
}
resource "aws_s3_bucket" "data" {
bucket = "devcraftly-data"
}
resource "aws_s3_bucket_logging" "data" {
count = var.enable_logging ? 1 : 0
bucket = aws_s3_bucket.data.id
target_bucket = aws_s3_bucket.data.id
target_prefix = "log/"
}
Using count = condition ? 1 : 0 is the idiomatic pattern for conditionally creating a resource: one instance when the flag is on, zero when it is off. See meta-arguments for the details of count.
Combining with variables and functions
The condition can be any boolean expression, so you can mix comparisons, logical operators, and functions:
variable "replica_count" {
type = number
default = 0
}
variable "region" {
type = string
default = "us-east-1"
}
locals {
has_replicas = var.replica_count > 0
is_us = startswith(var.region, "us-")
failover_mode = local.has_replicas && local.is_us ? "active-active" : "single"
}
Extracting intermediate booleans into named locals keeps the final conditional readable. See functions for built-ins like startswith, coalesce, and length that frequently feed into conditions.
Tip: For “use this value, or a fallback if it is null/empty,” prefer
coalesce(var.name, "default")ortry(...)over a ternary. They express intent more clearly and handle multiple fallbacks.
Nesting and readability
Conditionals can be nested to handle more than two outcomes, but readability degrades quickly:
locals {
size = var.environment == "prod"
? "large"
: var.environment == "staging" ? "medium" : "small"
}
A cleaner approach for multi-way selection is a lookup map, which scales to many cases without nesting:
locals {
size_for_env = {
prod = "large"
staging = "medium"
dev = "small"
}
size = lookup(local.size_for_env, var.environment, "small")
}
The lookup form is easier to extend and review than a chain of ternaries, and the third argument provides a safe default.
Type coercion gotcha
Both branches must agree on a type. Terraform converts to the most general common type, which can surprise you:
| Expression | Result | Notes |
|---|---|---|
true ? "a" : "b" | "a" | Both strings — straightforward |
true ? 1 : 2 | 1 | Both numbers |
true ? "1" : 2 | "1" | Number coerced to string |
true ? [] : ["x"] | [] | Both lists of strings |
true ? null : 5 | null | null allowed; type is number |
Warning: Mixing incompatible types — for example
cond ? "text" : ["list"]— produces an “Inconsistent conditional result types” error at plan time. Make both branches the same shape.
Best practices
- Keep conditions simple; lift complex boolean logic into named
localsso the ternary reads cleanly. - Use
count = cond ? 1 : 0to toggle whole resources, andarg = cond ? value : nullto toggle a single argument. - Prefer
coalesce,try, orlookupover deeply nested ternaries for fallbacks and multi-way choices. - Ensure both branches share a compatible type to avoid inconsistent-result-type errors.
- Drive conditions from typed input variables (
bool,string) rather than magic strings scattered through the config. - Validate the chosen branch with
terraform planbefore applying — the resolved values appear directly in the plan output.