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:
condition
- MUST return atrue
/false
value in order to be valid.error_message
- MUST start with a capital letter and end in a period (.
).
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:
australiaeast
australiasoutheast
australiacentral1
australiacentral2
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.