Infra Stack Review

Preview Environments for Pull Requests on Kubernetes

Each pull request gets its own isolated environment.

· 10 min read · Updated
Cover illustration for “Preview Environments for Pull Requests on Kubernetes”
CI/CD Workflows · August 3, 2026 · 10 min read · 2,335 words

Shared staging environments do not merely slow teams down. They work against the basic mechanics of how pull requests are supposed to flow through a modern codebase.

The core failure is simple to describe: several pull requests deploy into the same staging namespace at the same time. When a test fails, nobody can say which change caused it. Engineers end up doing forensic debugging instead of code review, tracing a broken build back through three or four unrelated commits that happened to land in the same window. Call it feature fratricide: changes that have nothing to do with each other end up fighting over the same environment, and the losers are whoever's code gets blamed for someone else's bug.

Staging also drifts. The environment keeps running, but what it tells engineers gets less reliable with each passing week.

The most visible symptom is the queue. Trunk-based development depends on constant, fast merges to a shared main branch. Hyperscaler Review put it directly: shared staging breaks that model, engineers queue for access, unrelated features collide in the same namespace, and one bad deploy blocks the whole team from testing anything.

Preview environments remove the collision by removing the sharing. Instead of one staging environment serving every open pull request, each pull request gets its own environment, provisioned the moment the PR opens and destroyed the moment it closes. No shared state carries between PRs, so no PR can break another PR's tests. The environment's lifetime is tied to the PR's lifetime, nothing more and nothing less. That single design choice is what the rest of this piece builds from.

The four properties a preview environment must have to be worth building

A preview environment earns a team's trust or it doesn't, and that trust comes down to four properties. If a team skips any one of these properties, the system either costs more than it should or fails in a way that sends engineers back to the old habits staging encouraged.

The first property is that the environment stays ephemeral. Its lifecycle ties strictly to the pull request: it comes up when the PR opens and goes away when the PR closes, with nobody needing to remember to tear it down by hand. Skip this property and environments pile up quietly in the background, each one still billing against the cloud account long after the PR that created it is gone.

The second property is fidelity to production. A preview environment that runs against mocks and stubs only moves the "it works on my machine" problem into a different machine. It needs to run the real services against data that looks like the data production actually handles, or the test run inside it proves very little.

The third property is isolation. One PR's tests cannot touch another PR's state. That isolation can live at different layers, the namespace, the virtual cluster, or the individual request, but the goal is identical across all three: nothing one engineer does in their preview should be visible to anyone working in a different one.

The fourth property is full automation. The system responds to GitHub events on its own, no human clicking a button, no ticket filed with an infrastructure team, no manual request sitting in a queue. The moment a person has to intervene to spin up or tear down an environment, the system has quietly turned back into a version of the staging queue it was supposed to replace.

Hyperscaler Review names five technical problems that any implementation has to solve to deliver on these four properties, regardless of which architecture it picks: the isolation boundary, image build and deploy, traffic routing to a unique URL, stateful service isolation, and cleanup. Every approach in the next section solves these five problems differently, and those differences are what separate a good fit from an expensive mistake.

Four approaches to preview environments on Kubernetes, and what each costs the team

Four approaches dominate how teams build preview environments on Kubernetes. Each one loses on at least one axis. Each trades isolation strength against infrastructure cost and the amount of platform engineering work it demands, and the right pick depends on how many services a team runs, how many PRs are open at once, and what kind of changes those PRs tend to make.

Namespace-per-PR is the simplest to picture. CI creates a namespace named something like preview-pr-142, deploys the full application stack into it with Helm or Kustomize, and a wildcard DNS record routes traffic in. Closing the PR deletes the namespace, and that deletion cascades through every resource inside it. Cost is what catches teams out. That math turns punishing fast for teams with large service graphs. This approach fits small-to-medium teams with a moderate number of services and a manageable PR volume best.

An ArgoCD ApplicationSet with a pull request generator takes a GitOps-native route to the same problem. Close the PR and ArgoCD deletes the Application along with everything it owns, automatically. This is the most auditable of the four options, and it fits naturally into a team that already runs GitOps. The trade-off is maintenance. Of the four approaches, this one carries the highest ongoing maintenance load, and scaling and performance are the top challenges teams report once PR volume gets real. It fits teams with existing ArgoCD investment and the platform engineering capacity to keep the tooling running well.

Virtual clusters, using a tool like vCluster, give each pull request its own virtual Kubernetes API surface inside one shared physical cluster. That's a stronger isolation boundary than a plain namespace provides, and it matters when a PR touches operators, custom resource definitions, or other cluster-scoped configuration that would otherwise step on other tenants sharing the cluster. The cost here lives in control-plane overhead, and it adds up. Hyperscaler Review cites a load test running thousands of virtual clusters on STACKIT infrastructure that landed between €20,000 and €40,000, a useful ceiling to know before committing to this path at scale. Virtual clusters fit teams whose PRs regularly touch operators or CRDs. For changes at the application level alone, it's rarely the first choice.

Request-level isolation inverts the whole cost model. Instead of standing up a full copy of the stack for every PR, it deploys only the services the PR actually changed, and routes everything else to shared, real dependencies using a routing key carried in the request header. Previews come up in seconds, because there's far less to build and deploy. Cost tracks the number of services a PR touches rather than the total number of services in the system, so the math gets better as the microservice graph grows, the opposite of what happens with namespace-per-PR. Bitso is the documented case here: its homegrown preview system got too expensive and too unreliable to support 250-plus engineers working across more than 200 microservices. Switching to Signadot's request-level model gave every change a lighter preview to work with. This approach fits teams running a large service graph or a high volume of concurrent PRs, where duplicating the full stack for every single one is simply too expensive to sustain.

Laid out as a decision: small-to-medium teams with a moderate service count should look at namespace-per-PR first. Teams already running ArgoCD, with the engineering capacity to maintain it, find the ApplicationSet pattern easiest to audit. Teams whose changes regularly touch operators or CRDs need the stronger isolation virtual clusters provide. Teams with a large service graph or heavy concurrent PR volume get the better cost curve from request-level isolation.

Diagram: Four Approaches to Preview Environments: Isolation vs. Cost vs. Effort. Visualizes: Visualize a ranked or quadrant-style comparison of the four Kubernetes preview environment approaches along two axes: isolation strength and infrastructure…

How the namespace-per-PR pattern works mechanically, step by step

Namespace-per-PR is the approach most teams build themselves, since it needs no extra platform beyond Kubernetes and a CI pipeline. The pattern runs as a repeatable loop with five steps: build, namespace, route, watch, destroy. Nearly every production failure in a self-built system traces back to one of these five steps getting cut short.

Step one is building and pushing the image. The unique tag lets several PRs' images sit in the same registry without colliding.

Step two is creating and labeling the namespace. The pipeline creates a namespace named preview-pr-{PR_NUMBER} and labels it at creation time, typically with something like preview=true and the PR number itself. Those labels are the anchor that automated cleanup will search for later; skipping this step breaks the cleanup process before it runs.

Step three is deploying the stack. Deployment, Service, and Ingress manifests get applied into the new namespace, with environment variables templated to the PR number so each preview points at its own version of the image. A minimal Deployment manifest for a preview looks roughly like this in shape:

apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: preview-pr-123
spec:
replicas: 1

        - name: app

Step five is posting the URL and cleaning up. The CI job comments the live preview URL directly on the pull request, so reviewers have one click to the running environment. A separate cleanup workflow listens for the pull_request: closed event and runs kubectl delete namespace preview-pr-{PR_NUMBER}. Deleting the namespace cascades through every resource inside it, so one command tears down the Deployment, Service, and Ingress together.

Young Gyu Kim's walkthrough of this pattern using GitHub Actions and ArgoCD covers the complete version of this loop: GitHub Actions handles the CI side, building the ephemeral image, ArgoCD's ApplicationSet creates and destroys the environment, and the Gateway API exposes each preview on its own URL.

One detail matters enough to flag on its own: for public repositories, the CI workflow must never expose a production-capable KUBECONFIG to pull request code from an untrusted contributor.

Where cleanup, data isolation, and dependency management break

Getting a namespace-per-PR prototype running takes an afternoon. Getting it to a state the team actually trusts takes considerably longer, because the hard part was never the deployment step. The hard part sits at the edges: what happens when cleanup doesn't fire, what happens to test data, and what happens when one service depends on three others.

Cleanup is the corner most self-built systems cut first. An orphaned namespace doesn't send up a flare. A ResourceQuota applied to each namespace adds a second layer of protection. It caps the preview at a fixed ceiling, so a misbehaving environment appears as a visibly Pending pod rather than quietly starving production workloads of capacity on the same cluster.

Data isolation is the genuinely difficult part of the whole system. A shared staging database running behind every preview reintroduces the exact collision problem preview environments were supposed to fix: one migration can break every other team's preview at the same moment. Giving each PR its own database solves isolation but means someone has to run migrations and load realistic seed data into every new database before it's usable for testing. Copy-on-write database branching, which Neon provides for Postgres, sidesteps most of that cost: each preview gets its own branch of the database state, at close to zero marginal cost, which matters a great deal when most previews are only active for a few hours a week.

Dependency management is the third place systems break down, particularly for backend services. A frontend preview is usually simple to stand up on its own. A backend service is rarely so isolated: it depends on a queue, a cache, maybe several other microservices, and each of those dependencies has to either be provisioned fresh inside the preview or borrowed from a stable shared baseline, which is the kind of dependency complexity that a deployment platform like Porter, a bring-your-own-cloud PaaS that provisions and manages application environments inside a team's own AWS, GCP, or Azure account, can absorb at the infrastructure layer before any preview-environment tooling even enters the picture. Manatal's engineering team, working with One2N, solved this by building configuration overrides into ArgoCD: a developer working on Service A could point its dependency on Service B at either the shared staging instance or another PR's live preview, without redeploying anything. That flexibility didn't come for free. Someone had to build it.

Request-level isolation has a real structural edge over namespace-per-PR here. Its routing layer resolves dependencies automatically, since every unchanged service just routes to the shared, real version by default. With namespace-per-PR, that resolution logic has to be built by hand, service by service. Teams weighing whether to build this loop themselves should treat it honestly as a two-week platform engineering project at minimum, not an afternoon's work, and decide up front whether that capacity is actually available before committing to the build-it-yourself path.

How cost compounds as preview count grows

Preview environment infrastructure cost is not a flat line item. It grows along three dimensions at once: how many services the stack runs, how many PRs are open at a given time, and how long each preview sits idle before anyone looks at it. The controls that keep that cost in check need to be designed into the system from day one. Retrofitting them after spend has already gotten out of hand is a much harder conversation.

The cost curve for namespace-per-PR follows a predictable shape. Past that point, a dedicated node pool reserved for previews becomes the right move, since it keeps ephemeral workloads from ever sharing fate with production ones, if a preview environment misbehaves and consumes more resources than expected, it shouldn't be able to take a production pod down with it.

Idle time is the single largest cost most teams fail to account for. Namespace-per-PR duplicates the full stack for every one of those idle hours. That's precisely the gap that copy-on-write database branching and request-level isolation both close from different directions: one by making idle database state nearly free to hold, the other by never duplicating the untouched parts of the stack in the first place. The controls to build in from the start are the TTL on namespaces, the ResourceQuota per namespace, and a clear-eyed decision about node pool separation before PR volume forces the decision during an incident instead of ahead of one.

Sources

  1. One2N
  2. How to Get Per-PR Preview Environments on Kubernetes Without DevOps · Hyperscaler Review
  3. Pull Request Generator - Argo CD - Declarative GitOps CD for Kubernetes
Filed underCI/CD Workflows

More in CI/CD Workflows