Terraform Variable Validation

Brendan Thompson • 3 December 2021 • 6 min read

In the past, I have spoken about Terraform variable validation in a TIL post. I thought I would expand on that a little more, given that it has been out for a while now.

Variable validation is compelling, but it has some downfalls that we will discuss later.

In order to add validation to an Input Variable you will need to add one or more validation blocks to the variable block. validation require two fields:

My general standard for the error_message is to prefix with Err: and finish off with the period. This also ensures that your consumers know that this is an error.

How to validate variables#

The first Input Variable we will talk about is going to be location; this is going to be used across all clouds. We may want to constrain a consumer to pass in only specific locations. Perhaps your organisation/project will abstract the location away from what the cloud defines it to be and use your custom name. Validation will allow us to deal with both of these situations. We will write a simple implementation to ensure that our consumers can only pass in one of the Australian regions. I will show a few ways to achieve this.

This first example will go through and regex on the variable to make sure that it is one of the following values:

If the passed-in value does not match one of these, an error is returned to the consumer.

variable "location" {
  type        = string
  description = <<EOT
  (Optional) The region in which to deploy our resources to.

  Options:
  - australiasoutheast
  - australiaeast
  - australiacentral1
  - australiacentral2

  Default: australiasoutheast
  EOT
  default     = "australiaeast"

  validation {
    condition     = can(regex("^australiaeast$|^australiasoutheast$|^australiacentral1$|^australiacentral2$", var.location))
    error_message = "Err: location is not valid."
  }
}

Instead of using a regex another way to achieve this would be to use the contains function, like below:

variable "location" {
  ...

  validation {
    condition = contains(
      ["australiaeast", "australiasoutheast", "australiacentral1", "australiacentral2"],
      var.location
    )
    error_message = "Err: location is not valid."
  }
}

Using this feels a lot cleaner than the regex to me, as we know exactly what we are expecting the inputs to be.

You might notice from the above that all of the options we intend to allow have an australia prefix, which leads to our second example. Instead of calling out each option individually in our regex, we can use a * to say match everything after. So what we have below is a regex to ensure the passed value is prefixed with australia.

variable "location" {
  ...

  validation {
    condition     = can(regex("^australia*", var.location))
    error_message = "Err: provided location is not within australia."
  }
}

You might be asking yourself:

Can I use multiple validation blocks on a single input?

The answer to this is a resounding yes! As you can see below, we are validating that our environment is from a specific list and a certain length. This scenario is not a real world one as there would be no point validating the length of a restricted list; however, it shows that multiple validation blocks can be used on a single variable or field.

variable "environment" {
  type        = string
  description = <<EOT
  (Optional) The environment short name to use for the deployed resources.

  Options:
  - dev
  - uat
  - prd

  Default: dev
  EOT
  default     = "prd"

  validation {
    condition     = can(regex("^dev$|^uat$|^prd$", var.environment))
    error_message = "Err: invalid environment."
  }

  validation {
    condition     = length(var.environment) <= 3
    error_message = "Err: environment is too long."
  }
}

Now jumping into something a little more complicated, we will look at some variable validation for complex objects. The object we will look at is a landing_zone, which has properties to create networks, and the landing_zone name and cost centre values.

variable "landing_zone" {
  type = object({
    name        = string
    cost_centre = number
    network = object({
      prefix        = string
      address_space = string
      subnets = list(object({
        name          = string
        address_space = string
      }))
    })
  })

  description = <<EOT
  (Optional) Details for the Azure landing zone.
  ...
  EOT
  default = {
    name        = "blt"
    cost_centre = 666
    network = {
      prefix        = "core"
      address_space = "10.0.0.0/21"
      subnets = [
        {
          name          = "subnet_1"
          address_space = "10.0.1.0/24"
        },
        {
          name          = "subnet_2"
          address_space = "10.0.2.0/24"
        },
        {
          name          = "subnet_3"
          address_space = "10.0.3.0/24"
        }
      ]
    }
  }

  validation {
    condition     = length(var.landing_zone.network.subnets) <= 3
    error_message = "Err: cannot have more than 3 subnets defined."
  }

  validation {
    condition     = var.landing_zone.cost_centre <= 1000
    error_message = "Err: cost_centre must be less than 1001."
  }

  validation {
    condition = length([
      for s in var.landing_zone.network.subnets : s
      if can(regex("^subnet_*", s.name))
    ]) == length(var.landing_zone.network.subnets)

    error_message = "Err: one or more of the subnets are misnamed."
  }
}

Some interesting points here are that you can test individual fields on an object, and you are not forced to validate all fields. In the above example, we are ensuring that there are not more than three subnets on any given network, that our cost_centre value is less than 1000 and that all the subnet names match our convention.

A helpful thing to test, which is currently not possible, would be validating that the subnet range is actually within the bounds of the network!

Pitfalls#

I would say the biggest pitfall for me when it comes to variable validation is that you can only reference the variable itself. You cannot interact with locals. A reason you might want to interact with a locals (or something similar to it) is to define a list/map of valid values and compare your input to that list triggering an error if there isn't a match.

A GitHub issue has already been raised here to get this added to Terraform, although there has not been much activity on it recently.

Final Thoughts#

Terraform variables are something that engineers writing Terraform will use every day. Variables are how we instruct our code to build resources the way we want them to be made. It is certainly not 100% perfect, but it is an absolute killer feature of the Terraform language! Before validation was introduced, it wasn't easy to check if the values passed into the Terraform configuration were valid or what we expected them to be. Validation helped solve this problem and assisted us in ensuring that the configuration is passed in is what we intended to be passed in.


Brendan Thompson

Principal Cloud Engineer

Discuss on Twitter