From the GoReplay team

GoReplay reproduces production bugs. Proof catches them before production.

See Proof
Published on 11/5/2026

Infrastructure as Code Testing: Layers, Tools and Checks

Infrastructure as Code Testing: Layers, Tools and Checks

Your Terraform pull request is green, the plan looks reasonable, and someone’s waiting for approval. But the change touches a shared load balancer, and nobody can tell you whether existing sessions will survive the new timeout settings. Running another linter won’t answer that question. Deploying directly to production shouldn’t be how you find out.

Infrastructure as code testing works best as ordered gates, with each gate answering a different question at an appropriate cost. Cheap checks belong early. Real cloud deployments belong later. Changes with a wider blast radius need stronger evidence, including proof that applications still behave correctly on the proposed infrastructure.

The Six Test Types and What Each One Catches

Checking a building plan isn’t the same as inspecting the finished walls. The drawing might be internally consistent while the contractor installs the wrong materials. Infrastructure requires the same separation between checking declarations, checking provisioned resources, and checking the service people use.

The useful distinction isn’t between tested and untested infrastructure. It’s between which failure modes you’ve checked and which remain unexamined.

IaC test types by what they catch, when they run and cost

Test typeWhat it catchesWhen it runsRelative cost
Linting and static analysisSyntax errors, style issues, insecure defaultsPre-commit and every pull requestLowest, local execution without cloud calls
Unit tests for modulesVariable logic, default values, output shapeEvery pull requestLow, plan-only or mocked execution
Policy-as-code checksUnencrypted storage, open security groups, missing tags or loggingPlan stageLow, evaluates configuration or plan artifacts
Integration testsResource creation errors, broken dependency wiringMerge to main or nightlyHigher, provisions real cloud resources
End-to-end deployment testsCross-module and cross-account failures, production-like behaviorBefore releaseHighest, provisions and exercises a full environment
Drift detectionChanges made outside code, such as console editsScheduled and on alertLow to moderate, primarily read-only API calls

Treat each row as a separate gate

A module test can prove that an input enables encryption in the planned resource. It can’t prove that the application’s runtime identity can decrypt an object. An integration test can establish that access, but it won’t necessarily exercise a user journey across accounts.

Write a specific expected result for each gate. Figr’s guide to writing effective test cases is useful for turning vague checks into explicit preconditions, actions, and assertions. For infrastructure, include the environment boundary and cleanup expectation too.

The costs in the table are relative operating characteristics, not execution-time promises. Provider initialization, resource readiness, test fixtures, and cleanup can dominate the duration of cloud-backed tests.

Start cheaply: Add static linting and plan-stage policy checks first. They give engineers actionable feedback before the pipeline spends money provisioning resources.

Keep the distinction between unit and integration tests honest. A test that creates a database is an integration test, even if it lives beside a module and uses a unit-test runner.

Why Infrastructure Mistakes Spread Faster Than Code Bugs

A bad network rule in a shared template becomes a bad network rule in every VPC that consumes it. A manual console edit stays local until someone notices. An automated defect ships as the standard.

Many teams now define infrastructure in code, but a meaningful share still apply changes without checks before deployment. That gap is what gives IaC failures their blast radius. One flawed default in a shared module replicates across development, staging and production in a single pipeline run, and cleanup means tracking down every consumer that already applied it.

The propagation path is straightforward.

A software engineer looking at a laptop screen displaying a diagram of a bad rule propagating errors.

Parsing isn’t a safety assessment

A valid template can still produce infrastructure that violates operating requirements. The parser understands the configuration language. It has no knowledge of trust boundaries, recovery targets, or who owns the risk when a default is too permissive.

That is why I treat policy and behavioral evidence as gates with different costs. Policy scans are cheap and fast. They reject known bad patterns at plan time. Disposable environments plus replayed traffic cost more to run, but they catch what policy cannot, like a role that looks correct yet fails at runtime or a latency shift that only appears under realistic load.

Pay particular attention to reusable settings where a single choice repeats everywhere:

  • Network rules: An overly broad ingress default can expose every service using the module.
  • Identity policies: A wildcard permission can spread through shared role definitions.
  • Encryption settings: An optional flag can leave consumers with weaker protection than intended.
  • Storage configuration: Missing access restrictions or observability can become the default for new environments.

Asking reviewers to read more carefully does not solve this governance problem. Give reviewers machine-generated evidence about proposed resources, and make required controls executable. A storage module should do more than accept an encryption argument. Its tests should verify the intended default, reject unsupported combinations, and establish that consumers cannot accidentally bypass mandatory controls.

IaC testing is a governance control, not just a developer convenience. Its primary job is to reject unsafe planned changes before they apply. Runtime inspection remains necessary, but finding a defective shared default after deployment leaves the harder work of finding and repairing every affected consumer.

How to Sequence Tests from Fast Checks to Full Deploys

Each layer must be independently executable, and only configurations that pass earlier gates should advance. Google Cloud’s Terraform testing guidance recommends progressive depth, moving from static analysis through bounded module integration tests to end-to-end deployments.

Use that ordering to control both feedback time and cloud spend. A formatting failure shouldn’t wait behind database provisioning, and a production-like test shouldn’t run against a configuration that already violates policy.

The pipeline should make those promotion boundaries visible.

A diagram outlining the four stages of sequencing tests from commit checks to full deployments.

Order gates by evidence and expense

  1. Start with credential-free commit checks. Run formatting, validation, tflint, and static security scanning without production credentials, blocking findings that meet your agreed severity policy. Keep exceptions explicit, owned, and reviewable rather than suppressing an entire rule because one resource needs an exemption.

  2. Run plan-only module tests on pull requests to exercise input combinations, defaults, and promised outputs. Use mocks where appropriate, and don’t mistake a mocked provider response for evidence that a cloud API will accept the configuration.

  3. Generate a plan and evaluate policy against the intended changes. Check encryption, logging, ownership, network exposure, and destructive actions before provisioning; secure the plan artifact because it can contain sensitive values. Apply the reviewed artifact where your workflow supports it, and invalidate approval when relevant code, inputs, or dependencies change.

  4. On merge, provision a bounded environment in an isolated test account and verify that resources exist, reach readiness, and connect through the intended identities and network paths. Don’t share mutable state between concurrent runs, or an unrelated pull request may become the apparent cause of your failure.

  5. Run production-like deployment tests before release. Exercise cross-module dependencies and important user journeys, adding a behavioral replay gate for HTTP services when the infrastructure change warrants it. Running the full deployment suite on every commit usually buys expensive waiting rather than proportionate risk reduction.

  6. After apply, compare observed resources with declared intent, verify service health, and schedule drift reconciliation. Don’t stop at a successful provisioning exit code, because resource creation and application readiness are different milestones.

Match depth to blast radius

A documentation-only change doesn’t need a cloud deployment. A shared network module, identity boundary, or stateful storage change does need stronger evidence than a small isolated adjustment. Encode that selection in pipeline rules rather than leaving engineers to choose whichever checks are convenient.

The practical details of CI/CD pipeline testing matter here: retain artifacts, identify the configuration under test, and make failures reproducible outside the orchestration interface.

A failed required gate stops promotion. Select the required gates by change risk before execution, not after a slow or inconvenient failure.

Test Data and Environment Strategies

A test result is only useful if you know what environment and data produced it. Give each pull request an isolated stack, or a dedicated account where stronger separation is necessary, with its own state backend location and resource names. Provision it using the code under review, not a hand-maintained staging template.

Keep plan evaluation, integration testing, and full deployments behind distinct roles and account boundaries appropriate to their privileges. Test jobs shouldn’t inherit production credentials. A sound development environment strategy makes these boundaries explicit instead of relying on engineers to remember them.

Make fixtures repeatable and side effects harmless

  • Module fixtures: Use synthetic inputs that cover supported boundaries, invalid combinations, and defaults without importing production records.
  • Production-derived requests: Mask personal fields, payment information, tokens, and sensitive headers at capture. Remove unsuitable requests or redirect their downstream effects to controlled substitutes.
  • Reference data: Seed the same logical records for each run and preserve relationships needed by sessions, authorization, and follow-up requests.
  • External services: Route email, payment, webhook, and messaging integrations to test endpoints or stubs. Network restrictions should enforce that isolation.

Budget for failed runs too

Set a budget alert for each test account, require owner and expiry tags, and run teardown even when provisioning or assertions fail. Add an independent expiry sweeper because the pipeline runner itself can disappear before cleanup executes.

Budget alerts notify; they don’t necessarily stop spending. Restrict permitted resource classes and account quotas where practical, and record failed cleanup as an operational issue with an owner.

A test environment is disposable. Its configuration, fixtures, and cleanup rules aren’t.

Don’t apply that disposability assumption to production databases or regulated records. A reproducible deployment isn’t proof that deleting and recreating a stateful resource is a safe recovery strategy.

Patterns for Module Tests and Policy Gates

A module should expose a small, testable contract: supported inputs, promised outputs, and required resource properties. Keep its tests independent of unrelated modules so a failure points toward the boundary you changed.

Use native Terraform tests when HCL assertions express the contract clearly. Use Terratest in Go when the test needs richer orchestration, SDK calls, retries, or application-level assertions. Neither choice removes the need to distinguish planned values from values available only after deployment.

Exercise the public contract

Consider a storage module that accepts an owner and an encryption setting, then exposes the effective owner tag. A useful plan-only test has this shape:

  • Arrange: Instantiate the module with a synthetic owner, encryption enabled, and an isolated naming prefix.
  • Execute: Run a test case with command = plan, using provider mocking where appropriate.
  • Assert: Check that output.owner_tag equals the supplied owner and that the planned storage resource has encryption enabled.
  • Reject: Add a separate case with an empty owner and assert that input validation fails.

That’s a concrete test contract without coupling the assertion to a generated resource name. For deployment-backed cases, register cleanup before assertions and retain enough diagnostic output to understand partial failures.

Test defaults as deliberately as explicit values. Otherwise, consumers who omit an argument may receive behavior that nobody exercised.

Put enforceable assertions near the code

Use variable validation for input rules, preconditions for requirements that must hold before an operation, and postconditions for guarantees about resulting resources. Use check blocks for ongoing assertions where suitable, but understand their failure behavior: don’t assume a failed check automatically blocks promotion.

Some expressions can be resolved during planning; others must wait until apply because the required values aren’t known yet. Your pipeline needs to distinguish blocking plan-time assertions from post-apply observations.

For an owner-tag policy, evaluate the plan’s taggable managed resources and reject creates or updates whose effective owner value is empty. Report the resource address and policy name so the author can fix the exact violation. Treat unknown values explicitly rather than accepting them as compliant, and maintain narrowly scoped exemptions for resource types that don’t support tags.

Protect consumers and reconcile reality

Shared modules also need contract tests against representative consumers. Removing an output or changing its meaning should fail those tests before the module is released, even if the module’s internal tests still pass.

Avoid asserting on incidental resource ordering, internal labels, or provider-generated identifiers unless they’re part of the interface. Those assertions turn harmless refactors into maintenance work without increasing safety.

NIST SP 800-204C explains that scanning infrastructure code can prevent security issues from reaching production, but can’t replace runtime checks because infrastructure drift remains possible. Analyze out-of-band changes and feed approved corrections back into code.

Test the contract a module promises, not the implementation it happens to use today.

Choosing Tools and Their Trade-offs

No single tool covers every testing layer. Choose tools by the evidence you need and the languages your team can maintain, not by how many scanners you can add to a pull request.

A small toolchain with clear responsibilities is easier to operate than overlapping scanners with inconsistent exemptions. Every blocking finding needs an owner, a remediation path, and a reason it belongs at that gate.

Common IaC testing tools by layer and trade-off

ToolWorks withTest layerMain trade-off
Native Terraform testTerraform HCL; verify compatibility with the selected OpenTofu releaseUnit and integrationBuilt into the toolchain, assertions expressed in HCL
TerratestTerraform, Kubernetes, and other infrastructureIntegrationExercises real resources, with provisioning cost and cleanup work
tflintTerraform with provider pluginsLintingCatches provider-specific problems, not architectural design flaws
CheckovTerraform, CloudFormation, Kubernetes, and morePolicy and static securityBroad built-in coverage, with findings and exceptions to maintain
Open Policy Agent with ConftestStructured configuration and plans, including JSON and YAMLPolicy-as-codeFlexible custom rules, requires learning Rego
cfn-lintAWS CloudFormationLinting and schemaFocused on CloudFormation, not runtime integration
GoReplayCaptured HTTP requestsBehavioral and load replayExercises runtime behavior, requiring isolation and sensitive-data handling

Start with complementary responsibilities

For a Terraform team, start with native plan-only tests, tflint, and a focused Checkov ruleset in the commit and pull-request workflow. Run Conftest against the generated plan for organization-specific requirements. Keep cloud-provisioning native tests in later gates rather than assuming every native test is cheap.

Checkov is useful when its built-in rules match your controls. Conftest earns its place when you need precise organization-specific decisions, such as allowed network relationships or conditional ownership requirements. Don’t maintain the same rule independently in both unless there’s a clear reason.

Terratest is appropriate when proving the result requires interacting with deployed infrastructure. Its adoption cost includes Go code, retry behavior, credentials, diagnostics, and teardown. A test that leaves resources behind isn’t finished.

Replay needs an oracle

A replay that completes proves that the target received and handled traffic. It doesn’t establish correctness. A service can return successful status codes while writing the wrong records or returning incomplete payloads.

Our replay workflow removes unsuitable requests and rewrites the sample for an isolated target with controlled external side effects. Evaluation then measures performance and compares responses and application state, rather than treating replay completion as a pass.

Choose assertions before choosing the tool. If the failure you care about is incorrect authorization, average latency won’t detect it. If the risk is connection exhaustion, response snapshots alone won’t provide enough evidence.

Tools earn their place by detecting a distinct, consequential failure, not by adding another green badge.

Metrics and Drift That Show Your Tests Work

A green suite means little when engineers rerun the same apply four times to get a clean result. 43% of surveyed organizations reported that pattern of repeated reruns in Spacelift’s 2025 Infrastructure Automation Report. Track reruns as a failure signal, with the cause attached to each attempt.

The measures that matter connect gate behavior to operational cost. A fast pipeline that ships broken change is expensive. A quiet drift dashboard is worthless if detection failed without notice.

A graphic displaying six key metrics for measuring the effectiveness of infrastructure as code testing processes.

Metrics that show whether IaC tests protect the team

MetricHow to measureHealthy signal
Change failure rateApplies needing a follow-up fix, divided by total appliesFalling across comparable reporting periods
Time per gateMedian duration of each gate, with queue time tracked separatelyFast checks remain fast enough that engineers don’t bypass them
Flaky test rateRuns that fail then pass unchanged, divided by total runsApproaching zero for static and unit gates
Reruns per deploymentDeployment attempts before successDeclining, with causes recorded rather than hidden by retries
Drift eventsResources changed outside code, counted weeklyRecurring causes disappear after corrective work
Time to detect driftElapsed time between an out-of-band change and detectionWithin the response window defined for the resource’s risk

Separate code defects from environment failures

State-management complexity and module and dependency management are commonly cited among the leading IaC challenges. Shared state, partial applies, upstream dependency changes, and exhausted quotas all produce failures that look like bad code in the logs.

That makes environment provenance part of test evidence. Record provider and module versions, the lockfile, region, account, fixture revision, and initial drift status with every run.

For a suspected transient failure, allow one bounded diagnostic rerun in a clean environment. Keep the original failed result. Automatic retries blur the line between a reliable test and a lucky deployment. Quarantine a flaky test only with a named owner, a repair task, and compensating coverage for the risk it was supposed to control.

Reconcile drift without hiding it

Scheduled reconciliation should compare observed resources with declared configuration and classify each difference. Approved emergency changes go back into code. Unauthorized edits get reverted after review. Provider-side behavior and code defects each get their own fix path.

Do not overwrite every difference on sight. An emergency capacity increase or a change to a stateful resource needs operator judgment before reconciliation runs.

An environment-caused test failure is an infrastructure problem to fix, not a reason to delete the test.

Read the table as a set. Faster gates help only when fewer changes need repair.

Add a Behavioral Gate Before Your Next Release

ResDiary used GoReplay to validate infrastructure requirements during its migration from RackSpace to Azure and successfully migrated its UK and Australia regions. Lewis at ResDiary described the result: “Using GoReplay, we were able to accurately estimate our infrastructure requirements and highlight the flaws in our prospective setup.”

That’s the missing evidence when a configuration passes policy checks but changes application behavior. Wrong timeouts, connection-pooling settings, routing rules, or TLS compatibility can break real requests without violating a static policy.

Use a bounded service-level experiment rather than starting with an entire production estate.

A software engineer monitors live web traffic data on computer screens in a modern tech office environment.

Build a comparison that can fail meaningfully

  1. Select a service and workload. Choose a representative traffic window containing important routes, request sizes, and session patterns. A convenient sample isn’t necessarily representative.

  2. Capture at a suitable plaintext point after TLS termination, record the requests you need, and measure capture overhead. Restrict access to the capture path and retained artifacts.

  3. Sanitize and adapt the sample. Mask sensitive fields, remove unsuitable requests, and rewrite destinations and credentials for the isolated target. Preserve required session dependencies while blocking uncontrolled email, payment, and webhook effects.

  4. Provision the candidate environment from the same IaC under test. Seed its fixtures and run a comparable baseline so application versions or inconsistent data don’t obscure the infrastructure change.

  5. Define the comparison before execution. Evaluate status codes, meaningful payload fields, latency, error rate, and resulting application state. Normalize expected differences such as timestamps, and set acceptance thresholds according to the service’s operating requirements.

  6. Place the replay gate after integration readiness and before final end-to-end promotion. Preserve failures with request identifiers, configuration versions, and relevant telemetry so engineers can reproduce them.

This demonstration provides additional context for a capture-and-replay workflow.

Add this gate when the traffic sample is representative and the change affects shared infrastructure or request handling. If capture coverage is poor or downstream effects aren’t controlled, fix those conditions before treating replay as release evidence. Any breached acceptance threshold blocks promotion, even when provisioning and policy checks pass.


GoReplay captures and replays HTTP traffic against isolated test services, giving your IaC pipeline a way to exercise the runtime consequences of infrastructure changes. Visit GoReplay to start with a filtered workload for one service, then pair it with baseline comparisons and explicit release thresholds.

Ready to Get Started?

Join these successful companies in using GoReplay to improve your testing and deployment processes.

Talk to the GoReplay team

Describe what you want to capture or replay, your deployment, and any PRO requirements. Or email [email protected].

Please leave out credentials and production request data.