Infrastructure code that manages production deserves the testing pyramid application code gets, and the tooling finally supports it properly. Today: the layers, from linting to live tests, and where each belongs in the pipeline.
Static Analysis: Fast and Always On
The base of the pyramid runs on every commit in seconds. terraform fmt and validate as table stakes. tflint with the azurerm ruleset catches provider specific mistakes, invalid VM sizes, deprecated arguments, naming violations, before a plan is ever run. Security scanners (Trivy in config mode, or Checkov) evaluate the configuration against the same hardening this series preaches, storage accounts with public access, missing TLS floors, wide open NSGs, and fail the pull request rather than filing a ticket. Tune them honestly: baseline suppress the findings you consciously accept with a comment explaining why, so the signal stays clean and developers keep trusting the gate. This layer catches the majority of defects for the least cost, which is why it is not optional anywhere.
The Native Test Framework
terraform test runs .tftest.hcl files containing run blocks: each executes plan or apply against the module with given variables and asserts on the resulting values. Two modes matter. Plan mode tests are unit tests, fast, no Azure resources created, ideal for asserting your variable validation, conditional logic, and computed naming:
run "public_access_is_rejected" {
command = plan
variables {
name = "stbadexample"
public_network_open = true
}
expect_failures = [var.public_network_open]
}
run "naming_convention_applied" {
command = plan
variables {
name = "orders"
environment = "prod"
}
assert {
condition = azurerm_storage_account.this.name == "stordersprodweu"
error_message = "Storage account name did not follow convention."
}
}
Apply mode tests are integration tests: the framework creates real resources (in a dedicated test subscription with aggressive cleanup and budget alarms), asserts against real outputs, and destroys on completion. Reserve them for what plans cannot prove, that the private endpoint actually resolves, that the identity can actually read the vault, and run them on module release rather than every commit, because they cost minutes and money. Mocking providers and override blocks let you fake expensive dependencies in intermediate cases. The discipline point: modules with consumers get tests as a release requirement, the same way libraries get tests, and a module PR without a test for its behavior change is incomplete.
Plan Review and Policy Gates
The plan itself is a test artifact, and the pipeline should treat it as one: post the plan summary to the pull request, require human review with special attention to destroys and replacements, and encode the non negotiables as automated plan checks, a small script or OPA policy over the JSON plan that blocks destroy of protected resource types, flags replacements of stateful resources, and rejects providers or regions outside the allowlist. This is also where the deploy time gates from the Azure Policy post meet their shift left counterparts: policy denies what escapes the pipeline, the pipeline catches it earlier and cheaper. Close the loop with the drift detection nightly plans from the state post, and infrastructure changes now have the full lifecycle software changes do: linted, unit tested, integration tested on release, reviewed as a diff, gated by policy, and monitored for drift. That is what infrastructure as code actually promised.
Cheers
Osama
Leave a comment