Testing Terraform
Testing Terraform
Infrastructure-as-code that is never tested is a liability disguised as productivity. A broken Terraform module merged to main and applied to production has caused countless outages — misconfigured security groups that open ports to 0.0.0.0/0, VPCs without DNS resolution, RDS instances with no backup retention. The Terraform testing ecosystem now gives you a layered defence: static validation, plan-level assertions, native terraform test, and policy gates in CI — each catching a different class of failure at the earliest possible moment.
Layer 1 — Static Validation
Before any state is touched, two built-in commands eliminate whole categories of errors in seconds:
terraform validate— type-checks your HCL, resolves references, and flags unknown attributes or missing required arguments. It runs without credentials and without network access, making it ideal as a pre-commit hook.terraform fmt -check— enforces canonical formatting. Inconsistent indentation in HCL is not just cosmetic; it is a leading cause of review-blocking diffs and merge conflicts. Fail the pipeline iffmtdisagrees.
validate in your module's own directory as well as in the root that calls it. A module can be syntactically valid in isolation but fail validation when composed with specific variable values.Layer 2 — Plan-Level Assertions
A plan converts your intent into a concrete diff against real state. Reading plans programmatically surfaces surprises before apply:
- Output a machine-readable plan with
terraform plan -out=tfplan && terraform show -json tfplan > plan.jsonand pipe it through tools like conftest or a simplejqscript. - Assert invariants: no resource of type
aws_s3_bucketshould haveacl = "public-read"; noaws_security_group_ruleshould permitcidr_blocks = ["0.0.0.0/0"]on port 22. - At Google and Meta, automated plan analysis runs on every PR, with reviewers receiving a human-readable diff summary generated from the JSON. No reviewer touches infra PRs without seeing the plan artifact.
Layer 3 — terraform test (Native Unit & Integration Tests)
Introduced in Terraform 1.6, the terraform test command loads *.tftest.hcl files alongside your module, provisions real infrastructure, runs assertions, and then destroys everything — in a single idempotent command. This is the closest equivalent to unit tests for infrastructure.
A test file lives next to your module and declares run blocks. Each run can apply or plan, then evaluate assert conditions using Terraform expressions:
Run the suite with terraform test from the module root. Terraform discovers all *.tftest.hcl files automatically. Use -filter=tests/s3_bucket.tftest.hcl to target a specific file during development.
command = plan mode for assertions that do not require real resources (type checks, computed name formats, variable constraints). Reserve command = apply for behaviours you can only verify after provisioning — outputs, data-source lookups, or cross-resource references. This keeps the test suite fast: a suite of 20 plan-mode tests runs in under 30 seconds, while an apply suite can take minutes and incurs cloud costs.Layer 4 — Policy Checks in CI
Policy-as-code tools enforce organisational guardrails that cannot be expressed inside a Terraform module because the module author and the policy author are different teams. HashiCorp Sentinel (Terraform Cloud/Enterprise) and the open-source Open Policy Agent (OPA) with conftest both consume the JSON plan and emit pass/fail verdicts.
terraform test with command = apply against a shared staging environment. These tests provision and destroy real resources — including ones that might match existing names. Isolate test runs in a dedicated throwaway AWS account or project. At scale, teams use Terratest or the native test framework with per-run randomised resource name suffixes (${random_pet.suffix.id}) to guarantee no collision.Putting It All Together — The CI Gate
A mature Terraform CI pipeline is a strict sequence of gates, each cheaper and faster than the one that follows it. Only code that passes every gate reaches human review:
- fmt + validate — milliseconds, no credentials required.
- tflint / checkov / trivy — static analysis for security misconfigurations and deprecated APIs (30–60 seconds).
- Plan + policy check — requires cloud read credentials; blocks on any policy violation.
- terraform test (plan-mode) — fast assertions on computed values.
- terraform test (apply-mode) — full integration tests in isolated accounts; runs on merge to main, not every PR.