Policy lifecycle
Versioning, rollout waves, exceptions with expiry.
In short. policy platforms focus on Policy lifecycle: Versioning, rollout waves, exceptions with expiry.
A city that wants a new speed limit does not switch on the cameras overnight. The rule gets drafted, debated, published, posted on warning signs for a month, and only then enforced with tickets — and years later, when the road changes, it gets repealed. A policy in infrastructure is the same kind of object: a rule a machine enforces, such as *every container image must come from our internal registry*. Writing the rule is the easy part. The dangerous part is everything around it — how it is reviewed, shipped to the engines that enforce it, switched on without breaking production, and eventually retired.
That surrounding process is the policy lifecycle: the managed path a rule takes from proposal to retirement — author, review, test, package, distribute, audit, enforce, monitor, retire. Because policy-as-code stores rules as files in Git and evaluates them with an engine such as OPA, the lifecycle is a software-delivery problem, and every discipline you already apply to application code — versioning, CI, staged rollout, telemetry — applies to policy too. Earlier lessons covered how an engine evaluates a decision and how to test Rego; this one covers what happens between git merge and a request being denied in production.
The failures a lifecycle prevents
Skipping the lifecycle fails in two opposite ways. Big-bang enforcement: a policy merges straight into deny mode. A new *require a team label* rule looks harmless in review, but 137 existing workloads lack the label, every redeploy is suddenly blocked, and the platform team spends the night hand-writing exemptions. Policy rot is the mirror image: rules sit in audit-forever mode, exemptions never expire, and nobody remembers why a rule exists — enforcement in name only. There is also a security dimension: the distribution channel is an attack path. Whoever can serve policy to your engines decides what is admitted to your clusters, so an unsigned bundle fetched over plain HTTP is roughly equivalent to handing out cluster-admin. Signing, versioning, and staged rollout are the mitigations — and they are lifecycle features, not engine features.
Packaging: bundles, revisions, signatures
OPA does not read Git. It consumes a bundle: a gzipped tarball containing .rego files, JSON data, and a .manifest that declares a *revision* (a version string you choose) and *roots* (the namespaces the bundle owns, which stops two bundles from silently overwriting each other's rules). CI builds the bundle, signs it with a private key, and pushes it to a bundle server — any static HTTP server or OCI registry works. Put both the semantic version and the Git SHA in the revision, because that string is what engines later report back, and it should answer *exactly which policy is running in cluster X right now* with a single field.
# Build a signed bundle from the policies/ directoryopa build -b policies/ -o bundle.tar.gz \-r "v1.4.0+3f9c2d1" \--signing-key /keys/bundle-private.pem# Verify what you just builtopa inspect bundle.tar.gz# -> MANIFEST:# -> +----------+----------------+# -> | FIELD | VALUE |# -> +----------+----------------+# -> | Revision | v1.4.0+3f9c2d1 |# -> | Roots | kubernetes |# -> +----------+----------------+# -> NAMESPACES:# -> +---------------------------+-------------------------------+# -> | NAMESPACE | FILE |# -> +---------------------------+-------------------------------+# -> | data.kubernetes.admission | /policies/admission/deny.rego |# -> +---------------------------+-------------------------------+
Distribution is pull-based. Each engine polls the server on a jittered interval, verifies the signature against a public key, and hot-swaps the new bundle atomically — no restarts. Two feedback channels close the loop: the status API reports which revision each engine has activated, and decision logs stream every allow/deny along with the input that produced it. Decision logs are both your audit trail and the evidence you use to promote a policy to its next stage.
services:bundle_registry:url: https://bundles.internal.example.comcredentials:bearer:token_path: /var/run/secrets/opa/token # never inline tokenskeys:bundle_signer:algorithm: RS256key: ${BUNDLE_PUBLIC_KEY} # public key only; private stays in CIbundles:admission:service: bundle_registryresource: bundles/admission/bundle.tar.gzpolling:min_delay_seconds: 30 # jittered poll windowmax_delay_seconds: 120signing:keyid: bundle_signer # reject unsigned or tampered bundlesstatus:service: bundle_registry # report active revision upstreamdecision_logs:service: bundle_registryreporting:min_delay_seconds: 5max_delay_seconds: 10
Staged enforcement: dryrun, warn, deny
An enforcement action is what the engine does when a rule is violated. Gatekeeper defines three: dryrun records the violation and does nothing else; warn admits the request but returns a warning the client sees (kubectl prints it); deny rejects it. The iron rule of the lifecycle: every new policy enters production in dryrun. Gatekeeper's audit controller also rescans objects that already exist in the cluster (every 60 seconds by default), so dry-run shows your true violation debt — including workloads created long before the policy existed.
# Stage 1: ship the constraint in dry-runkubectl apply -f require-team-label.yaml # spec.enforcementAction: dryrun# -> k8srequiredlabels.constraints.gatekeeper.sh/require-team-label created# The audit controller rescans existing objects (~60s); read the debtkubectl get k8srequiredlabels require-team-label \-o jsonpath='{.status.totalViolations}'# -> 137# Stage 2: remediation has driven the count to zero; surface warningskubectl patch k8srequiredlabels require-team-label \--type merge -p '{"spec":{"enforcementAction":"warn"}}'# -> k8srequiredlabels.constraints.gatekeeper.sh/require-team-label patched# Stage 3: soak period passed, violations still zero -> enforcekubectl patch k8srequiredlabels require-team-label \--type merge -p '{"spec":{"enforcementAction":"deny"}}'
Promotion between stages should be gated, and the gate should be automated: flip dryrun to warn only when every violation is either remediated or covered by a dated exemption; flip warn to deny only after totalViolations has held at zero through a soak period — a week of real deploys, not a quiet weekend. A CI job that reads the constraint status enforces this more reliably than a human who remembers to check.
deny the cluster looks healthy — until a node drain or an eviction recreates their pods and admission rejects them, turning routine maintenance into an outage. Gate promotion on the audit controller's count of *existing* violations, never on the absence of new denials.Retirement, exceptions, and what breaks at scale
The lifecycle does not end at deny. Exemptions accumulate: require every one to carry an owner and an expiry date in metadata, and fail CI when one is past due — otherwise exceptions quietly become permanent policy. Dead rules accumulate too: a rule that decision logs show has not fired in 90 days is a repeal candidate, and removal runs the rollout in reverse (deny to warn to delete) so nothing was depending on it silently. Know the trade-offs you have bought. Polling means minutes of lag between merge and enforcement — deliberate, but it surprises people expecting instant effect. The bundle server becomes a tier-0 dependency: engines keep the last good bundle when a fetch fails, but a freshly started node has nothing until its first successful pull. And one giant bundle serving hundreds of clusters costs activation time and memory on every engine — split bundles along team or domain boundaries with disjoint roots before that hurts.
The stages are universal; the machinery is not. Revisions and status reports are OPA's spelling, enforcementAction and totalViolations are Gatekeeper's, and Kyverno expresses the same ideas as validationFailureAction: Audit and PolicyException resources. The next lesson puts those two Kubernetes-native engines side by side — how each implements validation, mutation, and this lifecycle, and how to choose between them.