Writing Terraform Policy-as-Code Rules With AI
Rego and Sentinel are easy to get subtly wrong. AI can draft policy-as-code for Terraform fast, but every rule needs a failing test before you trust it as a gate.
- #terraform
- #ai
- #policy-as-code
- #opa
- #sentinel
Policy-as-code is supposed to be the safety net that catches what human review misses — the unencrypted bucket, the public security group, the resource without an owner tag. The irony is that the safety net itself is error-prone. Rego and Sentinel have just enough quirks (default values, partial rules, iteration semantics) that a policy can look like it enforces something while actually passing everything. A policy that never denies is worse than no policy, because it gives you false confidence.
AI is good at drafting these rules — it’s a fast junior engineer who knows the syntax cold. But “knows the syntax” and “wrote a rule that fails on the right input” are different things. The discipline that makes AI-written policy safe is simple and non-negotiable: every rule ships with a test that proves it denies a bad input and allows a good one. The model drafts; the test is the proof.
The policy that silently allows everything
Here’s a Rego rule meant to block unencrypted S3 buckets that an AI might hand you:
package terraform.s3
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := sprintf("Bucket %s has no encryption", [resource.address])
}
Looks right. But in modern AWS provider versions, encryption lives in a separate aws_s3_bucket_server_side_encryption_configuration resource, not inside the bucket. So server_side_encryption_configuration is always absent on the bucket, and this rule denies every bucket — or, if the author “fixes” it by loosening the condition, denies none. Either way it’s wrong, and you won’t know until you test it against a real plan.
Always generate against a real plan shape
Don’t let the AI write policy against an imagined input. Give it the actual plan JSON structure:
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
Then:
Here’s a real
terraform show -jsonplan. Write a Conftest/OPA policy that denies any S3 bucket without a correspondingaws_s3_bucket_server_side_encryption_configuration. Use the actual resource structure in this plan, not assumed field names.
Grounding the model in the real JSON eliminates the most common policy bug: writing against fields that don’t exist. The plan is the spec.
Demand the test alongside the rule
This is the rule of the house: no policy without a test. Prompt for both:
Provide the policy AND a test file with two cases: one plan fragment that must be DENIED and one that must be ALLOWED. The test must actually exercise the deny rule.
For OPA/Conftest:
package terraform.s3
test_unencrypted_bucket_denied {
deny[_] with input as {
"resource_changes": [{
"type": "aws_s3_bucket",
"address": "aws_s3_bucket.bad",
"change": {"after": {}}
}]
}
}
test_encrypted_bucket_allowed {
count(deny) == 0 with input as {
"resource_changes": [{
"type": "aws_s3_bucket_server_side_encryption_configuration",
"address": "aws_s3_bucket_server_side_encryption_configuration.good",
"change": {"after": {"rule": [{}]}}
}]
}
}
opa test policy/ -v
If test_unencrypted_bucket_denied passes, you have proof the rule fires on bad input. Without that test you have a hope. AI is excellent at writing these test fixtures — it’s exactly the exhaustive, mechanical work a junior should do — but the test passing is what earns trust, not the model’s say-so.
Pro Tip: Ask the AI to write a deliberately tricky “should allow” case — a resource that’s close to violating but legitimately fine. Those near-miss tests are where over-broad policies get caught. A rule that denies the obvious bad input but also nukes a valid edge case is a rule your platform team will rip out within a week.
Sentinel has the same trap
If you’re on Terraform Cloud/Enterprise with Sentinel, the failure mode is identical — a rule that looks restrictive but evaluates to true:
import "tfplan/v2" as tfplan
s3_buckets = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket" and
rc.mode is "managed"
}
mandatory_tags = rule {
all s3_buckets as _, b {
b.change.after.tags.Owner is not null
}
}
main = rule { mandatory_tags }
The subtlety: if s3_buckets is empty, all over an empty set is true, so a plan with no buckets passes — which is correct here, but the same pattern silently passes when your filter is wrong and matches nothing. Always test with sentinel test against a mock that should fail. The AI writes the mock; the failing run is the evidence.
Make the gate deterministic, keep AI out of the loop
The whole point of policy-as-code is that it’s deterministic: same plan, same verdict, every time, no judgment call. That’s why it pairs so well with AI-assisted Terraform — the model can draft creatively because a non-AI gate has the final word. Wire it into CI:
terraform show -json tfplan > plan.json
conftest test plan.json --policy policy/
The gate runs on the plan, not on the model’s opinion. AI helped write the rules; OPA enforces them. Never let an AI be the gate — its output isn’t reproducible, and a policy gate that gives different answers on Tuesday is not a gate.
The boundary
The AI drafts Rego and Sentinel and their tests. It does not run apply, hold credentials, or have state access. The policies it writes are reviewed, tested to prove they deny correctly, and then enforced by a deterministic engine. That layering — fast junior drafts, tests prove, deterministic gate enforces, human reviews — is what lets you move quickly without shipping a safety net full of holes.
For policy prompt templates, see the prompts library and prompt packs. The code review dashboard gives drafted policies an automated first pass, and more lives under AI for Terraform.
Conclusion
A policy that never denies is the most dangerous artifact in your pipeline because it looks like protection. Ground the AI in real plan JSON so it stops writing against imaginary fields, and refuse any rule that doesn’t ship with a test proving it denies bad input and allows good. The model is a fast junior who knows Rego’s syntax; the failing test and the deterministic gate are what make its work trustworthy.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.