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
planor evenapply.
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
defaultare required; if the caller does not supply a value, Terraform fails at validation time. - Variables with a
defaultare 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
containsor 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 onenvironment.
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.
-
Valid input:
terraform plan -var="environment=dev" -var="app_name=demo-app" -var="instance_count=2"- Here
-varis 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).
- Here
-
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
validationblock. -
Invalid app name:
terraform plan -var="environment=dev" -var="app_name=Demo_App" -var="instance_count=2"You should see the regex‑based
app_nameerror. -
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_countfails 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 -recursiveandterraform validatein CI on every PR. - Use
terraform plan(with-varor*.tfvars) to exercise and gate variable validations for concrete environments (dev, test, prod). - Enforce types and
validationblocks 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.
Leave a Reply