Variable Validation
Input variables accept any value that matches their type by default, which means a typo or an out-of-range number quietly flows into your resources and only fails much later — often during apply, against real cloud APIs. Variable validation lets you attach explicit rules to a variable so that bad input is rejected immediately at plan time, with a clear, human-readable message. This turns silent misconfiguration into a fast, actionable error and keeps your modules self-documenting. Validation blocks are supported in Terraform 1.5+ and are fully compatible with OpenTofu.
How validation blocks work
A validation block lives inside a variable block. It has two required arguments: a condition expression that must evaluate to true for the value to be accepted, and an error_message shown when the condition is false. You may declare multiple validation blocks per variable; every one is checked, so users see all failures at once.
The condition typically references the variable through var.<name> and must return a single boolean.
variable "environment" {
type = string
description = "Deployment environment."
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
If someone runs a plan with environment = "production", Terraform stops before evaluating any resources.
Output:
╷
│ Error: Invalid value for variable
│
│ on variables.tf line 1:
│ 1: variable "environment" {
│ ├────────────────
│ │ var.environment is "production"
│
│ environment must be one of: dev, staging, prod.
│
│ This was checked by the validation rule at variables.tf:5,3-13.
╵
Always start
error_messagewith a capital letter and end it with a period or punctuation. Terraform enforces this style and will warn you otherwise, keeping error output consistent across modules.
Allowed values
Restricting a string (or number) to a fixed set is the most common check. Use contains against a list of permitted values.
variable "instance_type" {
type = string
default = "t3.micro"
validation {
condition = contains(
["t3.micro", "t3.small", "t3.medium"],
var.instance_type
)
error_message = "instance_type must be a supported t3 size."
}
}
Numeric ranges
For numbers, combine comparison operators. Splitting the check into two validation blocks gives more precise messages, but a single combined condition is also fine.
variable "desired_capacity" {
type = number
description = "Number of instances in the ASG."
validation {
condition = var.desired_capacity >= 1 && var.desired_capacity <= 10
error_message = "desired_capacity must be between 1 and 10 (inclusive)."
}
}
Regex and string format
Use can() together with regex() to validate format without throwing on a non-match. regex() errors when there is no match, and can() traps that error and returns a boolean.
variable "bucket_name" {
type = string
description = "Globally unique S3 bucket name."
validation {
condition = can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", var.bucket_name))
error_message = "bucket_name must be 3-63 chars, lowercase alphanumeric, dots or hyphens."
}
}
The can() wrapper is also the standard pattern for validating that a string parses as a specific type, for example can(cidrhost(var.cidr, 0)) to confirm a value is a valid CIDR block.
Length and collection checks
Functions like length(), alltrue(), and anytrue() validate the size and contents of collections.
variable "availability_zones" {
type = list(string)
description = "AZs to spread subnets across."
validation {
condition = length(var.availability_zones) >= 2
error_message = "Provide at least two availability zones for high availability."
}
validation {
condition = alltrue([for az in var.availability_zones : can(regex("^[a-z]{2}-[a-z]+-[0-9][a-z]$", az))])
error_message = "Each availability zone must look like 'us-east-1a'."
}
}
Common validation functions
| Goal | Pattern |
|---|---|
| Allowed values | contains(["a", "b"], var.x) |
| Numeric range | var.x >= 1 && var.x <= 100 |
| Regex / format | can(regex("^...$", var.x)) |
| Minimum length | length(var.x) > 0 |
| All elements valid | alltrue([for v in var.x : ...]) |
| Parses as a type | can(cidrhost(var.x, 0)) |
| Non-empty string | trimspace(var.x) != "" |
Cross-variable validation
Since Terraform 1.9, a condition may reference other input variables, not just the one it is declared on. This enables rules that span related inputs.
variable "min_size" {
type = number
}
variable "max_size" {
type = number
validation {
condition = var.max_size >= var.min_size
error_message = "max_size must be greater than or equal to min_size."
}
}
On Terraform versions before 1.9 (and equivalent OpenTofu releases), a
conditioncould only reference its own variable. If you need broad compatibility, move cross-field rules into apreconditionon a resource orcheckblock instead.
Best Practices
- Write the
error_messagefor the person typing the value, not the maintainer — state what is allowed, not just what is wrong. - Prefer several focused
validationblocks over one giant boolean so failures point at the exact rule that broke. - Wrap parsing and
regex()calls incan()so a malformed value fails the validation cleanly instead of crashing the expression. - Keep allowed-value lists in sync with reality; pair them with a sensible
defaultso the happy path needs no input. - Validate at the module boundary, where the variable is declared, so every caller inherits the guarantee.
- Reserve runtime data checks (values only known after apply) for
precondition/postconditionblocks;validationis for input known at plan time.