Why policy-as-code matters
The governance problem
Managing infrastructure at scale gets complicated fast.
As your infrastructure grows, keeping it consistent and compliant gets harder. Manual reviews don’t scale past a few teams, and configuration drift arrives on its own. What you need is enforcement that runs on every change without a human in the loop.
Common infrastructure issues
- Developers accidentally making resources publicly accessible
- Missing required tags on cloud resources
- Non-compliant security group configurations
- Kubernetes deployments without resource limits
- Terraform modules bypassing organizational standards
The problem: manual governance
Why manual reviews fail
Code review and post-deployment checks depend on a tired human noticing one line in a 400-line diff. Standards get missed, security rules get worked around, and the issue surfaces after the apply. Guardrails have to run before the deploy to be worth anything.
What it costs
Unenforced policy shows up on the invoice and in the incident review:
- Security breaches from misconfigured resources
- Compliance violations leading to audits and fines
- Cost overruns from unoptimized infrastructure
- Operational chaos from inconsistent deployments
The fix: Open Policy Agent (OPA)
What OPA is
Open Policy Agent is a policy engine that keeps policy logic out of your application and infrastructure code. You write rules in Rego, a declarative query language, and OPA evaluates them against structured input: Terraform plans, Kubernetes manifests, CI/CD configuration, or any other JSON you hand it.
What you get
- Unified enforcement across Terraform, Kubernetes, and custom tools
- Declarative policies that are easy to understand and maintain
- Pre-deployment validation to catch issues before they reach production
- Audit trails for compliance and governance requirements
- Organization-wide standards enforced consistently
Getting started with OPA and Rego
Installing OPA
# macOSbrew install opa
# Linuxcurl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_x86_64chmod +x opasudo mv opa /usr/local/bin/A basic Rego policy
# Deny public S3 bucketspackage s3
deny[msg] { input.resource_type == "aws_s3_bucket" input.acl == "public-read" msg := sprintf("S3 bucket %s cannot be public", [input.name])}
deny[msg] { input.resource_type == "aws_s3_bucket" input.acl == "public-read-write" msg := sprintf("S3 bucket %s cannot be public", [input.name])}Integrating OPA with Terraform
Using Conftest for Terraform
# Install conftestbrew install conftest
# Validate Terraform planterraform plan -json | conftest test -Policy example: enforce tags
package terraform
deny[msg] { resource := input.resource_changes[_] resource.type in ["aws_instance", "aws_rds_cluster"] not resource.change.after.tags.Environment msg := sprintf("Resource %s must have Environment tag", [resource.address])}
deny[msg] { resource := input.resource_changes[_] resource.type in ["aws_instance", "aws_rds_cluster"] not resource.change.after.tags.CostCenter msg := sprintf("Resource %s must have CostCenter tag", [resource.address])}Enforcing policies in Kubernetes
Installing OPA Gatekeeper
# Deploy Gatekeeper to your clusterkubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yamlA Kubernetes ConstraintTemplate
apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequiredResourcesmetadata: name: require-resource-limitsspec: match: kinds: - apiGroups: [''] kinds: ['Pod'] excludedNamespaces: ['kube-system', 'gatekeeper-system']---apiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8srequiredresourcesspec: crd: spec: names: kind: K8sRequiredResources targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequiredresources
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not container.resources.limits.cpu msg := sprintf("Container %s must have CPU limit", [container.name]) }
violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not container.resources.limits.memory msg := sprintf("Container %s must have memory limit", [container.name]) }CI/CD pipeline integration
GitHub Actions example
name: Policy Validation
on: [pull_request]
jobs: policy-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Install OPA run: | curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_x86_64 chmod +x opa sudo mv opa /usr/local/bin/
- name: Run Policy Tests run: | opa test policies/ -v
- name: Validate Terraform if: hashFiles('*.tf') != '' run: | terraform init terraform plan -json | opa eval -d policies/ 'data.terraform.deny' -f prettyBest practices
1. Start at the organization level
Define policies at the organization level, not project level. Make them discoverable and documented.
# Start with clear policy namespacespackage org_policies.infrastructure.aws.securitypackage org_policies.kubernetes.workload2. Fail safely
Distinguish between hard denials and warnings:
package my_policies
deny[msg] { # Hard deny: completely block this input.security_critical_violation == true msg := "This violates critical security policy"}
warn[msg] { # Warning: recommend but allow with approval input.non_standard_naming == true msg := "Consider following naming standards"}3. Test your policies
# Test policy logic before deploymentopa test policies/ -v4. Keep policies in version control
Keep policies in the same repo as infrastructure code, with proper code review processes.
Monitoring and auditing
Log policy violations
Capture and log every policy decision for audit trails:
package audit
log_decision[decision] { decision := { "action": "denied", "reason": input.violation_reason, "timestamp": input.timestamp, "resource": input.resource_id }}A rollout timeline
| Phase | Focus | Timeline |
|---|---|---|
| Phase 1 | Basic security policies (public access, required tags) | Week 1-2 |
| Phase 2 | Terraform integration in CI/CD | Week 3-4 |
| Phase 3 | Kubernetes Gatekeeper deployment | Week 5-6 |
| Phase 4 | Full auditing and monitoring rollout | Week 7-8 |
Wrapping up
Policy-as-code moves the check from after the deploy to before it.
With OPA and Rego, the same rules apply across Terraform, Kubernetes and whatever else you can feed JSON. Start with the security policies you’d be embarrassed to explain in an incident review, get those passing, then widen the net. Rolling out fifty policies at once mostly teaches your team how to add exceptions.






