Type & Validation #
A variable without a type constraint and validation is just a placeholder — anyone can put in any value, and the error only surfaces when apply is already running and the provider tries to create a resource with the wrong value. Type constraints and validation rules move error detection earlier: to the terraform plan stage, before a single API call is made. This is the difference between failing fast locally and failing slowly in production.
flowchart TD
A["Terraform Data Types"] --> B["Primitive Types\nstring, number, bool"]
A --> C["Collection Types\nlist, map, set"]
A --> D["Object Types\nobject({...})"]
B --> E["Single & simple values"]
C --> F["Collections of the same type"]
D --> G["Structures with varying fields"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fff
style D fill:#8b5cf6,stroke:#6d28d9,color:#fffType Constraints in Detail #
A type constraint ensures a variable accepts the appropriate data type. Terraform will error at plan time if the provided value doesn’t match the declared type.
# PRIMITIVE TYPES with strict constraints
variable "environment" {
type = string
# Accepts: "dev", "production", "staging-01"
# Rejects: 42, true, ["dev"]
}
variable "replica_count" {
type = number
# Accepts: 3, 1.5, 0
# Rejects: "three", true
}
variable "enable_deletion_protection" {
type = bool
# Accepts: true, false
# Also accepts: "true", "false", "1", "0" (Terraform auto-converts)
}
# COLLECTION TYPES with element constraints
variable "allowed_cidrs" {
type = list(string)
description = "CIDR blocks allowed access"
# Accepts: ["10.0.0.0/8", "192.168.0.0/16"]
# Rejects: ["10.0.0.0/8", 42] ← element isn't a string
}
variable "instance_ports" {
type = map(number)
description = "Mapping of service names to port numbers"
# Accepts: { http = 80, https = 443, grpc = 9090 }
# Rejects: { http = "eighty" } ← value isn't a number
}
variable "unique_regions" {
type = set(string)
description = "Set of unique regions (duplicates auto-removed)"
# Accepts: ["ap-southeast-1", "us-east-1"]
# Duplicates in a set are automatically deduplicated by Terraform
}
# OBJECT — the most detailed type constraint
variable "rds_config" {
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
multi_az = bool
backup_retention = optional(number, 7) # Optional field with a default
})
default = {
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
multi_az = false
# backup_retention doesn't need to be set — it has a default of 7
}
}
Validation Rules #
Validation rules let you define constraints more specific than just data types — for example, “this string must be one of the allowed values” or “this number must be greater than 0”.
variable "environment" {
type = string
description = "Deployment environment name"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be one of: dev, staging, production."
}
}
# Try setting environment = "prod" → error at plan:
# ╷
# │ Error: Invalid value for variable
# │
# │ on variables.tf line 1:
# │ 1: variable "environment" {
# │
# │ Environment must be one of: dev, staging, production.
# │
# │ This was checked by the validation rule at variables.tf:6,3-13.
# ╵
variable "replica_count" {
type = number
description = "Number of replicas for the deployment"
validation {
condition = var.replica_count >= 1 && var.replica_count <= 20
error_message = "The replica count must be between 1 and 20."
}
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the main VPC"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "The VPC CIDR must be a valid CIDR block, e.g. 10.0.0.0/16."
}
}
flowchart TD
A["📥 Input Variable"] --> B{"Check Data Type"}
B -->|"Wrong Type"| C["❌ Error:\ntype mismatch"]
B -->|"Right Type"| D{"Check Validation\nRule 1"}
D -->|"Failed"| E["❌ Error:\nmessage from the validation block"]
D -->|"Passed"| F{"Check Validation\nRule N..."}
F -->|"Failed"| E
F -->|"Passed"| G["✅ Value accepted\nProceed to plan"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fff
style D fill:#f59e0b,stroke:#d97706,color:#fff
style E fill:#ef4444,stroke:#dc2626,color:#fff
style F fill:#f59e0b,stroke:#d97706,color:#fff
style G fill:#10b981,stroke:#059669,color:#fffMultiple Validation Blocks #
A single variable can have more than one validation block — each checking a different condition.
variable "instance_type" {
type = string
description = "EC2 instance type"
# Validation 1: Must start with an approved prefix
validation {
condition = can(regex("^(t3|t4g|m5|m6i|c5|c6i|r5|r6i)\\.", var.instance_type))
error_message = "Instance type must use an approved family (t3, t4g, m5, m6i, c5, c6i, r5, r6i)."
}
# Validation 2: Must not use nano or micro sizes in production
validation {
condition = !can(regex("\\.(nano|micro)$", var.instance_type))
error_message = "nano and micro instance sizes aren't allowed — use at least small."
}
}
Validation Referencing Other Variables #
A validation block can reference the variable itself, but can’t reference other variables. For cross-variable validation, use locals.
# ANTI-PATTERN: Trying to reference another variable in validation
variable "max_size" {
type = number
validation {
# ✗ Can't do this — var.min_size isn't accessible here
condition = var.max_size >= var.min_size
error_message = "max_size must be >= min_size."
}
}
# CORRECT: Use locals for cross-variable validation
variable "min_size" {
type = number
}
variable "max_size" {
type = number
}
locals {
# Cross-variable validation using a local value with a condition
validate_size_range = (
var.max_size >= var.min_size
? null
: tobool("max_size (${var.max_size}) must be greater than or equal to min_size (${var.min_size})")
)
}
Common Validation Patterns in Production #
# Validate the AWS resource ID format
variable "vpc_id" {
type = string
validation {
condition = can(regex("^vpc-[a-f0-9]{8,17}$", var.vpc_id))
error_message = "vpc_id must be formatted as 'vpc-' followed by 8-17 hex characters."
}
}
# Validate a safe name for resource naming
variable "project_name" {
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{2,30}[a-z0-9]$", var.project_name))
error_message = "project_name must be lowercase, start with a letter, only letters/numbers/hyphens, 4-32 characters long."
}
}
# Validate a sensitive value isn't empty
variable "db_password" {
type = string
sensitive = true
validation {
condition = length(var.db_password) >= 16
error_message = "The database password must be at least 16 characters."
}
}
# Validate a CIDR doesn't overlap with a forbidden range
variable "app_cidr" {
type = string
validation {
condition = can(cidrhost(var.app_cidr, 0)) && !startswith(var.app_cidr, "169.254.")
error_message = "app_cidr must be a valid CIDR and not link-local (169.254.x.x)."
}
}
Complex Type Validation #
Terraform supports validation for complex types like objects and lists.
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
multi_az = bool
storage_gb = number
})
validation {
condition = contains(["mysql", "postgres", "mariadb"], var.database_config.engine)
error_message = "Engine must be mysql, postgres, or mariadb."
}
validation {
condition = var.database_config.storage_gb >= 20 && var.database_config.storage_gb <= 1000
error_message = "Storage must be between 20 and 1000 GB."
}
}
variable "allowed_ports" {
type = list(number)
validation {
condition = alltrue([for p in var.allowed_ports : p > 0 && p < 65536])
error_message = "All ports must be between 1 and 65535."
}
}
Advanced Validation Patterns #
# Regex validation for a specific format
variable "domain_name" {
type = string
validation {
condition = can(regex("^[a-z0-9][a-z0-9.-]+[a-z0-9]$", var.domain_name))
error_message = "The domain name must be valid (e.g. app.example.com)"
}
}
# Validation with a length check
variable "allowed_cidrs" {
type = list(string)
validation {
condition = length(var.allowed_cidrs) > 0 && length(var.allowed_cidrs) <= 50
error_message = "The number of CIDRs must be between 1 and 50."
}
}
# Validation with alltrue for every element
variable "subnet_cidrs" {
type = list(string)
validation {
condition = alltrue([for cidr in var.subnet_cidrs : can(cidrhost(cidr, 0))])
error_message = "All values must be valid CIDR blocks."
}
}
Nullable Variables #
# Terraform 1.1+ supports nullable parameters
# Nullable = true (default): a null value uses the default
# Nullable = false: a null value is passed through to the module/resource
variable "optional_tags" {
type = map(string)
default = {}
nullable = false
# If the caller doesn't provide a value, null is passed through
# Useful for optional arguments that must be explicitly null
}
# Usage example:
variable "ami_override" {
type = string
default = null
nullable = true
# If null, the resource will use a data source
}
locals {
ami_id = var.ami_override != null ? var.ami_override : data.aws_ami.latest.id
}
Object Type Variables #
# Complex object type
variable "database_config" {
description = "Database configuration"
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
multi_az = bool
backup_retention = number
})
default = {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.micro"
allocated_storage = 20
multi_az = false
backup_retention = 7
}
}
# Optional attributes (Terraform 1.3+)
variable "server_config" {
type = object({
name = string
port = optional(number, 8080)
protocol = optional(string, "https")
})
}
Summary #
- Type constraints catch wrong types at plan time — far better than errors at apply when resources are already partially created.
object()withoptional()for variables with many sub-fields where some fields can be left empty.- Validation blocks for semantic constraints — values that are type-correct but business-invalid: environment may only be “dev/staging/production”, CIDRs must be valid, minimum lengths, etc.
- Multiple validation blocks to separate different conditions — error messages become more specific and helpful.
- Cross-variable validation uses
locals— a validation block can’t access other variables, buttobool()in locals can be used to create informative errors.- Investing in validation rules up front saves debugging time later — errors found at
planare far cheaper than errors found in production.