How do variable `validation` blocks work, and why use them over ad-hoc checks?

5 minintermediateterraformvalidationvariables

Quick Answer

A `validation` block inside a `variable` declares a `condition` expression and an `error_message`, checked immediately when the variable's value is set — before any resource is planned. It catches bad input at the earliest possible point with a clear, custom error message, rather than letting an invalid value propagate into a resource block and fail later with a confusing provider-level API error (or, worse, silently create something wrong). It moves validation logic into the module's own definition, so every caller gets the same guardrail automatically instead of relying on each caller to remember to check inputs themselves.

Detailed Answer

Without validation, a bad input variable's failure mode is often confusing: an invalid string might sail straight into a resource block and only fail deep inside a provider's API call, with an error message that has nothing to do with the actual root cause (a typo in an environment name, say).

Declaring a validation rule

variable "environment" {
  type = string

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

variable "instance_count" {
  type = number

  validation {
    condition     = var.instance_count > 0 && var.instance_count <= 10
    error_message = "instance_count must be between 1 and 10."
  }
}

condition is any boolean expression referencing the variable itself (and, in modern Terraform versions, potentially other variables); error_message is the custom text shown when the condition evaluates to false. Validation runs at the very start of plan, before any resource evaluation begins.

Why this beats ad-hoc checks

  1. Fails fast, with a clear message. Instead of a cryptic AWS API error three resources deep in the plan, the user immediately sees "environment must be one of: dev, staging, prod." — actionable and specific to the actual mistake.
  2. Centralizes the rule in the module, not the caller. Every consumer of a module automatically gets the same guardrail; nobody has to remember to write their own check before passing a value in.
  3. Multiple validation blocks compose. A single variable can have several validation blocks, each checking a different constraint, each with its own tailored message — clearer than one giant compound condition with a single generic error.
  4. It's documentation, too. A validation block tells the next engineer reading the module exactly what values are acceptable, without needing to trace through how the variable is eventually used deep in some resource's arguments.

A common real-world use

Guarding against common copy-paste mistakes in multi-environment codebases — validating that a CIDR block is actually a valid CIDR (can(cidrhost(var.cidr_block, 0))), that a region string is one of the org's approved regions, or that a naming convention regex is satisfied (can(regex("^[a-z0-9-]+$", var.name))) — catching exactly the kind of typo that would otherwise only surface as a failed apply partway through provisioning.

Related Resources