Going to AWS re:Invent 2024?

➡️ Book a meeting with Spacelift

Terraform

How to Use Terraform Conditional Expressions

How to Use Terraform Conditional Expressions

Terraform at Scale

Manage your infrastructure as it grows with a flexible, robust workflow, drift detection and reconciliation, and policies for security and compliance.

Book a demo

Conditional expressions are a key part of any programming language. Conditional expressions return a value based on whether an expression evaluates to 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:

  1. What is a conditional expression in Terraform?
  2. What is Terraform ternary operator?
  3. When to use Terraform conditionals?
  4. How to use Terraform conditionals?
  5. Terraform conditionals limitations
  6. Terraform conditionals best practices

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 which 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_part

The condition operand is any expression whose value resolves to a Boolean, like article == engaging. true_part is the value returned when the condition evaluates to true. false_part is the value when the condition evaluates to 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 automatically convert them to a common type.

For example, Terraform will automatically convert the result of the following expression to string since numbers can be converted to 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")

The example illustrates the point, but admittedly is a bit contrived.

When to use Terraform conditionals?

 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.environment

If 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 for_each meta-argument is used to create many instances of the same resource. for_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 as 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 as 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  

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 : 0

Conditional 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 : 0

Here, 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.

  • While they can be applied to many object types, they cannot be applied to providers.
  • count and for_each are mutually exclusive and cannot be used on the same object.
  • While this won’t affect many Terraform implementations, it’s important to note that module support was added for count and for_each in version 0.13. Both meta-arguments can be only applied to resource blocks in versions prior to 0.13.

Terraform conditionals best practices

As with all software development, conditional expressions have a few best practices to follow.

  • Remember to avoid complex conditions. Nested conditions, while possible, add complexity to the configuration, making it difficult to maintain and comprehend.
  • Descriptive variable names facilitate the readability of the configuration.
  • Be sure to test each conditional expression to ensure it works as intended.

Flexible configurations that adapt to different environments, requirements, and/or scenarios are possible with conditional expressions. 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.

Read also how to use the if / else statement in Terraform.

Key points

Conditional expressions are easy to learn and implement and are another essential tool in any IaC toolbox.

We encourage you also to explore how Spacelift makes it easy to work with Terraform. If you need any help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this.

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.

Start free trial

The Practitioner’s Guide to Scaling Infrastructure as Code

Transform your IaC management to scale

securely, efficiently, and productively

into the future.

ebook global banner
Share your data and download the guide