In Terraform projects, format and validation are your first line of defence against messy code and avoidable runtime errors. Think of them as “style checking” and “sanity checking” for infrastructure as code.


Why Format and Validate at All?

Terraform configurations tend to grow into large, multi‑module codebases, often edited by several engineers at once. Without conventions and guards:

  • Small style differences accumulate into noisy diffs.
  • Subtle typos, type mismatches, or broken references sneak into main.
  • Misused modules cause surprises late in plan or even apply.

Formatting (terraform fmt) standardises how the code looks, while validation (terraform validate and variable validation) standardises what values are acceptable.


Formatting Terraform Code with terraform fmt

What terraform fmt Actually Does

terraform fmt rewrites your .tf files into Terraform’s canonical style.

It:

  • Normalises indentation and alignment.
  • Orders and spaces arguments consistently.
  • Applies a single canonical style across the project.

Typical usage:

# Fix formatting in the current directory
terraform fmt

# Recurse through modules and subfolders (what you want in a real repo)
terraform fmt -recursive

For CI or pre‑commit hooks you almost always want:

terraform fmt -check -recursive

This checks formatting, returns a non‑zero exit code if anything is off, but does not modify files. That makes it safe for pipelines.

Why This Matters Architecturally

  • Consistent formatting reduces cognitive load; you can scan resources quickly instead of re‑parsing everyone’s personal style.
  • Diffs stay focused on behaviour instead of whitespace and alignment.
  • A shared style is essential when modules are reused across teams and repos.

Treat terraform fmt like go fmt: it’s not a suggestion, it’s part of the toolchain.


Structural Validation with terraform validate

What terraform validate Checks

terraform validate performs a static analysis of your configuration for syntactic and internal consistency.

It verifies that:

  • HCL syntax is valid.
  • References to variables, locals, modules, resources, and data sources exist.
  • Types are consistent (for example you’re not passing a map where a string is expected).
  • Required attributes exist on resources and data blocks.

Basic usage:

terraform init     # required once before validate
terraform validate

If everything is fine you will see:

Success! The configuration is valid.

This does not contact cloud providers; it is a “compile‑time” check, not an integration test.terraformpilot+1

Why You Want It in Your Workflow

  • Catches simple but common mistakes (typos in attribute names, missing variables, wrong types) before plan.
  • Cheap enough to run on every commit and pull request.
  • Combined with fmt, it gives you a fast gate that keeps obviously broken code out of main.

In CI, a very standard pattern is:

terraform fmt -check -recursive
terraform init -backend=false   # or with backend depending on your setup
terraform validate

You can choose whether init uses the real backend or a local one; the key is that validate runs automatically.


Input Variable Validation: Types and Rules

Terraform also validates values going into your modules via input variables. There are three important layers.

1. Type Constraints

Every variable can and should declare a type: string, number, bool, complex types such as list(string) or object({ ... }). Terraform will reject values that do not conform.

Example:

variable "tags" {
  type = map(string)
}

Passing a list here fails fast, long before any resource is created.

2. Required vs Optional

  • Variables without a default are required; if the caller does not supply a value, Terraform fails at validation time.
  • Variables with a default are optional; they still participate in type and custom validation.

This lets you express what callers must always provide versus what can be inferred or defaulted.

3. Custom validation Blocks

Inside each variable block you can define one or more validation blocks.

Each block has:

  • condition: a boolean expression evaluated against the value.
  • error_message: a human‑readable message if the condition is false.

Example patterns from common practice include:

  • Membership checks with contains or regex.
  • Ranges and integer checks for numbers.
  • Multiple validation blocks to capture several independent rules.

The rationale here is strong: you make invalid states unrepresentable at the module boundary, rather than having to handle them deep inside resource logic.


Beyond Variables: Preconditions and Postconditions

Terraform also lets you validate assumptions around resources and data sources using precondition and postcondition blocks.

  • A precondition asserts something must be true before Terraform creates or updates the object (for example, an input computed from multiple variables is within bounds).
  • A postcondition asserts something must be true after the resource or data source is applied (for example, an attribute returned by the provider matches expectations).

Conceptually:

  • Variable validation guards inputs to modules.
  • Preconditions/postconditions guard behaviour of resources and data sources exposed by those modules.

For a team consuming your module, this is powerful: they get immediate, clear errors about violated invariants instead of mysterious provider failures later.


A Simple Example (Format + Validate + Variable Rules)

Below is a small, self‑contained configuration you can run locally to see formatting and validation in action.

Files

Create the files.

variables.tf:

variable "environment" {
  description = "Deployment environment."
     type        = string

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

variable "app_name" {
  description = "Short application name used in resource naming."
  type        = string

  validation {
    condition     = can(regex("^[a-z0-9-]{3,20}$", var.app_name))
    error_message = "app_name must be 3-20 chars, lowercase letters, digits, and hyphens only."
  }
}

variable "instance_count" {
  description = "Number of instances to run."
  type        = number

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

  validation {
    condition     = !(var.environment == "prod" && var.instance_count < 3)
    error_message = "In prod, instance_count must be at least 3."
  }
}

This demonstrates:

  • Type constraints on all inputs.
  • A small “enumeration” for environment.
  • A format rule enforced via regex on app_name.
  • Multiple independent validation rules on instance_count, including one that depends on environment.

main.tf:

terraform {
  required_version = ">= 1.5.0"
}

locals {
  app_tag = "${var.app_name}-${var.environment}"
}

output "example_tags" {
  value = {
    Environment = var.environment
    App         = var.app_name
    Count       = var.instance_count
    AppTag      = local.app_tag
  }
}

Step 1 – Format the Code

From inside the folder:

terraform fmt -recursive

Observe that Terraform will adjust spacing/indentation if you intentionally misalign something and run it again. This confirms fmt is active and working.

Step 2 – Initialize

terraform init

No providers are actually used here, but validate requires initialization.

Step 3 – Structural Validation

Run:

terraform validate

This checks syntax, references, and type soundness of the configuration itself.

If you see:

Success! The configuration is valid.

you know the configuration is structurally sound.

Step 4 – Test Variable Validation with plan and -var

To exercise your variable validation logic with specific values, use terraform plan with -var flags.

  1. Valid input:

    terraform plan -var="environment=dev" -var="app_name=demo-app" -var="instance_count=2"
    • Here -var is supported and your custom validation blocks are evaluated.
    • This should succeed, producing a plan (no resources, but the important part is that there are no validation errors).
  2. Invalid environment:

    terraform plan -var="environment=stage" -var="app_name=demo-app" -var="instance_count=2"

    Expect Terraform to fail with the custom environment error message from the validation block.

  3. Invalid app name:

    terraform plan -var="environment=dev" -var="app_name=Demo_App" -var="instance_count=2"

    You should see the regex‑based app_name error.

  4. Invalid prod count:

    terraform plan -var="environment=prod" -var="app_name=demo-app" -var="instance_count=1"

    Here, the environment is valid and the type is correct, but the cross‑rule on instance_count fails with your custom prod message.

Optional – Use *.tfvars Instead of -var

If you prefer files over command‑line flags, create dev.auto.tfvars:

environment    = "dev"
app_name       = "demo-app"
instance_count = 2

Then just run:

terraform plan

Terraform will automatically load *.auto.tfvars files and apply the same variable validations.


Recommended Pattern for Teams

Updated to reflect current behaviour:

  • Run terraform fmt -check -recursive and terraform validate in CI on every PR.
  • Use terraform plan (with -var or *.tfvars) to exercise and gate variable validations for concrete environments (dev, test, prod).
  • Enforce types and validation blocks on all externally visible variables, not just a handful.
  • Use preconditions and postconditions where module consumers must rely on specific guarantees from your resources.

From an engineering‑lead perspective, this gives you a clear division of responsibilities:

  • fmt → canonical style.
  • validate → structural soundness of the configuration.
  • plan (with variables) → semantic correctness of inputs and module contracts.