Conditional expressions are a key part of any programming language. Conditional expressions return a value based on whether an expression is evaluated as true or false. In most modern languages, conditional expressions are represented by the if…else statement.
Here is an example of a conditional expression: If this article is engaging, then people will continue reading it, else, no one will see it.
We will cover:
- What is a conditional expression in Terraform?
- What is the Terraform ternary operator?
- When to use Terraform conditionals?
- How to use Terraform conditionals?
- Terraform conditionals limitations
- Terraform conditionals best practices
- Terraform’s conditional expressions vs. traditional if-else statements in programming
What is a conditional expression in Terraform?
Terraform doesn’t offer the traditional if…else statement. Instead, it provides a ternary operator for conditional expressions. Conditional expressions in Terraform can be applied to myriad objects, including resources, data sources, outputs, and modules.
Conditional expressions provide flexibility and re-usability to Terraform configurations. They allow configurations to adapt to different environments, requirements, or scenarios.
What is the Terraform ternary operator?
A Terraform ternary operator is one that operates on three operators. Syntactically, the ternary operator defines a boolean condition, a value when the condition is true, and a value when the condition is false.
The ternary operator in Terraform looks like this:
condition ? true_part : false_partconditionoperand is any expression whose value resolves to a boolean, like article == engaging.true_partis the value returned when the condition is evaluated as true.false_partis the value when the condition is evaluated as false.
Here is a basic example:
account_tier = var.environment == "dev" ? "Standard" : "Premium"The previous ternary expression can be broken down like so:
| Condition | ? | true part | : | false part |
If the environment variable is equal to “dev” |
then | assign the value “Standard” to the account_tier attribute | else | assign “Premium” |
The two result values, true_part and false_part, must both be the same data type, i.e., two strings. If the data types are different, Terraform will attempt to convert them to a common type automatically.
For example, Terraform will automatically convert the result of the following expression to a string since numbers can be converted to a string:
count = var.allow_public == true ? 1 : "0"While automatic data type conversion is a nice convenience, it should not be relied upon as it leads to configurations that are confusing and can be error-prone. Instead, explicitly convert data types to avoid automatic data type conversion:
count = var.allow_public == true ? 1 : tonumber("0")In real-world Terraform configurations, type mismatches often show up when you return different kinds of collections from a conditional. For example, the following looks reasonable at a glance, but it will fail because one branch returns a map and the other returns a list:
# ❌ This will fail with an "Inconsistent conditional result types" error
resource "azurerm_storage_account" "my_storage_account" {
# ...
tags = var.environment == "prod" ? {
environment = "prod"
tier = "premium"
} : ["no-tags"]
}Terraform expects both results to have the same type, so you need to make sure both sides return a map of strings. A safer version returns an empty map when no tags should be applied:
# ✅ Both branches now return a map(string)
resource "azurerm_storage_account" "my_storage_account" {
# ...
tags = var.environment == "prod" ? {
environment = "prod"
tier = "premium"
} : {}
}Keeping the result types consistent makes the configuration easier to reason about and avoids confusing type errors later in the plan.
When to use Terraform conditionals?
Terraform conditionals allow dynamic resource configuration based on input variables or environment-specific logic. They enable you to write more flexible, reusable, and concise infrastructure code.
Conditionals are typically used to toggle resource creation, set variable values, or configure resource arguments without duplicating code.
1. Testing for the existence of a variable’s value
A common use case for conditional expressions is to test for the existence of a variable’s value and define a default value to replace invalid values:
var.environment == "" ? "dev" : var.environmentIf the value of var.environment is an empty string then set its value to “dev”, otherwise use the actual value of var.environment.
2. Configuring settings differently based on certain conditions
Conditional expressions are often used to configure settings differently based on certain conditions. In this example, a conditional expression is used to configure an Azure storage account’s access_tier attribute.
If the var.environment value is “dev”, the access tier will be set to “Cool”. Otherwise, it will be “Hot”.
resource "azurerm_storage_account" "my_storage" {
name = "stmystorage"
resource_group_name = "rg-conditional-demo"
location = "eastus"
access_tier = var.environment == "dev" ? "Cool" : "Hot"
}How to use Terraform conditionals?
In Terraform, you can write a condition in absolutely any parameter of a resource, data source, output, or local.
Let’s take a look at some examples.
Example 1: Create a resource using a conditional expression
By default, Terraform creates one instance of a resource.
Terraform’s count meta-argument instructs Terraform to create several similar objects without writing a separate block for each one. If a resource or module block includes a count argument with a whole number value, Terraform creates that many instances of the resource. Setting the count to zero results in no instances of the resource being created.
When combined with a conditional expression, count can be used to create powerful logic to control whether to create a resource.
The following example evaluates the value of the add_storage_account Boolean variable.
If it is true, count will be assigned 1. When this happens, an Azure storage account will be created. However, if add_storage_account is false, the count will be zero, and no storage account will be created.
variable "add_storage_account" {
description = "boolean to determine whether to create a storage account or not"
type = bool
}
resource "azurerm_storage_account" "my_storage_account" {
count = var.add_storage_account ? 1 : 0
resource_group_name = "rg-conditional-demo"
location = "eastus"
account_tier = "Standard"
account_replication_type = "LRS"
name = "stspacelift${count.index}${local.rand_suffix}"
}Similar to count, Terraform meta-argument is used to create many instances of the same resource. for_eachfor_each works with a list of values to create resources with distinct arguments.
The difference between the two meta-arguments is that count is best used when nearly identical resources need to be created. for_each is best for creating resources where some of the resources need distinct attribute values. (Learn more about Terraform count vs. for_each.)
A typical use case for the for_each argument is to use a map of objects to assign multiple users to a group. A conditional expression can be added to filter out resources that should be added to a group based on their user type.
This example shows one way to do that.
variable "users" {
description = "A list of users to add"
type = map(object({
email = string,
user_type = string
}))
default = {
"member1" = {
email = "member1@abc.com",
user_type = "Member"
},
"member2" = {
email = "member2@abc.com",
user_type = "Member"
},
"guest1" = {
email = "guest@abc.com",
user_type = "Guest"
}
}
}
# Get the users from AAD
data "azuread_user" "my_users" {
for_each = var.users
user_principal_name = each.value.email
}
resource "azuread_group" "my_group" {
display_name = "mygroup"
security_enabled = true
}
# Only add users who are members to the group
resource "azuread_group_member" "my_group_members" {
for_each = { for key, val in data.azuread_user.my_users :
key => val if val.user_type == "Member" }
Group_object_id = azuread_group.my_group.id
Member_object_id = data.azuread_user.my_users[each.key].id
}The users variable defines an object map, with each object having property named “email”. Three user objects are added to the map, two members and one guest.
A data source is used to retrieve users from AAD. The for_each argument in the azuread_group_member resource loops through the users returned from AAD and uses a condition to apply a filter for users who are members.
Each user in the filtered results will be added to the group named “my_group”.
Example 2: Using conditionals to deploy a Terraform module
In addition to their application to resources, conditional expressions can be combined with count and for-each on the following Terraform objects: module blocks, data sources, dynamic blocks, and local and/or output variables.
The syntax for module blocks is identical to that shown for a resource block.
| Object | Use Case |
| module block | control the creation and number of instances |
Here’s an example that uses conditional expressions with count and for_each on a module block.
# module examples
# determine if an account should be created
module "storage" {
count = var.add_storage_account ? 1 : 0
source = "./path to module tf file"
...
}
# filter list of users to add to a group
module "group_members" {
for_each = { for key, val in data.azuread_user.my_users :
key => val if val.user_type == "Member" }
source = "./path to module tf file"
...
}Example 3: Using conditionals in data sources
Again, the syntax for data sources is identical as shown for a resource block.
| Object | Use Case |
| data source | reduce the number of records via filter |
Here’s an example that uses conditional expressions with count and for_each on a data source.
# data source example
# filter a data source using the `users` variable from above, looking for "members"
data "azuread_user" "my_users" {
for_each = { for key, val in var.users :
key => val if val.user_type == "Member" }
user_principal_name = each.value.email
}Example 4: Using conditionals with local values
The syntax for local variables is identical to that shown for a resource block.
| Object | Use Case |
| local variable | set variable values based on conditions |
Here’s an example that uses conditional expressions with count and for_each on a local variable.
# local variable example
# uses a conditional expression to assign a value to the "rand_suffix" variable if the `add_storage_account` variable is true
locals {
# "rand_suffix" can be appended to the storage account name.
rand_suffix = var.add_storage_account ? ${random_string.random.result} : null
}Example 5: Using conditionals with output variables
The syntax for the output block is identical as shown for a resource block.
| Object | Use Case |
| output variable | return values based on conditions |
Here’s an example that uses conditional expressions with count and for_each an output variable.
# output variable example
# return a storage account name, if an account was created. Empty string otherwise
output "storage_account_name" {
value = var.add_storage_account ? azurerm_storage_account.my_storage_account[0].name : ""
}Example 6: Using conditionals in dynamic blocks
The syntax for dynamic blocks is also the same as shown for a resource block.
| Object | Use Case |
| dynamic block | control the creation and number of instances |
Dynamic blocks are especially useful when you need to create nested blocks only when some input is present. For example, you might want to add IP rules to a storage account only when a list of allowed IPs is provided:
variable "allowed_ips" {
description = "Optional list of IP addresses that should be allowed to access the storage account."
type = list(string)
default = []
}
resource "azurerm_storage_account" "my_storage_account" {
name = "stspacelift${count.index}${local.rand_suffix}"
resource_group_name = "rg-conditional-demo"
location = "eastus"
account_tier = "Standard"
account_replication_type = "LRS"
network_rules {
default_action = "Deny"
# Only create ip_rules blocks when allowed_ips is not empty
dynamic "ip_rules" {
for_each = length(var.allowed_ips) > 0 ? var.allowed_ips : []
content {
ip_address = ip_rules.value
}
}
}
}In this example, the dynamic "ip_rules" block uses a conditional expression in for_each. When var.allowed_ips is empty, for_each evaluates to an empty list and no ip_rules blocks are created. When one or more IPs are provided, Terraform creates one nested ip_rules block per IP.
Example 7: Writing multiple conditions
Complex logic can be created when conditional expressions are combined with Terraform’s logical operators. Terraform provides the logical operators && (AND), || (OR), and ! (NOT).
This example combines two conditions using the and operator.
In this case, if add_storage_account is true and environment equals “prod”, two instances of the resource are created. Otherwise, none are created.
count = var.add_storage_account && var.environment == "prod" ? 2 : 0Conditional logic can also be nested. For instance, the true_part or false_part of the ternary operator could be another conditional expression.
Converting the previous example, but replacing the logical and with nested logic would look like this:
count = var.add_storage_account ? var.environment == "prod" ? 2 : 1 : 0Here, the true_part is another condition, eg., does environment equal “prod”. While the result is similar to the code using a logical and, the nested version is a bit harder to read and not as clean.
Terraform conditionals limitations
There are a few limitations to be aware of when using conditional expressions.
- Terraform conditionals only work with values of the same or compatible types. You can’t return different resource blocks, entire modules, or incompatible types from conditionals.
- What to do instead: Keep conditionals focused on values (strings, numbers, maps, lists, etc.), and model larger structural choices with separate resources or modules. For example, if you need two very different resource definitions, create two resource blocks and use
countorfor_eachon each instead of trying to choose between whole blocks in a single expression.
- What to do instead: Keep conditionals focused on values (strings, numbers, maps, lists, etc.), and model larger structural choices with separate resources or modules. For example, if you need two very different resource definitions, create two resource blocks and use
- While conditional expressions in Terraform can be applied to many object types, they cannot be applied to providers.
- What to do instead: Use separate configurations, workspaces, or stacks for different providers, or use provider aliases and wire them into different modules. The selection of which provider alias to use should happen at the module or stack level (for example, via variables or separate Spacelift stacks), not inside a conditional expression on the provider itself.
- count and
for_eachare mutually exclusive and cannot be used on the same object.- What to do instead: Choose
countwhen you are creating a fixed number of nearly identical resources, andfor_eachwhen you are working with a collection keyed by names or IDs and need to track instances individually. If you need to migrate fromcounttofor_each, do so carefully (often in two steps) to avoid resource address changes.
- What to do instead: Choose
- While this won’t affect many Terraform implementations, it’s important to note that module support was added for
countand for_each in version 0.13. Both meta-arguments can only be applied to resource blocks in versions prior to 0.13.- What to do instead: if you are still on a pre-0.13 version, keep
count/for_eachon resources only, and factor module usage so that each module encapsulates a single pattern. Where possible, plan an upgrade path to a current Terraform or OpenTofu version to take advantage of conditional modules.
- What to do instead: if you are still on a pre-0.13 version, keep
- Starting with Terraform v1.5, declarative imports are supported using the
importblock. However, conditional imports are not supported within HCL, as import blocks do not allowcount,for_each, or conditional expressions. To conditionally import resources, you must either runterraform importmanually for each resource as needed or use external scripting (e.g., shell scripts or wrapper tools) to automate the import process conditionally based on your logic.- What to do instead: Keep imports as one-off or scripted operational tasks. For repeatable flows, wrap
terraform importin scripts or CI/CD pipelines (or a Spacelift stack) that apply your conditions outside HCL, then run the appropriate import commands based on that logic.
- What to do instead: Keep imports as one-off or scripted operational tasks. For repeatable flows, wrap
Terraform conditionals best practices
As with all software development, conditional expressions have a few best practices to follow.
- Avoid overly complex conditions. While nested conditions are possible, they add complexity to the configuration, making it difficult to maintain and comprehend. Prefer breaking complex logic into multiple
localsor intermediate variables and using simple, single-purpose conditionals. - Use descriptive variable and local names. Clear names such as
add_storage_account,allowed_ips, oris_productionmake it easier to understand what a condition is doing at a glance, especially when revisiting code months later. - Test your conditional expressions with the Terraform CLI. Use
terraform consoleto experiment with expressions in isolation, and runterraform validateandterraform planwith different input variable values (for example, different.tfvarsfiles fordev,test, andprod) to make sure each branch behaves as expected. - Add automated tests where it makes sense. For reusable modules, you can use Terraform’s built-in testing framework (
terraform test) or external tools such as Terratest to validate that your conditional logic does the right thing across different scenarios. This helps catch regressions when conditionals or types change. - Use policy-as-code and CI/CD tooling to enforce safe patterns. Tools like Open Policy Agent and Spacelift policies can prevent risky combinations (for example, disallowing certain values when a flag is enabled) and ensure that all changes go through the same conditional logic and review gates before being applied. This is especially powerful when combined with environment-specific variables managed by Spacelift stacks and contexts.
Conditional expressions allow flexible configurations that adapt to different environments, requirements, and/or scenarios. Terraform’s ternary operator is the main way to apply conditional logic. Ternary operators used on variables are helpful for setting default and invalid values.
Conditional expressions combined with count and for_each offer the ability to control whether a resource is created, and how many instances of a resource to create. They also allow for filtering data and configuring specific resource attributes.
Terraform's conditional expressions vs. traditional if-else statements in programming
Terraform’s conditional expressions are concise, ternary-like expressions, unlike traditional if-else blocks, which support multiple execution paths and statements.
In Terraform, a conditional expression follows the syntax condition ? true_result : false_result, always returning a value. This makes it suitable for setting arguments or assigning variables, but not for executing logic or multiple steps. In contrast, traditional if-else statements (like in Python or JavaScript) allow for full control flow, including executing multiple operations, nesting, and side effects.
For example:
instance_type = var.env == "prod" ? "m5.large" : "t2.micro"This is declarative and evaluates to a single value, not an executable block.
Terraform’s model fits its declarative nature, where the goal is to describe infrastructure rather than control logic execution.
Managing Terraform resources with Spacelift
Terraform is really powerful, but to achieve an end-to-end secure Gitops approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs
- Build self-service infrastructure – You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. Read the documentation for more information on configuring private workers.
You can check it out for free by creating a trial account or booking a demo with one of our engineers.
Key points
Conditional expressions are easy to learn and implement and are another essential tool in any IaC toolbox.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Manage Terraform better with Spacelift
Build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.
