Skip to content
Infrastructure as Code iac testing 5 min read

Validation & Testing Overview

Infrastructure code deserves the same quality discipline as application code, but the techniques are layered differently. A single terraform apply can create or destroy production resources, so the goal is to catch every class of problem — bad style, broken syntax, anti-patterns, insecure defaults, and incorrect behavior — before anything reaches the cloud. Terraform’s ecosystem gives you a ladder of checks, each faster and cheaper than the one above it, that you run in order so failures surface as early as possible. This page maps that ladder and shows where each rung fits in your local workflow and CI pipeline. Everything here applies equally to OpenTofu.

The quality ladder

Think of validation as a pipeline of increasingly expensive checks. The cheapest run offline in milliseconds; the most expensive provision real infrastructure. You order them so a trivial formatting mistake never wastes time waiting on a security scan or an integration test.

LayerToolWhat it catchesNetwork / cloud?
Formatterraform fmtInconsistent style and indentationNone
Validateterraform validateSyntax errors, bad references, type mismatchesNone (needs init)
LinttflintAnti-patterns, deprecated syntax, invalid instance typesOptional (provider rules)
Security scancheckov, tfsec, trivyInsecure configurations and misconfigurationsNone
PolicyOPA / Sentinel / ConftestOrg-specific rules and guardrailsNone
Unit / contractterraform testModule behavior against plan and applySometimes
IntegrationTerratestReal deployed resources behave correctlyYes (provisions real infra)

Format and validate

The first two rungs are built into Terraform itself and cost nothing to run. terraform fmt rewrites files into canonical HCL so every diff is minimal and reviews stay focused on substance. terraform validate parses the configuration and confirms it is internally consistent — references resolve, types line up, required arguments exist — without making a single API call.

terraform fmt -check -recursive
terraform init -backend=false
terraform validate

Output:

Success! The configuration is valid.

These run in seconds and require no credentials, so they belong at the very front of CI and in a pre-commit hook. See terraform validate for the full command reference.

Linting

validate confirms a configuration is well-formed, but it cannot tell you whether it is sensible. That is the job of a linter. TFLint catches deprecated syntax, unused declarations, and — with its provider plugins — cloud-specific mistakes such as an EC2 instance type that does not exist in the chosen region.

tflint --init
tflint --recursive

Output:

2 issue(s) found:

Warning: instance type "t2.mikro" is invalid (aws_instance_invalid_type)
  on main.tf line 8:
   8:   instance_type = "t2.mikro"

Linting still makes no changes to infrastructure, so it remains a fast, safe early gate.

Security and policy scanning

Even a valid, idiomatic configuration can be dangerously insecure — a publicly readable S3 bucket, an unencrypted volume, an open 0.0.0.0/0 security group. Static security scanners read your HCL and flag these patterns against a built-in rule library. Checkov and tfsec are the most widely used.

checkov -d . --quiet --compact

Output:

Passed checks: 14, Failed checks: 1, Skipped checks: 0

Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
  FAILED for resource: aws_s3_bucket.data
  File: /main.tf:1-4

Where security scanners enforce general best practices, policy as code enforces your organization’s rules — for example, “every resource must carry a CostCenter tag” or “no resources outside eu-west-1.” Tools like Open Policy Agent (Conftest), HashiCorp Sentinel, and Terraform Cloud run policies against the plan JSON, so they can block an apply that would violate a guardrail.

Run security and policy checks against the saved plan output (terraform show -json plan.tfplan) as well as the raw HCL. Scanning the plan catches issues that only become concrete after variables and module inputs are resolved.

Testing behavior

The top rungs verify that your modules actually do the right thing. The native terraform test framework (stable since Terraform 1.6) lets you write assertions in .tftest.hcl files that run against a plan or a real apply and tear down afterward.

run "bucket_is_private" {
  command = plan

  assert {
    condition     = aws_s3_bucket_public_access_block.this.block_public_acls == true
    error_message = "S3 bucket must block public ACLs"
  }
}
terraform test

Output:

tests/storage.tftest.hcl... in progress
  run "bucket_is_private"... pass
tests/storage.tftest.hcl... teardown complete

Success! 1 passed, 0 failed.

For deeper integration testing — deploying a module, then making real HTTP or SDK calls to confirm the resources behave — Terratest drives Terraform from Go. It is slower and provisions billable infrastructure, so it runs last, typically against an ephemeral environment.

Where each layer fits in CI

A typical pipeline runs the rungs in cost order and fails fast. The first three stages are offline and need no cloud credentials; only the integration stage touches real infrastructure.

terraform fmt -check -recursive   # style
terraform validate                # syntax + consistency
tflint --recursive                # lint
checkov -d .                      # security scan
terraform test                    # behavior

The earlier a check sits, the more often it should run: format and validate on every save, lint and scan on every push, native tests on every pull request, and Terratest on a schedule or before release. This ordering keeps feedback tight and cloud spend low.

Best Practices

  • Run the cheapest checks first so a malformed file fails in seconds, not minutes.
  • Make fmt -check and validate non-negotiable gates in both pre-commit and CI.
  • Add a security scanner (Checkov or tfsec) early — insecure defaults are the most common production incident.
  • Scan the resolved plan JSON, not just raw HCL, so dynamically computed values are covered.
  • Use terraform test for fast module-level assertions and reserve Terratest for full integration paths.
  • Pin tool versions (TFLint, Checkov, providers) so CI results are reproducible across machines.
  • Treat policy as code as a guardrail, not a gatekeeper — keep policies few, clear, and well-documented.
Last updated June 14, 2026
Was this helpful?