Lesson 9 of 16
Section 3: Variables, Expressions, Loops & Meta-Arguments35 min read & walkthrough

Variables, Custom Type Constraints & Validation Rules

01 / TYPES
Strict Types

Enforce strings, numbers, booleans, lists, and structural object constraints.

02 / VALIDATION
Custom Rules

Use 'validation {}' blocks with regex and bounds checks to block invalid deployments.

03 / LOCALS
DRY Expressions

Assign computed formulas and merged objects to 'locals {}' for readable code.

The Big Idea

Hardcoding values like CIDR blocks, instance sizes, and environment names directly inside your resource declarations makes your code brittle, impossible to reuse across environments, and prone to copy-paste drift.

Input Variables serve as the parameters of your infrastructure modules. Like function arguments in programming, they define what callers can customize while establishing strict type constraints, default fallbacks, and human-friendly documentation.

Modern Terraform allows you to build defensive infrastructure through Custom Validation Rules. Instead of waiting for a 15-minute AWS API call to fail because an AMI ID was invalid or an environment name had uppercase letters, you can enforce validation rules that fail instantaneously during the local planning phase.

💡

Jargon Buster: Plain-English Translations

  • Type Constraint: Specifying the data type a variable accepts (e.g., 'type = list(string)' or 'type = object({...})').
  • Validation Block: A block inside a variable declaration containing a boolean 'condition' and an 'error_message' string.
  • Precedence Order: The hierarchy Terraform uses to resolve variable values: CLI flags (-var) override .tfvars, which override environment variables (TF_VAR_*).
  • Locals Block: Named values computed within a module that cannot be directly overridden by external callers.

Production Variable Validation & Computed Locals

# variables.tf: Defensive input variable with custom validation
variable "environment" {
  type        = string
  description = "Deployment environment tier (dev, staging, or prod)"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "The environment variable must be exactly one of: dev, staging, or prod."
  }
}

variable "vpc_cidr" {
  type        = string
  default     = "10.0.0.0/16"
  description = "IPv4 CIDR block for the VPC"

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0)) && endswith(var.vpc_cidr, "/16")
    error_message = "VPC CIDR must be a valid IPv4 address block with a /16 prefix."
  }
}

# locals.tf: Compute derived values and standard tagging policies
locals {
  is_production = var.environment == "prod"
  
  # Standard tagging contract
  standard_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    BillingCode = local.is_production ? "CORP-PROD-01" : "CORP-DEV-NONPROD"
  }
}
⚠️

Beginner Traps & Gotchas

1. Confusing variable default values with required arguments

The Gotcha: Leaving out a default on a variable you intended to be optional, forcing callers to supply dummy values.

The Fix: If an input has a sensible fallback, provide a default. If an input is mandatory (like database credentials), omit default.

2. Exposing secret credentials via standard variables without masking

The Gotcha: Declaring 'variable "db_password" {}' without the 'sensitive = true' flag, printing secrets in terminal logs.

The Fix: Always add 'sensitive = true' to any variable holding tokens, passwords, or private keys.