Production engineering guide for policy as code opa covering patterns, implementation strategies, and operational best practices.
Policy As Code Opa enables infrastructure and application teams to define, version, and enforce system-wide policies using a declarative, rule-based language. It matters when teams need to consistently apply compliance, security, and operational rules across cloud environments, Kubernetes clusters, or microservices architectures—especially when policy drift, manual audits, or ad hoc configuration checks are common failure modes. Opa is the standard for policy as code, and this page details how to implement and maintain it in a real engineering workflow.
Rego is Opa’s policy language, built around named rules, sets, and structured data. Policies are written as .rego files and evaluated against input data, typically in JSON. A policy file named authz.rego defines access control rules.
package authz
default allow = false
allow {
input.method = "POST"
input.path = "/api/users"
input.user.role = "admin"
}
allow {
input.method = "GET"
input.path = "/api/users"
input.user.role = "editor"
}
allow {
input.method = "GET"
input.path = "/api/users"
input.user.role = "viewer"
input.user.region = input.request.region
}
Input data is passed at evaluation time. The most common way is via an HTTP request body:
{
"method": "GET",
"path": "/api/users",
"user": {
"role": "viewer",
"region": "us-west-1"
},
"request": {
"region": "us-west-1"
}
}
Opa evaluates all rules and returns a result object with result and input keys. The result is a JSON object with the allow rule’s value, or true if any rule matches.
The opa CLI tool is used for local development, CI, and integration with other tools. The core command is opa eval, which takes input and evaluates a policy.
opa eval --format pretty --input data.json --data policy.rego 'allow'
This command:
policy.rego as the policydata.json as inputallowThe --format flag supports json, pretty, table, and yaml. The --data flag can accept a file path or inline data using --data-dir.
For CI, a script might run:
#!/bin/bash
opa build --format docker --image myorg/policies:latest
opa test --format tap --input-dir test/data
opa build compiles all .rego files into a single static binary or Docker image. The --format docker option generates a Docker image with the policy bundle, including the Opa engine and all rules.
Opa can run as a long-lived server with a REST API. The opa run command starts the service.
opa run \
--server \
--addr 0.0.0.0:8181 \
--log-level info \
--set plugins.kubernetes.enabled=true \
--set plugins.kubernetes.namespace=monitoring \
--set plugins.kubernetes.watch=true
This starts a server listening on :8181 with:
infomonitoring namespace--set key=valueThe server exposes a /v1/policies endpoint to manage policies and a /v1/query endpoint for evaluation.
To evaluate a policy using curl:
curl -X POST http://localhost:8181/v1/query \
-H "Content-Type: application/json" \
-d '{
"query": "data.authz.allow",
"input": {
"method": "POST",
"path": "/api/users",
"user": {
"role": "admin"
}
}
}'
Response:
{
"result": true,
"input": {
"method": "POST",
"path": "/api/users",
"user": {
"role": "admin"
}
}
}
The input is preserved in the response, and result is the evaluated value of data.authz.allow.
Opa integrates with Kubernetes via the opa-kube plugin. When enabled, Opa watches Kubernetes resources and evaluates policies on admission.
To configure Opa to act as a Kubernetes Admission Controller:
--plugins.kubernetes.enabled=true flag.ValidatingWebhookConfiguration pointing to the Opa service.Example ValidatingWebhookConfiguration:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: opa-validating-webhook
webhooks:
- name: opa.example.com
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Fail
sideEffects: None
admissionReviewVersions: ["v1"]
clientConfig:
service:
namespace: monitoring
name: opa
path: /v1/validate
The Opa server evaluates policies defined in data.authz against the Pod object in the admissionReview.request.object field.
A policy to validate pod resource requests:
package authz
deny[reason] {
input.request.object.spec.containers[_].resources.limits.cpu == null
reason = "Pod is missing CPU limit"
}
deny[reason] {
input.request.object.spec.containers[_].resources.limits.memory == null
reason = "Pod is missing memory limit"
}
deny[reason] {
input.request.object.spec.containers[_].resources.limits.cpu < "1"
reason = "CPU limit is less than 1"
}
deny[reason] {
input.request.object.spec.containers[_].resources.limits.memory < "100Mi"
reason = "Memory limit is less than 100Mi"
}
When a pod is created or updated, Kubernetes sends an AdmissionReview object to the Opa server. Opa evaluates the policy and returns an AdmissionReview with allowed: true and status.conditions if validation fails.
Opa includes a built-in test framework using .test files. Each test file defines input, expected output, and optional metadata.
Example: authz_test.rego
package authz
test_allow_admin: {
input := {
"method": "POST",
"path": "/api/users",
"user": {
"role": "admin"
}
}
result := true
}
test_allow_viewer_region_mismatch: {
input := {
"method": "GET",
"path": "/api/users",
"user": {
"role": "viewer",
"region": "us-east-1"
},
"request": {
"region": "us-west-1"
}
}
result := false
}
test_allow_editor: {
input := {
"method": "GET",
"path": "/api/users",
"user": {
"role": "editor"
}
}
result := true
}
Run tests with:
opa test --format tap --input-dir test/data
The --format tap flag outputs in Test Anything Protocol format, ideal for CI. opa test validates that every test_* rule evaluates to the expected result.
If a test fails, the output includes:
A common failure is missing result key in the test file. Opa expects result to be the expected value, but a typo like results or expected causes silent mismatch.
Policies are versioned in Git, with a policies/ directory containing .rego files and test/ subdirectories. A CI pipeline builds and deploys Opa policies on every merge to main.
Example .github/workflows/policy-ci.yml:
name: Policy CI
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Opa Policy
run: |
opa build --format docker --image ghcr.io/myorg/policies:latest
- name: Push to Container Registry
run: |
docker push ghcr.io/myorg/policies:latest
- name: Deploy to Kubernetes
run: |
kubectl apply -f deploy/opa.yaml
kubectl rollout restart deployment opa
The deploy/opa.yaml file defines a Kubernetes Deployment for Opa with the built Docker image and volume mounts for policy files.
A sharp edge: Opa evaluates policies lazily. When a policy is added or updated, it must be loaded into the engine. The opa load command is used to refresh the policy bundle.
opa load --format json --data-dir policies/ --endpoint http://opa.monitoring.svc.cluster.local:8181
This command sends the contents of policies/ to the Opa server at http://opa.monitoring.svc.cluster.local:8181, replacing the current policy bundle. The server applies changes without restart, but during load, the Opa server continues serving requests using the old bundle.
Failure mode: if the policies/ directory contains a file data.json with input data, but the opa load command does not include it, the input key in evaluation is missing, leading to unexpected false results.
input.user.role but input.user.role is input.user.roles[0]. The query evaluates to false silently, with no indication in logs.result key in test files: Opa expects result but a test uses expected, output, or actual, leading to silent test failures..rego file uses data.authz.allow but the file is in policies/authz.rego and the data key is missing in the bundle. Opa cannot resolve data.authz at runtime.plugins.kubernetes.namespace but the config uses kubernetes.namespace or plugins.k8s.namespace, causing the plugin to watch the wrong namespace.allow but the input is not passed, or the input is nested under request.input. The input key is often missing, leading to null values.Policies can be composed using import and package declarations. A policy in policies/common/limits.rego can be imported into authz.rego.
package authz
import data.common.limits
# Validate pod resources
deny[reason] {
input.request.object.spec.containers[_].resources.limits.cpu == null
reason = "Pod is missing CPU limit"
}
# Use a reusable rule from another package
deny[reason] {
input.request.object.spec.containers[_].resources.limits.memory == null
reason = "Pod is missing memory limit"
}
# Use a function defined in another package
allow {
limits.has_cpu_limit(input.request.object, "1")
limits.has_memory_limit(input.request.object, "100Mi")
}
This pattern enables modular, reusable policy libraries across teams and environments.
Policy As Code with Opa is a powerful, standards-based approach to enforcing system-wide rules. The workflow—writing .rego files, testing with opa test, building with opa build, evaluating with opa eval, and deploying with opa run—is repeatable and predictable. Opa’s REST API, Kubernetes integration, and test framework support real-time policy enforcement across complex systems. The key is consistency: every policy is a reusable, versioned, tested unit of logic that can be applied to any input data.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy