Skip to content

Policy as Code and Static Analysis

Static analysis tools catch known-bad patterns in your HCL before a plan ever runs, while policy-as-code tools evaluate the actual computed plan against organizational rules and mechanically block it from being applied if it violates them.

terraform validate only checks that your HCL is syntactically well-formed and internally consistent — types match, required arguments are present, references resolve. It has no idea whether a f1-micro machine type is a sensible choice or whether a Cloud Storage bucket should really be publicly readable. Two categories of tool fill that gap, and both run fast, before anything is planned or applied:

  • tflint is provider-aware. It ships plugin rule sets, including tflint-ruleset-google, that catch mistakes validate cannot see, because they require knowing what the real Google Cloud API actually accepts — a deprecated machine type, an invalid region name, an argument a provider has quietly stopped supporting.
  • A security and misconfiguration scanner in the checkov/tfsec-successor family pattern-matches the HCL itself against known bad configurations: a Cloud Storage bucket with public read access, an unencrypted persistent disk, a firewall rule open to 0.0.0.0/0 on a sensitive port. tfsec covered similar ground and its checks have since been folded into Trivy; checkov remains a widely used, actively maintained choice for this layer.
.tflint.hcl
plugin "google" {
enabled = true
version = "0.29.0"
source = "github.com/terraform-linters/tflint-ruleset-google"
}
Terminal window
tflint --init
tflint
# 1 issue(s) found:
#
# Warning: "us-central1-z" is an invalid value as zone (google_compute_instance_invalid_zone)
#
# on main.tf line 12:
# 12: zone = "us-central1-z"
checkov -d .
# Check: CKV_GCP_28: "Ensure that Cloud Storage bucket is not anonymously or publicly accessible"
# FAILED for resource: google_storage_bucket.reports
# File: /main.tf:5-8

Both tools run against the raw configuration — no GCP credentials, no plan, no state. That is exactly why they belong as the very first gate in a pipeline: they are cheap, fast, and catch an entire class of mistake before Terraform ever talks to Google Cloud.

Policy-as-code at plan time: Sentinel and OPA with conftest

Section titled “Policy-as-code at plan time: Sentinel and OPA with conftest”

Static analysis stops at the code. It has no way to answer a question like “does this plan create more than five n2-standard-4 instances in the production project” — that number does not exist until a plan has actually been computed. Policy-as-code tools close that gap by evaluating the plan’s machine-readable output, not the HCL:

  • Sentinel is HCP Terraform’s built-in policy engine. It is tightly integrated with HCP Terraform and Terraform Enterprise runs — policies are attached to a workspace or organization and evaluated automatically as part of every run there.
  • Open Policy Agent (OPA), used through the conftest CLI, is the more provider-agnostic, open-source alternative: it works against any CI system, with or without HCP Terraform, by evaluating a JSON document you hand it.

The shared idea is the same either way: someone writes a policy once — “no Cloud Storage bucket may allow public access,” “every resource must carry a cost-center label” — and it is evaluated automatically against every plan from then on, blocking a violating plan from ever being applied. It is a mechanical, always-enforced version of what a human reviewer would otherwise have to remember to check by eye, every single time, without fail.

Terminal window
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
conftest test plan.json
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_storage_bucket_iam_binding"
resource.change.after.members[_] == "allUsers"
msg := sprintf("%s grants public access via allUsers", [resource.address])
}

conftest reads the same plan.json that a person could read by hand, and rejects the run the moment deny produces any message — no plan matching that rule ever reaches apply.

Two complementary layers, not redundant ones

Section titled “Two complementary layers, not redundant ones”

It is tempting to see tflint/checkov and Sentinel/OPA as doing the same job twice, but they check fundamentally different things:

  • Static analysis checks the code itself, independent of what is actually changing. It would flag a public Cloud Storage bucket the same way whether this run creates one bucket or a hundred.
  • Policy-as-code checks the actual planned change, and can reference real, computed values that only exist once a plan exists — an actual instance count, an actual label value, an actual member list that only got resolved after variable interpolation and module composition ran. A rule like “block any plan that would create more than five n2-standard-4 instances in the production project” simply has no equivalent at the raw-HCL level; there is no way to know the instance count without computing a plan first.

Both stages belong in the same pipeline, back to back: cheap static checks first, catching whole classes of mistake instantly; policy evaluation against the computed plan second, catching the specific, situational violations that only show up once Terraform knows exactly what it is about to do.

flowchart LR
  hcl["HCL code"] --> lint["tflint / checkov: static checks"]
  lint -->|passes| plan["terraform plan"]
  plan --> policy["Sentinel / OPA + conftest: policy check"]
  policy -->|passes| apply["apply"]
  policy -->|violates a rule| blocked["Blocked, never applied"]
HCL passes through static analysis, then a plan is computed and checked against policy before apply is allowed
What can tflint and checkov catch that plain terraform validate cannot
What is the key difference between what static analysis tools check and what policy-as-code tools like Sentinel or OPA check
Which policy could only be enforced against a computed plan, not against raw HCL alone
How does Sentinel differ from Open Policy Agent with conftest