Terraform Variable Validation
Variable validation lets you define rules that input values must satisfy. When a rule fails, Terraform stops before planning with a clear error message — catching configuration mistakes before any API calls are made.
Basic Validation Syntax
variable "variable_name" { type = string
validation { condition = <boolean expression using var.variable_name> error_message = "Human-readable error message." }}A variable can have multiple validation blocks — all must pass.
Common Validation Patterns
Allowed Values (Enum)
variable "environment" { type = string
validation { condition = contains(["dev", "staging", "production"], var.environment) error_message = "environment must be one of: dev, staging, production." }}
variable "log_level" { type = string
validation { condition = contains(["DEBUG", "INFO", "WARN", "ERROR"], var.log_level) error_message = "log_level must be DEBUG, INFO, WARN, or ERROR." }}Numeric Range
variable "replica_count" { type = number
validation { condition = var.replica_count >= 1 && var.replica_count <= 50 error_message = "replica_count must be between 1 and 50." }}
variable "port" { type = number
validation { condition = var.port >= 1 && var.port <= 65535 error_message = "Port number must be between 1 and 65535." }
validation { condition = var.port >= 1024 || contains([80, 443], var.port) error_message = "Non-standard ports must be >= 1024. Only 80 and 443 are allowed below 1024." }}String Pattern with Regex
variable "cluster_name" { type = string
validation { condition = can(regex("^[a-z][a-z0-9-]{2,62}[a-z0-9]$", var.cluster_name)) error_message = "cluster_name must be 4-64 chars, start with a letter, and contain only lowercase letters, numbers, and hyphens." }}
variable "s3_bucket_name" { type = string
validation { condition = can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", var.s3_bucket_name)) error_message = "S3 bucket names must be 3-63 chars, start and end with a letter or number, and contain only lowercase letters, numbers, hyphens, and dots." }}
variable "ecr_image_tag" { type = string
validation { condition = can(regex("^(latest|v[0-9]+\\.[0-9]+\\.[0-9]+)$", var.ecr_image_tag)) error_message = "Image tag must be 'latest' or semantic version like 'v1.2.3'." }}CIDR Block Validation
variable "vpc_cidr" { type = string
validation { condition = can(cidrnetmask(var.vpc_cidr)) error_message = "vpc_cidr must be a valid IPv4 CIDR block (e.g., '10.0.0.0/16')." }
validation { condition = ( can(regex("^10\\.", var.vpc_cidr)) || can(regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\.", var.vpc_cidr)) || can(regex("^192\\.168\\.", var.vpc_cidr)) ) error_message = "vpc_cidr must be a private IP range (10.x.x.x, 172.16-31.x.x, or 192.168.x.x)." }}List Length and Content
variable "availability_zones" { type = list(string)
validation { condition = length(var.availability_zones) >= 2 error_message = "At least 2 availability zones are required for high availability." }
validation { condition = length(var.availability_zones) <= 3 error_message = "No more than 3 availability zones are supported." }
validation { condition = alltrue([ for az in var.availability_zones : can(regex("^[a-z]{2}-[a-z]+-[1-9][a-z]$", az)) ]) error_message = "Each availability zone must be a valid AWS AZ name (e.g., 'us-east-1a')." }}Validating Object Variables
variable "database_config" { type = object({ instance_class = string allocated_storage = number backup_days = number })
validation { condition = contains([ "db.t3.micro", "db.t3.small", "db.t3.medium", "db.r6g.large", "db.r6g.xlarge", "db.r6g.2xlarge" ], var.database_config.instance_class) error_message = "database_config.instance_class must be an approved RDS instance class." }
validation { condition = var.database_config.allocated_storage >= 20 && var.database_config.allocated_storage <= 65536 error_message = "allocated_storage must be between 20 GB and 65,536 GB." }
validation { condition = var.database_config.backup_days >= 1 && var.database_config.backup_days <= 35 error_message = "backup_days must be between 1 and 35." }}Cross-Variable Validation with Locals
Terraform validation blocks can only reference the variable being validated (var.variable_name). For cross-variable validation, use locals with preconditions:
# Lifecycle preconditions (Terraform 1.2+) — can reference any valueresource "aws_db_instance" "main" { instance_class = var.database_config.instance_class allocated_storage = var.database_config.allocated_storage multi_az = var.enable_multi_az
lifecycle { precondition { condition = !(var.environment == "production" && !var.enable_multi_az) error_message = "Production databases must have multi_az enabled." }
precondition { condition = !(var.environment == "production" && var.database_config.backup_days < 7) error_message = "Production databases must have at least 7 days of backup retention." } }}can() Function for Safe Validation
can() returns true if an expression succeeds without an error, false if it errors. Essential for validating format-dependent functions:
# Without can() — if regex doesn't match, the condition itself errors (not returns false)# condition = regex("^[a-z]", var.name) != null # BAD — errors on non-match
# With can() — gracefully handles cases where the expression errorsvalidation { condition = can(regex("^[a-z]", var.name)) error_message = "Name must start with a lowercase letter."}# Validate URL formatvariable "webhook_url" { type = string
validation { condition = can(regex("^https://", var.webhook_url)) error_message = "Webhook URL must start with https://." }}Best Practices for Validation Messages
# Bad error message — tells user what failed, not what to doerror_message = "Invalid value."
# Good error message — specific, actionable, includes valid optionserror_message = "environment must be 'dev', 'staging', or 'production'. Received: ${var.environment}"
# Also good — reference the docs or constraints clearlyerror_message = "instance_type must use an approved class. Approved types: t3.micro, t3.small, t3.medium, t3.large."