The release of Terraform 1.9 brings with it a welcome improvement regarding input variable validation. In this post I’ll review the change in functionality and provide a few examples for reference.
If you’d prefer your information in video format, check out the Terraform Tuesday video instead.
A core tenet of programming is to sanitize your inputs, and a big portion of sanitization is making sure the input structure and values match what you expect. In the world of Terraform, there are two controls you can leverage in the variable
block to perform validation of input values.
The first is the type
argument, allowing you to define the data structure you expect the input value to match. You can actually get pretty sophisticated with type
by using the tuple
and object
structural data types to include required and optional keys and elements. But what about the contents of the value? That’s what the validation
block is for.
Inside the variable
block you can include one or more validation
blocks. Those validation blocks include two arguments:
condition
- with a value that is true or falseerror_message
- the message to print if validation failsThe condition
argument can verify that the value(s) submitted match what you’re expecting to receive. You can compare the value to a list of allowed values, a regular expression, a range, or anything else that will result in a bool
value.
Here are a few quick examples:
variable "region" {
type = string
description = "The region where the resources will be deployed"
validation {
condition = can(regex("^us-.*", var.region))
error_message = "Region must start with 'us-'"
}
}
variable "instance_type" {
type = string
description = "The type of instance to be launched"
validation {
condition = contains(["t2.micro", "t3.micro"], var.instance_type)
error_message = "Invalid instance type"
}
}
variable "instance_count" {
type = number
description = "The quantity of instances to deploy"
validation {
condition = var.instance_count >= 1 && var.instance_count <= 20
error_message = "Instance count must be between 1 and 20"
}
}
Validation blocks are pretty cool and I recommend using them.
The problem with variable validation blocks is that prior to Terraform 1.9, they could only reference the variable value being tested. You couldn’t reference other variables, local values, data sources, resources, etc. This made validation somewhat less dynamic than you might want. Consider the following:
Here’s that input variable example that checks if an instance_type
is in an allowed list.
variable "instance_type" {
type = string
description = "The type of instance to be launched"
validation {
condition = contains(["t2.micro", "t3.micro"], var.instance_type)
error_message = "Invalid instance type"
}
}
The allowed list is stored with the variable block. So if I want to change that list, that’s a code change to the config. Wouldn’t it be nice if I could pull that list from somewhere?
Here’s an input variable that uses a subnet ID:
variable "subnet_id" {
type = string
description = "ID of subnet to use for application"
}
Wouldn’t it be nice to check and see if that subnet actually exists?
Or what about this configuration, where I want to use two different regions and make sure they aren’t the same value?
variable "primary_location" {
type = string
description = "Primary location for the resource group"
}
variable "secondary_location" {
type = string
description = "Partner location to use for deployment"
}
The good news is that with the release of Terraform 1.9, the validation block can refer to any other object in the same module. The only restriction is that Terraform needs to know the value during the plan if you want the validation block to fire. More on that later.
Let’s give it a try!
If you’d like to try this out yourself, all my examples can be found on the terraform tuesdays repository.
Let’s take the previous location example for a spin. The first variable is primary_location
and this would be my first or primary location for a deployment. The second is called secondary_location
and I probably don’t want it to be the same as my primary location. That would be silly!
To check that, I’ve simply added a validation block with the condition that the secondary location is not equal to the primary location. If it is, I’ll get back an error message.
variable "primary_location" {
type = string
description = "Primary location for the resource group"
}
variable "secondary_location" {
type = string
description = "Partner location to use for deployment"
validation {
condition = var.secondary_location != var.primary_location
error_message = "Secondary location must be different from the primary location"
}
}
To test it, I have a terraform.tfvars
file that has eastus
for the location and westus
for the partner location.
primary_location = "eastus"
secondary_location = "westus"
I’ll run terraform plan, and once it finishes its process, everything comes back green.
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.main will be created
+ resource "azurerm_resource_group" "main" {
+ id = (known after apply)
+ location = "eastus"
+ name = "main-resources"
}
# azurerm_resource_group.partner will be created
+ resource "azurerm_resource_group" "partner" {
+ id = (known after apply)
+ location = "westus"
+ name = "partner-resources"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Now I’ll change the secondary location to be eastus
and run terraform plan a second time. This time the validation fails and I get a helpful error back.
│ Error: Invalid value for variable
│
│ on terraform.tfvars line 2:
│ 2: secondary_location = "eastus"
│ ├────────────────
│ │ var.primary_location is "eastus"
│ │ var.secondary_location is "eastus"
│
│ Secondary location must be different from the primary location
│
│ This was checked by the validation rule at main.tf:24,3-13.
Neat!
What about using a data source?
For this example, let’s assume I’ve got an existing Virtual Network in Azure with three subnets in it: web, app, and db. In my deployment I want to use one of the subnets, and I want to make sure that the subnet actually exists.
I can use a data source to get the list of subnets for an existing VNet like so:
data "azurerm_virtual_network" "main" {
name = var.vnet_name
resource_group_name = var.resource_group_name
}
For the subnet_name
input variable, I’ve added a validation block that uses the contains
function to check and see if the subnet_name
value is in the list of subnets from the data source.
variable "subnet_name" {
type = string
description = "Name of the subnet"
validation {
condition = contains(data.azurerm_virtual_network.main.subnets, var.subnet_name)
error_message = "Subnet name must be in the list of subnets from the virtual network."
}
}
In the terraform.tfvars
file I have the correct Virtual Network and resource group in it.
vnet_name = "nettest-vnet"
resource_group_name = "nettest-resource-group"
At the command line, I’ll run terraform plan -var subnet_name=“web” and after a few moments the plan comes back successful.
data.azurerm_virtual_network.main: Reading...
data.azurerm_virtual_network.main: Read complete after 0s [id=/subscriptions/4d8e572a-3214-40e9-a26f-8f71ecd24e0d/resourceGroups/nettest-resource-group/providers/Microsoft.Network/virtualNetworks/nettest-vnet]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
The data source is queried before the validation for the input variables is run, so Terraform can reference the contents of the subnet
attribute for the VNet data source. In the subnet
attribute it found the web
subnet, thus all is well.
Now let’s try a subnet name that’s not in the virtual network, like tacos
for instance.
$ terraform plan -var subnet_name="tacos"
...
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Invalid value for variable
│
│ on main.tf line 31:
│ 31: variable "subnet_name" {
│ ├────────────────
│ │ data.azurerm_virtual_network.main.subnets is list of string with 3 elements
│ │ var.subnet_name is "tacos"
│
│ Subnet name must be in the list of subnets from the virtual network.
│
│ This was checked by the validation rule at main.tf:35,3-13.
This time it comes back with a failure and the error message. Sadly, there is no tacos subnet. 😢
Pretty useful stuff!
The validation block improvement in Terraform 1.9 can reference any object in the same module. But what happens when the value for an object is not known during plan? For example, let’s say I reference the attribute of a resource that isn’t known until the resource is created:
variable "partner_resource_group_id" {
type = string
description = "Partner resource group to use for deployment"
validation {
condition = azurerm_resource_group.main.id != var.partner_resource_group_id
error_message = "Secondary resource group must be different from the primary resource group"
}
}
The attribute azurerm_resource_group.main.id
won’t be known until the resource group is created. So if I run terraform plan
before any resources are created, what will Terraform do?
I had assumed that Terraform would do one of two things:
It turns out that Terraform does neither of these things. Instead, it simply ignores the validation block and produces an execution plan. No error, no warning, just produces the plan.
If you apply the plan and the resource is created, Terraform will evaluate the validation block on the next plan run as expected.
I think Terraform should at least issue a warning so you know the validation block was skipped, or the docs should explicitly acknowledge this behavior. Guess I’ve got an issue to log. Shout out to Mattias Fjellström for pointing out this odd behavior.
While it was possible to do this type of checking by hacking something together with pre or post condition blocks for resources and data sources, I think this catches things earlier on in the evaluation cycle. You can also define acceptable values using locals
or reference another input variable in the configuration.Overall this is an excellent addition to the existing variable validation block and I’m excited to see it!
If you’d like to try this feature out for yourself, the example code is in my Terraform Tuesday repository. Thanks for reading!
October 18, 2024
What's New in the AzureRM Provider Version 4?
August 27, 2024
Debugging the AzureRM Provider with VSCode
August 20, 2024
State Encryption with OpenTofu
August 1, 2024