Checking the Configuration Before It Applies
Terraform modules handle the creation of compliant resources. But what about enforcing rules that span across modules, or that require business logic too complex for HCL? This is where Open Policy Agent (OPA) comes in.
OPA is a general-purpose policy engine. You write policies in a language called Rego (pronounced "ray-go"), and OPA evaluates those policies against structured data — including Terraform plan output. The tool Conftest makes it easy to run OPA policies against Terraform plans in a CI pipeline.
Example rules that OPA handles well:
* "No repository can be made public unless it is in the opensource GitHub team."
* "All AWS resources must have a cost-center tag."
* "No IAM policy may use a wildcard (*) resource."
How It Fits in the Pipeline
The workflow is:
- Developer runs
terraform plan -out=tfplan.binary - CI converts the plan to JSON:
terraform show -json tfplan.binary > tfplan.json - CI runs
conftest test tfplan.json --policy ./policies/ - If any policy is violated, the pipeline fails before
terraform applyruns.
This is a preventive control — the violation never reaches production.
A Real OPA Policy Example
# policies/github.rego
package main
# Deny any GitHub repository that is set to public visibility
deny[msg] {
resource := input.resource_changes[_]
resource.type == "github_repository"
resource.change.after.visibility == "public"
msg := sprintf(
"Governance Violation: Repository '%s' cannot be public. Submit an exception request.",
[resource.change.after.name]
)
}
# Deny any GitHub repository missing a 'cost-center' topic tag
deny[msg] {
resource := input.resource_changes[_]
resource.type == "github_repository"
not resource.change.after.topics
msg := sprintf(
"Governance Violation: Repository '%s' must have at least one topic tag for cost attribution.",
[resource.change.after.name]
)
}
If a developer's Terraform code tries to create a public repository, conftest test exits with a non-zero code and prints the denial message. The pipeline stops. The developer sees exactly which policy they violated and why.
OPA vs. Terraform Validation: When to Use Each
Terraform's built-in validation blocks (inside variable {}) are good for simple, single-resource constraints — "this string must match this regex." OPA is better for cross-resource rules, organization-wide policies that apply to many different modules, and policies that need to be maintained by a security team independently of the Terraform modules themselves. In practice, mature platforms use both: Terraform validation for module-level guardrails, OPA for organization-level policy.
A common interview question is: "What's the difference between Terraform validation and OPA for policy enforcement?" The key distinction is scope. Terraform validation lives inside a module and enforces constraints on that module's inputs. OPA operates on the entire Terraform plan as a data structure, so it can enforce rules that span multiple resources, multiple modules, or even multiple providers — and it can be maintained by a separate security team without touching the Terraform code at all.