The "Deny" Pattern
The most common pattern in Terraform policy enforcement is a set of deny rules. If any deny rule evaluates to true for a given input, the policy fails. If no deny rules match, the policy passes.
Example 1: Simple Condition — No Friday Deploys
Input data sent to OPA: {"day_of_week": "Friday"}
package main
# The 'deny' set collects all violation messages.
# If it's empty, the policy passes.
deny[msg] {
input.day_of_week == "Friday"
msg := "Deployments are not allowed on Fridays."
}
If input.day_of_week equals "Friday", the rule body is true, and the message is added to the deny set. The CI pipeline checks: is deny empty? If not, it fails.
Example 2: Iterating Over Resources — Require Tags
Checking a Terraform plan is more involved because you must iterate over all resources in the plan. The [_] syntax is Rego's way of saying "for each element in this array."
package terraform.tags
deny[msg] {
# Step 1: Iterate over every resource change in the plan
resource := input.resource_changes[_]
# Step 2: Filter — only apply this rule to S3 buckets
resource.type == "aws_s3_bucket"
# Step 3: Check — does the 'after' state lack an 'Env' tag?
not resource.change.after.tags.Env
# Step 4: Build a descriptive error message
msg := sprintf("S3 bucket '%v' is missing the required 'Env' tag.", [resource.name])
}
Read this rule as: "For every S3 bucket that will exist after this plan is applied, if it does not have an Env tag, add a violation message to the deny set."
Example 3: Allowlisting — Restrict Instance Types
package terraform.compute
# Define the set of allowed instance types
allowed_instance_types := {"t3.micro", "t3.small", "t3.medium"}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
# Get the instance type from the planned state
instance_type := resource.change.after.instance_type
# Check if it's NOT in the allowed set
not allowed_instance_types[instance_type]
msg := sprintf(
"EC2 instance '%v' uses disallowed instance type '%v'. Allowed types: %v",
[resource.name, instance_type, allowed_instance_types]
)
}
This pattern — defining an explicit allowlist and checking membership — is far more maintainable than a blocklist. When a new instance type is approved, you add it to the set. You don't need to update a long list of != conditions.
A common interview question is: "How would you enforce that production resources always have a specific tag, but allow dev resources to skip it?" The answer demonstrates understanding of Rego's conditional logic. You'd add a condition to the rule body: resource.change.after.tags.Environment == "production" — so the rule only fires for production resources. This shows you understand that Rego rules are conjunctions (AND logic): every line in the rule body must be true for the rule to trigger.