slide

Variable validation improvements in terraform 1.9

Ned Bellavance
8 min read

Cover

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.

Variable Validation

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 false
  • error_message - the message to print if validation fails

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

Limitations

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!

Location Example

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?

Network Example

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!

Unknown Values

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:

  1. Issue a warning that the validation block couldn’t be processed and produce an execution plan.
  2. Throw an error that all referenced values must be known during plan.

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.

Final Thoughts

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!