Skip to main content
Cloud Migration9 min read

Policy-as-Code Beyond Kubernetes: Turning Gatekeeper Controls into Full-Stack Cloud Governance (and Avoiding Configuration Debt)

Cloud migrations can eliminate legacy code debt—only to replace it with configuration debt: inconsistent IAM, one-off network rules, and environment drift. By evolving from Kubernetes Gatekeeper checks to full-stack governance with Open Policy Agent (OPA), teams can standardize controls across infrastructure and delivery pipelines while keeping modernization changes auditable and repeatable.

Cloud modernization has a sneaky failure mode: you migrate workloads, refactor services, and finally retire brittle legacy systems—then you wake up months later drowning in configuration debt. IAM policies proliferate, network rules diverge by environment, and what “passed security review” in staging quietly drifts in production.

That’s why policy-as-code (PaC) can’t stop at Kubernetes admission control. The real payoff comes when you take the patterns you built in the Gatekeeper era and extend them into full-stack cloud governance—so modernization doesn’t trade code debt for a bigger, harder-to-debug mess.

Context: how modernization turns code debt into configuration debt

Policy-as-Code Beyond Kubernetes: Turning Gatekeeper Controls into Full-Stack Cloud Governance (and Avoiding Configuration Debt)
Policy-as-Code Beyond Kubernetes: Turning Gatekeeper Controls into Full-Stack Cloud Governance (and Avoiding Configuration Debt)

Modernization programs rarely fail because teams can’t write code. They fail because teams can’t keep decisions consistent across a growing surface area:

  • Identity and access: ad-hoc IAM roles, wildcard permissions “temporarily” added for migrations, and inconsistent tagging.
  • Networking and exposure: security groups, firewall rules, load balancers, and public endpoints created under time pressure.
  • Data controls: encryption, retention, backups, and cross-account access varying by service and team.
  • Environment drift: staging and prod become “mostly similar,” except for dozens of undocumented exceptions.

These aren’t just security problems—they’re maintenance problems. Every future upgrade (Kubernetes version bumps, library updates, cloud service changes, incident response playbooks) becomes more expensive because your baseline is inconsistent.

Policy-as-code is one of the most practical tools to keep modernization sustainable: it lets you encode guardrails as versioned artifacts, run them automatically, and audit decisions later.

From Gatekeeper to governance: the Pulumi framing

Many organizations first met Open Policy Agent (OPA) through Kubernetes, typically via Gatekeeper—OPA’s policy controller for Kubernetes admission control. That’s a useful starting point, but it’s also a narrow one: it governs what gets created in the cluster, not the cloud resources and delivery steps that surround it.

Pulumi describes a clear progression from Kubernetes Gatekeeper usage to what it calls “full-stack governance” with OPA (Pulumi Blog: “From Kubernetes Gatekeeper to Full-Stack Governance with OPA”). The core framing is straightforward:

  • Start with OPA policies enforcing rules at the Kubernetes layer.
  • Expand the same OPA-based control model beyond a single layer (Kubernetes) to cover cloud infrastructure, identity, networking, and delivery workflows.
  • Treat governance as a practical evolution as organizations scale—because the number of teams, environments, and policy needs increases faster than manual review can handle.

That progression matches what Vibgrate sees during maintenance and modernization work: teams naturally start with the most visible layer (Kubernetes), then realize the real risk is the gaps between layers.

Why Gatekeeper alone doesn’t prevent configuration debt

Gatekeeper is excellent at preventing certain classes of mistakes:

  • Enforcing labels/annotations
  • Blocking privileged containers
  • Restricting hostPath mounts
  • Requiring resource limits

But modernization isn’t only a “cluster hygiene” problem. Configuration debt often accumulates outside the cluster:

  • A service is compliant in Kubernetes but deployed behind an overly permissive cloud load balancer.
  • A workload uses least-privilege in-cluster RBAC, but its cloud role has broad access to storage and secrets.
  • Production data ends up in a bucket without required encryption or lifecycle policy.

If your governance model only inspects Kubernetes manifests at admission time, you’ll still end up with inconsistent IAM, networking, and data controls—just with slightly prettier YAML.

What “full-stack governance” looks like in practice

Pulumi’s article positions full-stack governance as applying OPA policies across the broader infrastructure lifecycle—not just Kubernetes. In practical terms, it means your policies run in more places and against more kinds of inputs.

Policy everywhere decisions happen

A full-stack approach treats governance as something you can apply:

  • During infrastructure provisioning (e.g., IaC previews and updates)
  • During application delivery (build/release gates)
  • During configuration changes (environment config, secrets references, tagging)
  • Continuously (detecting drift and out-of-band changes)

The goal is to catch issues when they’re cheap to fix—before they become embedded in production or copied into the next environment.

One policy language, multiple layers

OPA (via Rego) is valuable because it decouples policy definition from policy enforcement points. Organizations can standardize on a single approach to express rules such as:

  • “No public buckets”
  • “All resources must be tagged with owner, environment, and cost-center”
  • “Production workloads must use approved instance types or node pools”
  • “Only these teams can create internet-facing load balancers”

When those rules exist as versioned code, you can review them like any other change: pull requests, peer review, tests, and release notes.

Governance as an evolution (not a bureaucracy)

Pulumi’s framing is important: governance isn’t positioned as a heavyweight compliance program; it’s a practical evolution for organizations that are scaling infrastructure and policy needs.

That matters for CTOs and platform leaders because modernization is already disruptive. The governance strategy has to accelerate teams, not slow them down.

Modernization lens: preventing rework and late-stage security findings

Configuration debt is expensive primarily because it creates rework:

  • Security flags appear late (after deployment), forcing emergency retrofits.
  • Teams implement “temporary” exceptions that become permanent.
  • One business unit sets patterns that another copies—multiplying mistakes.

Full-stack OPA governance helps by shifting the system from “detect and shame” to “prevent and guide.”

Replace tribal knowledge with repeatable guardrails

Modernization often involves multiple teams migrating at different speeds. Without shared guardrails, each team invents its own approach to IAM, network exposure, naming, and tagging.

Policy-as-code creates a single source of truth:

  • Rules are explicit, discoverable, and versioned.
  • Exceptions can be managed intentionally (with justification and expiry).
  • New teams can onboard faster because the “right way” is enforced automatically.

Make upgrades safer by standardizing invariants

Maintenance and upgrades are easier when invariants hold:

  • All production resources have the same tagging and logging controls.
  • Encryption requirements don’t vary by team.
  • Network boundaries are consistent.

That consistency reduces the blast radius of change. When you later upgrade Kubernetes versions, rotate credentials, or adopt new cloud services, you’re not untangling a hundred bespoke configurations first.

Practical implications for engineering teams

Full-stack governance can sound abstract until you map it to day-to-day workflows. Here are concrete ways teams can implement it during modernization.

1) Define a “minimum viable policy set” for migration waves

Don’t start by encoding every possible compliance rule. Start with the few controls that prevent the most common configuration debt:

  • Identity: no wildcard admin permissions; enforce role boundaries; require team ownership.
  • Network: block public exposure by default; restrict inbound ports; require approved ingress patterns.
  • Data: encryption at rest required; backups required for stateful services.
  • Hygiene: required tags/labels; naming conventions; logging enabled.

Then expand coverage as you learn where drift appears.

2) Enforce policies at the earliest meaningful point

A policy that fails after resources are created is still useful—but it’s more expensive.

Recommended progression:

  • PR time: run policy checks against IaC previews (fast feedback for developers).
  • Pipeline gates: prevent deployments that violate critical rules.
  • Post-deploy monitoring: detect drift and out-of-band changes.

This aligns with Pulumi’s emphasis on governance as organizations scale: enforcement has to move closer to where changes originate, because centralized review doesn’t scale.

3) Treat policy as a product (with versioning and tests)

If policies are code, they deserve software engineering rigor:

  • Version policies and publish release notes (“v1.8 blocks public S3 buckets by default”).
  • Write unit tests for policies (especially for exceptions and edge cases).
  • Use staged rollouts: warn-only mode → enforce in non-prod → enforce in prod.

This approach reduces friction: teams trust the system when changes are predictable and communicated.

4) Build an exception workflow that doesn’t become a loophole

Modernization sometimes needs exceptions (e.g., a legacy integration requires a public endpoint temporarily). The anti-pattern is informal exceptions that live forever.

Instead:

  • Require a ticket/justification reference for exceptions.
  • Add time limits (expiry dates) and re-approval.
  • Track exceptions as inventory you can burn down over time.

This turns “temporary” from a lie into a managed lifecycle.

5) Use governance to standardize platform golden paths

Policies are most effective when paired with paved roads:

  • Templates/modules for common infrastructure patterns
  • Approved reference architectures
  • Reusable CI/CD workflows

Policies keep teams from leaving the road; golden paths make it easy not to.

Actionable takeaways (what to do next)

If you’re modernizing and already using Gatekeeper, here’s a pragmatic next step plan:

  1. Inventory your current Gatekeeper constraints and classify them: security, cost, reliability, hygiene.
  2. Identify the top three sources of configuration debt outside Kubernetes (typically IAM, networking, and data storage).
  3. Adopt OPA-based policies at the infrastructure layer—the same governance mindset, applied to cloud resources and delivery pipelines.
  4. Start with “deny the worst, guide the rest”: block critical misconfigurations; use warnings for non-critical standards while teams adjust.
  5. Operationalize policy changes with versioning, tests, and a rollout process.

For a deeper look at the progression and rationale, see Pulumi’s discussion of moving “From Kubernetes Gatekeeper to Full-Stack Governance with OPA,” which explicitly frames this shift as scaling from single-layer enforcement to full-stack governance as infrastructure and policy needs grow (Pulumi Blog: https://www.pulumi.com/blog/kubernetes-gatekeeper-full-stack-governance-opa/).

Conclusion: governance as an accelerator for sustainable modernization

Modernization succeeds when the new platform is easier to operate than the old one. If your migration replaces code debt with configuration debt, you’ll feel the pain later—during upgrades, audits, incidents, and the next wave of change.

Taking the Gatekeeper-era lessons you’ve already learned and extending OPA-based controls into full-stack cloud governance is a practical evolution, not a compliance detour. It standardizes the rules that matter, reduces late-stage security rework, and keeps platform changes auditable as your systems evolve—exactly the conditions you need to modernize with confidence.

Vibgrate CLI

See a real scan run

A replay of the actual CLI running against our test repositories — live progress, real findings, a genuine DriftScore. Nothing executes in your browser.

Replay
demo@vibgrate — bash
npx @vibgrate/cli scan
 
╭──────────────────────────────────────────╮
Vibgrate Drift Report
╰──────────────────────────────────────────╯
 
── node-turborepo (node) .
Runtime: >=18.0.0 (6 majors behind)
Frameworks:
Turbo: 1.13.4 → 2.10.11 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 1 1-behind 3 2+ behind 1 unknown
 
── @repo/admin (node) apps/admin
Frameworks:
TanStack Query: 5.101.4 → 5.101.4 (current)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vite: 5.4.21 → 8.2.1 (3 behind)
Dependencies:
3 current 9 1-behind 3 2+ behind 4 unknown
 
── @repo/api (node) apps/api
Frameworks:
Express: 4.22.2 → 5.2.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
7 current 5 1-behind 3 2+ behind 4 unknown
 
── @repo/web (node) apps/web
Frameworks:
Next.js: 14.2.35 → 16.3.1 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 6 1-behind 3 2+ behind 5 unknown
 
── @repo/config (node) packages/config
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 2 1-behind 5 2+ behind 0 unknown
 
── @repo/database (node) packages/database
Frameworks:
Prisma: 5.22.0 → 7.9.1 (2 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 0 1-behind 3 2+ behind 1 unknown
 
── @repo/types (node) packages/types
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
0 current 0 1-behind 1 2+ behind 1 unknown
 
── @repo/ui (node) packages/ui
Frameworks:
React: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
Dependencies:
1 current 4 1-behind 1 2+ behind 1 unknown
 
── @repo/utils (node) packages/utils
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
0 current 1 1-behind 2 2+ behind 1 unknown
 
Tech Stack
Frontend: React, React DOM
Meta-frameworks: Next.js
Bundlers: tsx, Turbo, Vite
CSS / UI: Autoprefixer, PostCSS, Tailwind CSS
Backend: Express
ORM / Database: Prisma, Prisma Client
Testing: Vitest
Lint & Format: ESLint, ESLint Prettier, ESLint React, Prettier, typescript-eslint
 
Services & Integrations
Auth: JWT 9.0.3
Databases: Prisma 5.22.0
 
TypeScript
v5.3.3 · strict ✔ · MIXED · target: ES2022
 
Build & Deploy
Package Managers: pnpm
Monorepo: npm-workspaces, pnpm-workspaces, turbo
 
Product Purpose Signals
Frameworks: react, nextjs
Evidence: 177
Top Signals:
- [heading] Dashboard (apps/admin/src/pages/Dashboard.tsx)
- [title] Revenue Overview (apps/admin/src/pages/Dashboard.tsx)
- [copy] workspace:* (packages/ui/package.json)
- [copy] ./dist (packages/ui/tsconfig.json)
- [copy] ./src/index.ts (packages/ui/package.json)
- [copy] @repo/config/tsconfig-base.json (packages/ui/tsconfig.json)
- [copy] @repo/ui (packages/ui/package.json)
- [copy] #3b82f6 (apps/admin/src/pages/Dashboard.tsx)
Unknowns:
- No pricing or billing evidence found.
- No integrations/connectors evidence found.
- No route structure evidence found.
 
Security Posture
Lockfile ✖ · .env ✔ · node_modules ✔
 
Platform
Native modules: turbo
 
Code Quality
Files: 36 · Functions: 183 · Avg complexity: 2.62 · Avg length: 21.13 lines
Max nesting: 2 · Circular deps: 0 · Dead code: 0%
God files: apps/admin/src/pages/Products (448 lines)
 
Database Schema
postgresql · 8 models · 1 enum
Models: Address, CartItem, Category, Order, OrderItem (+3 more)
 
Findings (16 errors, 11 warnings)
Node.js runtime ">=18.0.0" reached end-of-life on 2025-04-30 (latest: 24.0.0).
vibgrate/runtime-eol in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in .
60% of dependencies are 2+ major versions behind in node-turborepo.
vibgrate/dependency-rot in .
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.2.0).
vibgrate/dependency-major-lag in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/admin
Vite is 3 major versions behind (current: 5.4.21, latest: 8.2.1).
vibgrate/framework-major-lag in apps/admin
vite is 3 major versions behind (spec: ^5.0.12, latest: 8.2.1).
vibgrate/dependency-major-lag in apps/admin
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/api
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in apps/api
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.2.0).
vibgrate/dependency-major-lag in apps/api
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in apps/api
Next.js is 2 major versions behind (current: 14.2.35, latest: 16.3.1).
vibgrate/framework-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/web
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.2.0).
vibgrate/dependency-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/config
56% of dependencies are 2+ major versions behind in @repo/config.
vibgrate/dependency-rot in packages/config
eslint-plugin-react-hooks is 3 major versions behind (spec: ^4.6.0, latest: 7.1.1).
vibgrate/dependency-major-lag in packages/config
Prisma is 2 major versions behind (current: 5.22.0, latest: 7.9.1).
vibgrate/framework-major-lag in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/database
75% of dependencies are 2+ major versions behind in @repo/database.
vibgrate/dependency-rot in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/types
100% of dependencies are 2+ major versions behind in @repo/types.
vibgrate/dependency-rot in packages/types
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/ui
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/utils
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in packages/utils
67% of dependencies are 2+ major versions behind in @repo/utils.
vibgrate/dependency-rot in packages/utils
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in packages/utils
 
╭──────────────────────────────────────────╮
Top Priority Actions
╰──────────────────────────────────────────╯
 
1. Upgrade EOL runtime in node-turborepo
End-of-life runtimes no longer receive security patches and block ecosystem upgrades.
./.
>=18.0.0 → 24.0.0 (6 majors behind)
Impact: −10 drift points (runtime & EOL)
 
2. Fix security posture: no lockfile found
Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.
./
Missing: package-lock.json, pnpm-lock.yaml, or yarn.lock
 
3. Upgrade Vite 5.4.21 → 8.2.1 in @repo/admin (+2 more)
3 major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.
./apps/admin
Vite: 5.4.21 → 8.2.1 (3 majors behind)
./apps/api
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
./packages/utils
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
Impact: −5–15 drift points
 
4. Reduce dependency rot in @repo/types (100% severely outdated)
1 of 1 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/types
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
5. Reduce dependency rot in @repo/database (75% severely outdated)
3 of 4 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/database
@prisma/client: 5.22.0 → 7.9.1 (2 majors behind)
prisma: 5.22.0 → 7.9.1 (2 majors behind)
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
╭──────────────────────────────────────────╮
Architecture Layers
╰──────────────────────────────────────────╯
 
Archetype: nextjs (80% confidence)
Files classified: 24 (11 unclassified)
Folders classified: 8
apps/admin/src presentation 100% 4 files
apps/admin/src/pages presentation 100% 2 files
apps/api/src/middleware middleware 100% 2 files
apps/api/src/routes routing 100% 2 files
apps/web/src/app presentation 100% 4 files
apps/web/src/app/products presentation 100% 2 files
apps/web/src/app/products/[id] presentation 100% 1 file
packages/ui/src presentation 100% 6 files
Unclassified source (sample): 11
 
presentation 15 files drift ████████████████████ 100 risk high
routing 4 files drift ████████████████████ 100 risk high
middleware 2 files drift ███████▍░░░░░░░░░░░░ 37 risk moderate
config 2 files drift ░░░░░░░░░░░░░░░░░░░░ 0 risk none
shared 1 file drift ████████████████████ 100 risk high
 
╭──────────────────────────────────────────╮
DriftScore Summary
╰──────────────────────────────────────────╯
 
DriftScore: 66/100
Risk Level: HIGH
Projects: 9
Classified: 8 nano · 1 micro · 0 small · 0 standard
Billable: 0.42 · 9 detected → 0.42 billable projects (micro-project pricing)
0.1 micro · 0.32 nano
These fractions add up across repositories, then round down to whole billable projects.
 
Score Breakdown
Runtime: ████████████████████ 100
Frameworks: █████████▏░░░░░░░░░░ 46
Dependencies: ██████▏░░░░░░░░░░░░░ 31
EOL Risk: ████████████████████ 100
 
Scanned at 2026-08-19T10:20:40.993Z · 5.9s · 286 files scanned · 56 workspace files · 27 dirs
Press Run to start.