Practical Guide to Implementing Kubernetes Policy-as-Code with OPA Bundle Server + GitOps
When operating a Kubernetes cluster, policy requirements such as "which container images should be allowed and which should be blocked" or "resource limits must be set for all Pods" become increasingly common. The problem is that these policies are often scattered throughout the application code, exist only in the minds of the person in charge, or rely solely on manual reviews. In this article, we will examine step-by-step how to deploy an Open Policy Agent (OPA) bundle server to Kubernetes and automatically deploy Rego policies using a GitOps workflow.
Policy-as-Code means managing policies as code in a Git repository rather than in YAML or documentation. When combined with GitOps, policy changes can be reviewed like code pull requests, automatically verified by CI, and deployed to the cluster without downtime. Understanding the flow of the entire pipeline beforehand makes the subsequent explanations flow much more naturally.
Git push → GitHub Actions (opa test → opa build → s3 cp)
→ S3 번들 서버 → OPA 폴링(10~30초 주기) → 클러스터 정책 실시간 반영Through this article, you can learn the operating principles of OPA bundles, how to set up a bundle server, configure pipelines based on GitHub Actions, and patterns that can be immediately applied to actual operations, including Gatekeeper and OPAL.
Key Concepts
OPA and Rego: Expressing Policies in Code
OPA is a CNCF graduation project and a general-purpose policy engine that integrates and manages policies across various environments, including microservices, Kubernetes, and CI/CD pipelines. Policies are written in a declarative language called Rego, completely separating application code from policy logic.
Below is an example of a Rego policy that prohibits the use of latest tag images in OPA working with Kubernetes Admission Webhook.
# policies/k8s/no_latest_tag.rego
# 이 정책은 kube-mgmt 패턴의 OPA Admission Webhook용입니다.
# input.request 구조체를 통해 Kubernetes 요청 원문에 접근합니다.
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
# [_]는 Rego의 와일드카드 이터레이션 문법입니다 — 배열의 모든 요소를 순회합니다.
container := input.request.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("컨테이너 '%v'는 latest 태그를 사용할 수 없습니다.", [container.name])
}What is Rego? It is a declarative query language derived from Datalog. By describing "what is true" rather than "how" to make a decision, you can express even complex policies concisely once you become familiar with it. You can try writing directly in the browser environment of Rego Playground (https://play.openpolicyagent.org)%EC%97%90%EC%84%9C).
Bundle: A unit for packaging policies
A bundle is a unit that combines Rego policy files and static data (including data.json) into a single compressed archive (.tar.gz). OPA instances fetch the latest bundles by periodically polling HTTP(S) bundle servers or subscribing via Long Polling, and update policies in real-time without restarting Pods.
| Type | Description | Suitable Situation |
|---|---|---|
| Snapshot Bundle | Includes full policy and data state. Completely replaces existing cache upon reception | When the number of policies is small or a full update is required |
| Delta Bundle | Includes changes (JSON Patch) only | When network and processing cost reduction is required for large datasets |
Bundles are created with the opa build command.
# policies/ 디렉터리의 모든 Rego 파일과 data.json을 번들로 패키징합니다.
# -b(--bundle) 플래그는 .rego 파일뿐 아니라 data.json 등 정적 데이터도 함께 포함합니다.
opa build -b policies/ -o bundle.tar.gzGitOps: Git as the Single Source of Truth for Policy
GitOps is an operational model that manages infrastructure and configurations through a PR → CI verification → automated deployment cycle, using the Git repository as the Single Source of Truth. When combined with ArgoCD (a CD tool) that automatically synchronizes the cluster state with Git, the following becomes naturally possible simply by storing Rego policies in Git.
- Change history and approver tracking for all policy changes
- Automated verification of policy syntax and logic during the PR phase
- Immediate rollback via
git revertin case of an issue
Practical Application
You can select and use four examples depending on the situation.
| Situation | Recommended Example |
|---|---|
| When you need to immediately block the creation and modification of Kubernetes resources | Example 2 (Gatekeeper + ArgoCD) |
| When applying OPA to various systems such as microservices and Terraforms | Example 1 (GitHub Actions + S3 Bundle) |
| When you want to pre-validate a manifest during the PR phase | Example 3 (Conftest CI) |
| When Bundle Polling Delay (Several Seconds to Tens of Seconds) Is Not Allowed | Example 4 (OPAL Real-time Push) |
Example 1: GitHub Actions + S3 Bundle Server Pipeline
The most common pattern is that when a PR is merged into the policy repository, GitHub Actions builds a bundle and uploads it to S3, and an OPA instance in the cluster (running as a Standalone Deployment) polls it to update the policy.
Repository Structure Example:
opa-policies/
├── policies/
│ ├── k8s/
│ │ ├── no_latest_tag.rego
│ │ └── require_resource_limits.rego
│ └── rbac/
│ └── role_binding_rules.rego
├── tests/
│ └── k8s_test.rego
└── .github/workflows/
└── deploy-bundle.ymlGitHub Actions Workflow:
# .github/workflows/deploy-bundle.yml
name: Build and Deploy OPA Bundle
on:
push:
branches: [main]
paths: ['policies/**']
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup OPA
uses: open-policy-agent/setup-opa@v2
with:
# 프로덕션 환경에서는 재현성을 위해 'latest' 대신 특정 버전을 고정하는 것을 권장합니다.
# 예: version: "0.68.0"
version: latest
- name: Run policy tests
run: opa test policies/ tests/ -v
- name: Build OPA bundle
run: opa build -b policies/ -o bundle.tar.gz
- name: Upload to S3
run: aws s3 cp bundle.tar.gz s3://my-opa-bundles/latest/bundle.tar.gz
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}| Steps | Description |
|---|---|
opa test |
Run unit tests for the Rego policy. Deployment is blocked if it fails. |
opa build -b |
Bundles the .rego files and data.json in the specified directory. |
aws s3 cp |
Upload bundles to S3 to use as a bundle server. |
OPA Configuration in Kubernetes (ConfigMap):
OPA runs as a Standalone Deployment with the ConfigMap below mounted. In a production environment, it is recommended to configure the service account and network policy together, and to set the bundle server endpoint to HTTPS.
# opa-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: opa-config
data:
config.yaml: |
services:
bundle-server:
url: https://s3.amazonaws.com/my-opa-bundles
# 프로덕션에서는 allow_insecure_tls를 기본값(false)으로 유지하세요.
bundles:
main:
service: bundle-server
resource: latest/bundle.tar.gz
polling:
min_delay_seconds: 10
max_delay_seconds: 30Example 2: OPA Gatekeeper + ArgoCD (Kubernetes Admission Control)
It is suitable for cases where the creation and modification of Kubernetes resources need to be blocked in real time. Gatekeeper uses a two-tiered structure of ConstraintTemplate (Rego policy definition) and Constraint (apply parameters), and unlike Example 1, it accesses Kubernetes requests through the input.review structure.
# gatekeeper/templates/no-latest-tag.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snolatestimage
annotations:
argocd.argoproj.io/sync-wave: "1" # Constraint보다 먼저 배포
spec:
crd:
spec:
names:
kind: K8sNoLatestImage
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
# Gatekeeper용 Rego: 패키지명 컨벤션과 input.review 구조가
# kube-mgmt 패턴(예시 1, input.request)과 다름에 주의하세요.
package k8snolatestimage
violation[{"msg": msg}] {
# [_]는 배열의 모든 요소를 순회하는 Rego 와일드카드 문법입니다.
container := input.review.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("latest 태그 사용 불가: %v", [container.image])
}# gatekeeper/constraints/no-latest-tag-constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoLatestImage
metadata:
name: no-latest-image
annotations:
argocd.argoproj.io/sync-wave: "2" # Template 이후 배포
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]# ArgoCD Application — 정책 리포지토리를 감시
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: opa-policies
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/org/opa-policies
path: gatekeeper/
targetRevision: HEAD
destination:
server: https://kubernetes.default.svc
namespace: gatekeeper-system
syncPolicy:
automated:
prune: true
selfHeal: trueWhat is Sync Wave? It is a feature in ArgoCD that controls the order of resource deployment. Resources with lower numbers in the argocd.argoproj.io/sync-wave annotation are deployed first. This order guarantee is key because ConstraintTemplate must be created before Constraint in Gatekeeper.
Example 3: Pre-validation of the CI Phase using Conftest
This is a pattern that pre-validates the Kubernetes manifest as a policy during the PR phase before the bundle is reflected in the cluster. If the policy is violated, the merge itself is blocked.
# .github/workflows/pr-check.yml
name: Policy Validation
on:
pull_request:
paths: ['k8s/**']
jobs:
conftest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# setup-opa와 일관되게 공식 액션을 활용하는 방식을 권장합니다.
- name: Setup Conftest
uses: open-policy-agent/setup-conftest@v0
with:
conftest: "0.50.0"
- name: Validate K8s manifests against policies
# conftest는 Kubernetes 네임스페이스 개념을 다루지 않으므로
# --all-namespaces 플래그 없이 디렉터리와 정책 경로만 지정합니다.
run: conftest test k8s/ --policy policies/Example 4: Pushing Real-Time Policies with OPAL
OPAL can be utilized in environments where bundle polling delays (seconds to tens of seconds) are unacceptable. When the OPAL server detects a Git change, it immediately pushes the policy to OPAL clients (running as a sidecar or separate deployment with OPA) via WebSockets.
# OPAL 서버 및 클라이언트를 Helm으로 배포합니다.
# opal-server: Git 리포지토리를 감시하고 변경을 감지합니다.
# opal-client: OPA 인스턴스와 같은 네임스페이스에 배포되어 정책을 수신합니다.
helm repo add opal https://permitio.github.io/opal-helm-chart
helm install opal-server opal/opal-server \
--set opal_server.policy_repo_url=https://github.com/org/opa-policies \
--set opal_server.policy_repo_main_branch=main
helm install opal-client opal/opal-client \
--set opal_client.server_url=ws://opal-server:7002/wsPros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Version Control & Audit Tracking | All policy changes are recorded as Git commits, making it easy to track change history and approvers. |
| Non-downtime policy updates | Policies are updated in real-time without restarting OPA pods via bundle polling or OPAL push. |
| Separation of Concerns | Completely separate policy logic from application code to enable independent deployment and testing. |
| Multi-cluster Consistency | You can consistently deploy the same policy to tens to hundreds of clusters with a single bundled server. |
| Flexible Deployment Backend | Supports various bundle server options such as HTTP server (Nginx), object storage (S3/GCS), and OCI registry. |
Disadvantages and Precautions
| Item | Content | Response Plan |
|---|---|---|
| Bundle Polling Delay | In the default polling method, there is a delay of several seconds to tens of seconds between a bundle update and the reflection of OPA. | You can implement OPAL or Delta bundles + Long Polling. |
| Bundle Server Operation Overhead | Availability management, TLS authentication, and access control are required for the bundle server itself. | Utilizing S3 or an OCI registry as a bundle server reduces the management burden. |
| Rego Learning Curve | Rego syntax, derived from Datalog, may not be intuitive for developers accustomed to imperative languages. | Using Rego Playground and VS Code OPA Extensions lowers the barrier to entry. |
| ArgoCD Sync Wave Missing | When using Gatekeeper, ConstraintTemplate must be deployed before Constraint. | You can explicitly specify the deployment order using the argocd.argoproj.io/sync-wave annotation. |
What is an OCI Registry Bundle Server? It is a pattern where OPA bundles are pushed to OCI registries such as Harbor, GHCR, and ECR like container images using the ORAS(OCI Registry As Storage) tool, and OPA pulls them directly. Since existing container infrastructure can be reused, bundles can be deployed without operating additional servers.
The Most Common Mistakes in Practice
1. Deployment without policy testing
If opa test is not included in the CI pipeline, policies with syntax errors or logical defects may be deployed to production. It is important to include tests in the pre-build stage to automatically guarantee policy quality.
2. Combining Gatekeeper and Bundle Server Patterns
Gatekeeper is simple if you only need Kubernetes admission control, while the Bundle Server pattern is suitable if you need to apply OPA to multiple systems, such as microservices or Terraform. It is important to clearly define the purpose and scope of both patterns first.
3. Bundle Server TLS Not Configured
If TLS is not applied to communication between the bundle server and the OPA instance, the policy file is transmitted in plain text. In a production environment, it is recommended to use an HTTPS endpoint and keep allow_insecure_tls as the default value (false) in the OPA configuration.
4. Fix OPA version to latest in CI
Using version: latest in setup-opa@v2 results in reduced reproducibility because CI results may vary whenever the OPA version changes. For production pipelines, it is recommended to specify a specific version, such as version: "0.68.0".
In Conclusion
Combining an OPA bundle server with GitOps enables a system to review, verify, and deploy policies just like code. According to an OPA adoption case study released by the CNCF, a team that previously manually deployed policies to dozens of clusters reported significantly reducing policy deployment times and configuration inconsistency incidents after switching to a bundle server + GitOps pattern. There is no need to implement a complex OPA configuration from the start; it is much more realistic to begin with small units and scale up gradually.
3 Steps to Start Right Now:
- Creating and Testing Rego Policies: You can create a simple policy in Rego Playground and run unit tests locally using the
opa test ./policies/ ./tests/ -vcommand. - Configuring a GitHub Actions Pipeline: Refer to
deploy-bundle.ymlin the example above to configure a basic pipeline consisting of three steps,opa test → opa build → s3 cp.setup-opaUsing GitHub Actions simplifies the OPA installation process. - Deploy OPA to Kubernetes and Connect Bundle: You can deploy OPA as a Standalone Deployment and apply S3 bundle server polling settings by referring to the ConfigMap example above. If you get stuck, you can refer to the step-by-step Quickstart in the Official OPA Kubernetes Deployment Guide.
Next Post: Covers advanced patterns for configuring OPA Gatekeeper's ConstraintTemplate with practical security policies (image signature verification, namespace label enforcement, and privileged container blocking) and integrating with Conftest.
Reference Materials
- OPA Official Documentation - Bundles | openpolicyagent.org
- OPA Official Documentation - Deploying OPA on Kubernetes | openpolicyagent.org
- OPA 공식 문서 - Using OPA in CI/CD Pipelines | openpolicyagent.org
- Rego Playground | play.openpolicyagent.org
- CNCF 블로그 - Open Policy Agent: Best Practices for a Secure Deployment (2025) | cncf.io
- OPAL Official Documentation - Helm Chart for Kubernetes | docs.opal.ac
- ORAS - Bundle, test and deploy Gatekeeper policies as OCI image | oras.land
- GitOps Security: Enforcing Policy as Code in Flux and ArgoCD | policyascode.dev
- Conftest Official Documentation | conftest.dev
- GitHub - open-policy-agent/awesome-opa | github.com