Skip to content
Infrastructure as Code iac variables 4 min read

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_message with 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

GoalPattern
Allowed valuescontains(["a", "b"], var.x)
Numeric rangevar.x >= 1 && var.x <= 100
Regex / formatcan(regex("^...$", var.x))
Minimum lengthlength(var.x) > 0
All elements validalltrue([for v in var.x : ...])
Parses as a typecan(cidrhost(var.x, 0))
Non-empty stringtrimspace(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 condition could only reference its own variable. If you need broad compatibility, move cross-field rules into a precondition on a resource or check block instead.

Best Practices

  • Write the error_message for the person typing the value, not the maintainer — state what is allowed, not just what is wrong.
  • Prefer several focused validation blocks over one giant boolean so failures point at the exact rule that broke.
  • Wrap parsing and regex() calls in can() 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 default so 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/postcondition blocks; validation is for input known at plan time.
Last updated June 14, 2026
Was this helpful?