Helpful HashiCorp Links For Module/Variable Validation
- Input Variables – Configuration Language | Terraform | HashiCorp Developer: This page explains how to declare input variables in Terraform modules and customize them without altering the module’s source code.
- Custom Conditions – Configuration Language | Terraform | HashiCorp Developer: If you want to specify custom conditions for input variable validation, this page provides guidance on how to add validation blocks within the variable block.
- Command: validate | Terraform | HashiCorp Developer: The
validate
command in Terraform is useful for verifying the syntactic validity and internal consistency of a configuration, regardless of variables or existing state.
Overview
Terraform input variable validation is a Terraform feature that enables you to enforce data quality or data type constraints on the input variables in your Terraform configuration. This is especially useful when writing modules, as it allows you to enforce the correct usage of the module and provide clear error messages when an input is formatted incorrectly.
Provider-Level Validation
It’s important to note that many Terraform providers perform their own validation on input variables. For GCP, for example, this may be a validation on a project name length or format prior to creation of that resource. This is generated by the API itself and is typically provided at the apply step of the Terraform workflow.
One advantage of provider-level validations is that they are kept up-to-date relative to provider version updates.
An example of provider-level input validation would be the configuration and error message below:
resource "google_sql_database_instance" "postgres" {
name = "pg_instance"
database_version = "POSTGRES_13"
region = "asia-northeast1"
deletion_protection = false
settings {
tier = "db-f1-micro"
disk_size = 10
}
}
resource "google_sql_user" "users" {
name = "postgres"
instance = google_sql_database_instance.postgres.name
password = "admin"
}
$ Error: Error, failed to create instance pg_instance: googleapi: Error 400: Invalid request: instance name (pg_instance)., invalid
The Downsides of Relying on Provider-Level Validation Only
Validation is evaluated at the apply step, meaning user feedback is delayed.
SRE principles emphasize quick feedback and rapid iteration to maintain high availability and reliability of services. delayed feedback can potentially lead to longer resolution times in case of misconfigurations, thus potentially affecting the service’s error budget and increasing toil for the operators, as they would have to spend additional time rectifying the configurations and re-applying them.
Error messages are frequently enigmatic, broad, and lacking specifics on what is erroring your run.
When error messages are not clear or specific, it requires additional effort from users to diagnose and rectify issues (operational overhead). It can also lead to longer Mean Time To Recovery (MTTR) in incident management, reducing the overall reliability of Terraform runs. User satisfaction and trust are negatively impacted as a result. I’ve seen it before.
Clear, actionable, and specific error messages are crucial for rapid troubleshooting in order to minimize downtime and maintain high service quality.
The Benefits of Terraform-Level Variable Input Validation
Terraform input variable validation differs from provider-level validation in that it Terraform input variable validation is evaluated at the plan step of the Terraform workflow whereas provider-level validation occurs during the apply step.
There are a few distinct benefits of input validation:
Better than API Information
- Sometimes, error messages from the API can be difficult to understand or lacking in detail. By validating Terraform variables, you can ensure that the error messages displayed are clear and descriptive, which can make it easier for users to resolve issues with their inputs.
Early Error Detection
- Because input validation is evaluated at the plan step instead of the Terraform
apply
step, a user is prevented from progressing on to theapply
stage with erroneous configurations.
Enhanced Security
- By thoroughly validating inputs, developers can mitigate potential security vulnerabilities stemming from insecure variable input.
A Better User Experience Overall
- Immediate Feedback: Offers quick error identification at the planning stage.
- Helpful User-Friendly Error Messages: Provides specific and clear error details, reducing user frustration.
- Consistency: Ensures a uniform and intuitive user experience across the platform.
- Streamlined Development Process: Enables a more efficient and optimized development workflow.
- User Confidence: Builds user trust by validating inputs and preventing inadvertent mistakes.
- Educational Value: Informs and educates users about system constraints and requirements.
Enhanced Error Clarity
- Granular Error Identification: Helps in pinpointing and correcting input errors swiftly, improving the reliability of Terraform configs.
- Environment-Specific Clarity: Provides error messages that may be tailored or specific to an organization’s unique environment and restrictions, including org-level policies, Sentinel policies, or Wiz.io policies.
Feedback in the Terraform Workflow
The mermaid.js chart below illustrates the Terraform workflow and where input variable validation provides user feedback within that workflow.
Variable Validation Development Best Practices
Terraform has a few requirements for variable validations ; it requires that error messages start with a capital letter and end with a period or question mark. Besides those current limitations, there are some additional best practices when writing variable validation.
Granular Validation Blocks:
- When building validations, make the individual validation blocks as granular as possible to ensure specific error messages are returned to users when an error occurs.
- By making validation blocks more granular, we’re able to provide users more precise and specific error messages, significantly aiding in diagnosing and resolving configuration issues.
- ❌ The validation below combines multiple validations in a single block:
variable "name" {
type = string
validation {
condition = length(var.name) <= 64 && can(regex("^[^_]*$", var.name)) && can(regex("^[a-z0-9]*$", var.name))
error_message = "The name must not exceed 64 characters, must not contain underscores, and must not contain uppercase letters.\n"
}
}
- ✅ It’s preferred to structure the above validation into multiple separate validations:
variable "name" {
type = string
validation {
condition = length(var.name) <= 64
error_message = "The name must not exceed 64 characters."
}
validation {
condition = can(regex("^[^_]*$", var.name))
error_message = "The name must not contain underscores."
}
validation {
condition = can(regex("^[a-z0-9]*$", var.name))
error_message = "The name must not contain uppercase letters."
}
}
Adopt a user-centric approach to Terraform and module development for users.
- Clear & User-Friendly Messages: Strive for clarity in error messages to enhance user experience.
- Guidance in Messages: Error messages should direct users on rectifying issues and, if possible, offer suggestions or acceptable values.
- Informative Links: Including links to API, cloud platform, or Terraform documentation can provide users with easy access to crucial information and further assistance.
Add validation limitations in individual variable description as well as in error messages.
- Proactive Guidance: Clearly stating limitations in variable descriptions can guide users to provide correct inputs.
- Automation & Documentation: When using tools like Terraform-docs for README.md creation, note that individual validation blocks aren’t populated in the generated documentation. However, variable descriptions are included. This is also true for adding modules to the Hashicorp Terraform Cloud Private Module Registry. Therefore, always integrating validation explanations in variable descriptions is crucial for comprehensive documentation.
Test your own validations
- Sometimes Terraform regex and other validation mechanisms can be difficult to ensure validity without an actual test. Luckily, it should be as easy as dropping a few incorrect values in a local module call and running a
terraform plan
orterraform validate
against your validations.
Avoid validation on arguments with requirements that may change in the future.
- Validation Scope & Maintenance: For arguments in a single module, especially when requirements may change and apply to multiple resources and modules, like organizational regional limitations, prefer using broader code scanning tools like Hashicorp Sentinel. This approach minimizes the need for frequent updates to validations across different modules and resources in the future, ensuring that input validations are as resource-specific as possible, reducing the necessity of frequent updates across an organization or within a private module registry.
Helpful Examples
Data Type: Strings
Length
Validate from a List of Allowed Values
Adding a condition with a list of allowed values is useful in many scenarios. For example, checking the VM size is as desired. In the 3 examples below, we
check that the VM size is set to one of 3 values, “Standard_DS2”, “Standard_D2” or “Standard_DS2_v2” :
variable "vm_size" {
type = string
description = "VM Size"
validation {
condition = anytrue([
var.env == "Standard_DS2",
var.env == "Standard_D2",
var.env == "Standard_DS2_v2"
])
error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
}
}
Another way to achieve this without the anytrue function:
variable "vm_size" {
type = string
description = "VM Size"
validation {
condition = contains(["Standard_DS2", "Standard_D2", "Standard_DS2_v2"], var.vm_size)
error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
}
}
Another way with the OR operator:
variable "vm_size" {
type = string
description = "VM Size"validation {
condition = var.vm_size == "Standard_DS2" ||var.vm_size == "Standard_D2"||var.vm_size ==
"Standard_DS2_v2"
error_message = "VM Size must be \"Standard_DS2\", \"Standard_D2\" or \"Standard_DS2_v2\".
}
}
variable "vm_size" {
type = string
# using regex
validation {
condition = can(regex("^(Standard_DS2|Standard_D2)$", var.vm_size))
error_message = "Invalid input, options: \"Standard_DS2\", \"Standard_D2\"."
}
# using contains()
validation {
condition = contains(["approved", "disapproved"], var.string_only_valid_options)
error_message = "Invalid input, options: \"approved\", \"disapproved\"."
}
}
Data Type: Numbers
Number is Within a List of Allowed Values
variable "allowed_num" {
type = number
validation {
condition = contains([1, 2, 5, 10], var.allowed_num)
error_message = "The number must be one of the following: 1, 2, 5, 10."
}
}
Number Must be Between a Range of Values
variable "num_in_range" {
type = number
default = 1
validation {
condition = var.num_in_range >= 1 && var.num_in_range <= 16 && floor(var.num_in_range) == var.
num_in_range
error_message = "Accepted values: 1-16."
}
}
Data Type: Lists
Each entry in a list must start with an allowed list of prefixes
variable "admin_group" {
description = <<INFO
List of users, groups, or service accounts to assign role of
roles/documentai.admin.
This role has the following roles/permissions:
roles/documentai.editor
Must use the following format per member type:
user:{email_id}
group:{email_id}
serviceAccount:{email_id}
INFO
type = list(string)
default = []
validation {
condition = alltrue([
for item in var.admin_group : (
starts_with(item, "user:") ||
starts_with(item, "group:") ||
starts_with(item, "serviceAccount:")
)
])
error_message = "Each member in the admin_group must start with 'user:', 'group:', or 'serviceAccount:'."
}
Each entry in a list must be validated against a deny List
variable "admin_group" {
description = <<INFO
List of users, groups, or service accounts to assign role of
roles/documentai.admin.
This role has the following roles/permissions:
roles/documentai.editor
Must use the following format per member type:
user:{email_id}
group:{email_id}
serviceAccount:{email_id}
INFO
type = list(string)
default = []
validation {
condition = alltrue([
for item in var.admin_group : true
if alltrue([
!contains(["allUsers", "allAuthenticatedUsers"], item)
])
])
error_message = "The admin_group must not contain allUsers or allAuthenticatedUsers."
}
}
Data Type: Maps, Objects, and Lists of Objects
Validate a map with optional conflicting keys
variable "only_one_optional_key" {
type = object({
name = optional(string)
cidrs = optional(list(string))
netmask = optional(number)
})
default = {
cidr = "10.0.0.0/16"
name = "test"
}
validation {
error_message = "Can only specify either \"cidrs\", or \"netmask\"."
condition = length(setintersection(keys(var.only_one_optional_key), ["cidrs", "netmask"])) == 1
}
List of Objects
variable "index_fields" {
type = list(object({
field_path = string
order = string
array_config = string
}))
description = " The fields supported by this index. Refer https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/firestore_index#fields for details."
validation {
condition = length([
for field in var.index_fields : true
if contains(["CONTAINS"], field.array_config) || field.array_config == ""
]) == length(var.index_fields)
error_message = "Array config must by one of the following values: CONTAINS."
}
validation {
condition = length([
for field in var.index_fields : true
if contains(["ASCENDING", "DESCENDING"], field.order) || field.order == ""
]) == length(var.index_fields)
error_message = "Order must by one of the following values: ASCENDING, DESCENDING."
}
validation {
condition = length([
for field in var.index_fields : true
if (field.order == "" && field.array_config != "") || (field.order != "" && field.array_config == "")
]) == length(var.index_fields)
error_message = "Only one of [order] or [array_config] must be provided."
}
}
Unique Input Types
IP Addresses
variable "ip_address" {
type = string
description = "Example to validate IP address."
validation {
condition = can(regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",var.ip_address))
error_message = "Invalid IP address provided."
}
}
Here we are using the can function which evaluates the regex expression and returns a boolean value indicating whether the expression produced a result without any errors.
This would flag any invalid inputs such as :
- “192.0.1.” (missing the last octet)
- “192.65.0.256” (outside of 1–255)
- “10.0.10,180” (comma instead of .)
The following would validate a list of IP addresses:
variable "ip_address_list" {
type = string
description = <<INFO
Example to validate list of IP addresses.
INFO
validation {
condition = can([for ip in var.ip_address_list: regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-
5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-
9]?)$", ip)])
error_message = "Invalid List of IP addresses provided."
}
}
Timestamps
variable "timestamp" {
type = string
description = "Example to validate a timestamp" validation {
condition = can(formatdate("", var.timestamp))
error_message = "The timestamp argument requires a valid RFC 3339 timestamp."
}
}
IPv4 CIDR
variable "string_like_valid_ipv4_cidr" {
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.string_like_valid_ipv4_cidr, 0))
error_message = "Must be valid IPv4 CIDR."
}
}
Semantic Versions
variable "semv1" {
default = "10.57.123"
validation {
error_message = "Must be valid semantic version."
validation {
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-]
[0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)
*))?$", var.semv1))
}
}
}
Email Address
variable "email_address" {
type = string
description = "Example to validate an email address."
validation {
condition = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.email_address))
error_message = "Invalid email address provided."
}
}
Domain Name
variable "domain_name" {
type = string
description = "Example to validate a domain name."
validation {
condition = can(regex("^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$", var.domain_name))
error_message = "Invalid domain name provided."
}
}