Putting It All Together
Understanding each testing tool in isolation is useful, but the real value comes from wiring them together into a CI pipeline that runs automatically on every change. Here's how a production-grade pipeline typically looks:
Pull Request Opened
│
├── Stage 1: Static Analysis (< 1 minute)
│ ├── terraform fmt --check # formatting
│ ├── terraform validate # syntax + internal consistency
│ └── tflint # semantic rules + provider checks
│
├── Stage 2: Plan + Policy Check (2–5 minutes)
│ ├── terraform plan -out=tfplan.binary
│ ├── terraform show -json tfplan.binary > tfplan.json
│ └── opa eval --input tfplan.json --data policies/ "data.terraform.deny"
│
└── Stage 3: Integration Tests (10–30 minutes, nightly or on merge)
└── go test ./test/... -timeout 60m -v
Each stage is a gate: if Stage 1 fails, Stage 2 never runs. This fail-fast approach saves time and cloud spend. A formatting error caught in Stage 1 costs nothing; the same error caught during an integration test costs 15 minutes and real cloud resources.
A minimal CI workflow implementing Stages 1 and 2 splits into two jobs: a static-analysis job that formats, validates, and lints, and a plan-and-policy job (gated on the first job passing) that authenticates to the cloud account, generates and converts the plan, and runs the OPA policy check — failing the job outright if any deny rule matched.
Key point: That pipeline authenticates using OIDC-based role assumption rather than long-lived AWS access keys. This is a security best practice: the CI runner assumes an IAM role with the minimum permissions needed to run terraform plan, and the credentials expire automatically after the job completes. Never store AWS access keys as CI secrets if you can avoid it.