Terraform Conditionals

Brendan Thompson • 10 December 2021 • 6 min read

What is a conditional?#

Terraform, like many programming languages or Domain-Specific Languages (DSL), has the concept of Conditional Expressions, this uses the value of a boolean expression to select two values.

The syntax for a conditional is:

condition ? true : false

Some expression that must return a boolean result is used as the condition; if that expression returns true, then we will use the value provided on the left-hand side of the colon (:) and the right-hand side if false.

When to use a conditional?#

Conditionals are a pretty valuable part of the Terraform DSL. It allows our code to be selective when it runs. Here are some common scenarios when conditionals should be used:

How to use a conditional?#

As a feature flag#

In this section, we will go through a few ways that conditionals can be used as feature flags, there are two ways to achieve this; count and for_each.

In the below count example, we will set the instance count of the resource to be 1 if our variable enabled is true and set it to 0 if it is false. This gives you a straightforward way to create one or more resource instances if your condition is true. The thing to remember here is that you will have to deal with your resource as an array from here on out.

terraform {
  required_providers {
    scratch = {
      source  = "BrendanThompson/scratch"
      version = "0.1.0"
    }
  }
}

variable "enabled" {
  type        = bool
  description = <<EOT
    (Optional) Enable feature X.

    Default: true
  EOT
  default     = true
}

resource "scratch_string" "this" {
  count = (var.enabled ? 1 : 0)

  in = "Enabled"
}

We will use the same variable as above (enabled) for our following scenario. We are going to create one or more instances of an object where we will be setting some properties, we will use a locals block.

When the condition is true, our code will iterate over a map defined in our locals, although this could be defined anywhere, including the output of another resource or data source. This option allows us a lot more flexibility as we can change properties on each instance of our resource and we could even include conditionals inside the resource to further let us define if a property is required or not!

variable "enabled" {
  ...
}

locals {
  networks = {
    primary = {
      name          = "primary-network"
      address_space = "10.0.0.0/24"
    }
    secondary = {
      name          = "secondary-network"
      address_space = "10.0.1.0/24"
    }
  }
}

resource "scratch_string" "complex" {
  for_each = (var.enabled ? local.networks : {})

  in = format("Name: %s, Address Space: %s", each.value.name, each.value.address_space)
}

Dynamic Blocks#

In this instance, we will be dynamically set the properties for the SSH Key on an Azure Virtual Machine instance. We will use some of the above for conditionally enabling or disabling properties on the resources as well!

provider "azurerm" {
  features {}
}

variable "ssh_enabled" {
  type        = bool
  description = <<EOT
    (Optional) Enable feature SSH key authentication.

    Default: true
  EOT
  default     = true
}

locals {
  ssh_keys = {
    bltadmin = file("/Users/brendanthompson/.ssh/bltadmin.pub")
  }
}

resource "azurerm_resource_group" "this" {
  name     = "rg-aus-blt"
  location = "Australia Southeast"
}

resource "random_password" "this" {
  count  = (var.ssh_enabled ? 0 : 1)
  length = 16
}

resource "azurerm_virtual_network" "this" {
  name                = "vn-aus-blt"
  address_space       = ["10.0.0.0/24"]
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name
}

resource "azurerm_subnet" "this" {
  name                 = "sn-iaas"
  resource_group_name  = azurerm_resource_group.this.name
  virtual_network_name = azurerm_virtual_network.this.name
  address_prefixes     = ["10.0.0.0/24"]
}

resource "azurerm_network_interface" "this" {
  name                = "nic-blt"
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.this.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "this" {
  name                            = "vmblt"
  resource_group_name             = azurerm_resource_group.this.name
  location                        = azurerm_resource_group.this.location
  size                            = "Standard_F2"
  admin_username                  = "bltadmin"
  admin_password                  = (var.ssh_enabled ? null : random_password.this[0].result)
  disable_password_authentication = (var.ssh_enabled ? true : false)


  network_interface_ids = [
    azurerm_network_interface.this.id,
  ]

  dynamic "admin_ssh_key" {
    for_each = (var.ssh_enabled ? local.ssh_keys : {})
    content {
      username   = admin_ssh_key.key
      public_key = admin_ssh_key.value
    }
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }
}

The following excerpt demonstrates how we use dynamic blocks:

resource "azurerm_linux_virtual_machine" "this" {
  ...

  dynamic "admin_ssh_key" {
    for_each = (var.ssh_enabled ? local.ssh_keys : {})
    content {
      username   = admin_ssh_key.key
      public_key = admin_ssh_key.value
    }
  }

  ...
}

If the variable ssh_enabled is set to true, we are going to create an instance of the admin_ssh_key block and consume some properties from the locals block we have defined. If the condition is false then we will pass in an empty map; this will mean that the entire block will not get set! Doing this is even more helpful when creating multiple instances of a block. You want to pass in the parameters via a Terraform variable or ingest out of config.

Resource Fields#

The last scenario I would like to go through in this post is how to enable or disable fields on a resource using conditionals. There are situations where you might do this either on its own or in conjunction with one of the other conditional scenarios described above. I will use the above Virtual Machine example to talk through this; however, I will pull out the relevant pieces for the sake of clarity.

In the below excerpt, it can be seen that we are applying some conditional logic on the admin_password and disable_password_authentication properties. When var.ssh_enabled is false, we still need to authenticate to our instance; the only other way Azure allows you to do this is via password authentication. Thus, when it is disabled, we are enabling the creation of the random_password resource (see the above), and setting its result to the admin_password property, we must ensure that disable_password_authentication is set to true.

resource "azurerm_linux_virtual_machine" "this" {
  ...

  admin_password                  = (var.ssh_enabled ? null : random_password.this[0].result)
  disable_password_authentication = (var.ssh_enabled ? true : false)

  ...
}

Closing Out#

Hopefully, this post has given you some insight into how to use conditionals in Terraform and some typical scenarios where they could be used.


Brendan Thompson

Principal Cloud Engineer

Discuss on Twitter