ship faster PR feedback without shared staging contention
Ephemeral Kubernetes namespaces for pull request previews: automate, isolate, and tear down
11 min read
Shared staging clusters turn into queues and config drift. This guide shows how to provision one namespace per pull request with Helm and GitHub Actions, enforce quotas, route preview traffic, and delete resources when the PR closes.
Why shared staging environments become delivery bottlenecks
Teams that rely on one or two long-lived staging clusters eventually hit the same friction: developers queue for an environment, configurations drift from production, and failures reproduce inconsistently. Local testing helps for unit work, but it cannot validate ingress rules, shared dependencies, or cluster-level policies. Scaling static environments by cloning entire clusters is expensive and operationally heavy. Ephemeral preview environments trade permanence for isolation: each pull request gets its own slice of a shared cluster, and resources disappear when the change merges or closes.
Ephemeral namespaces: lifecycle, isolation, and toolchain choices
An ephemeral environment is a short-lived Kubernetes namespace provisioned for a single change request. CI builds immutable container images tagged with the commit SHA, deploys them into that namespace, runs smoke or integration checks, and exposes a preview URL for reviewers. Tear-down runs on pull_request closed events and through backup janitor jobs for orphaned namespaces. Two common implementation paths exist. CI-driven Helm or kubectl deploys are the fastest starting point and match the workflow below. GitOps controllers such as Argo CD or Flux can manage the same namespaces by syncing an Application or Kustomization per PR with automated prune enabled—useful when platform teams already reconcile production from Git and want previews to follow the same contract.
Reference workflow: GitHub Actions, Helm, and one namespace per PR
Keep one stable namespace name per pull request (for example pr-42) instead of embedding the commit hash in the namespace. Helm upgrade --install then rolls forward on every synchronize event without leaving orphaned namespaces behind. Authenticate the workflow to the target cluster, label the namespace for later cleanup queries, apply quota manifests before the chart install, and post the preview URL back to the PR. The teardown job deletes the namespace by exact name when the PR closes—kubectl does not accept wildcard namespace names, so label-based janitor scripts are the backup path for missed CI runs.
name: Ephemeral Environment Deployment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
deploy-ephemeral:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Authenticate to Kubernetes
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Set namespace name
id: namespace
run: echo "name=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Deploy to ephemeral namespace
run: |
kubectl create namespace ${{ steps.namespace.outputs.name }} \
--dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace ${{ steps.namespace.outputs.name }} \
app.kubernetes.io/managed-by=ci \
pr-number=${{ github.event.number }} \
--overwrite
kubectl apply -f ./manifests/ephemeral-guardrails.yaml \
-n ${{ steps.namespace.outputs.name }}
helm upgrade --install app-${{ github.event.number }} ./chart \
--namespace ${{ steps.namespace.outputs.name }} \
--set image.tag=${{ github.sha }} \
--set ingress.host=pr-${{ github.event.number }}.preview.example.com \
--wait --timeout 5m
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Ephemeral environment ready: https://pr-${context.issue.number}.preview.example.com`
})
teardown-ephemeral:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Authenticate to Kubernetes
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Delete ephemeral namespace
run: |
kubectl delete namespace pr-${{ github.event.number }} \
--wait=false --ignore-not-found=trueGuardrails: ResourceQuota, LimitRange, and orphaned namespace cleanup
Preview clusters are multi-tenant by design. Without guardrails, a single misconfigured chart can schedule unbounded pods and starve production-adjacent workloads. Apply ResourceQuota to cap aggregate CPU, memory, and pod count per namespace. Pair it with LimitRange so individual containers inherit sensible defaults and cannot request more than the quota allows. For environments CI failed to delete, run a scheduled job or a Kyverno cleanup policy that removes namespaces labeled pr-number older than a defined TTL (seven days is a common starting point).
apiVersion: v1
kind: ResourceQuota
metadata:
name: ephemeral-quota
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: ephemeral-limits
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "2"
memory: 2GiOperational practices platform teams should enforce
Route preview traffic with wildcard DNS and an ingress controller (NGINX, Traefik, or a service mesh gateway) so pr-N.preview.example.com maps to the correct namespace Service without manual DNS tickets per change. Isolate data dependencies: prefer per-PR database schemas, ephemeral Postgres instances, or Testcontainers in CI over sharing one mutable staging database that creates cross-PR test interference. Mirror production security gates—image scanning, network policies, and admission checks—so previews do not become an ungoverned path for vulnerable images. Scope RBAC so CI service accounts can create namespaces only in approved preview clusters. Track preview cluster cost with namespace labels and feed findings into FinOps routines so engineering speed does not silently inflate the cloud bill.
Ephemeral previews fit naturally into a broader deployment abstraction; see how teams standardize the experience in our internal developer platform guide.
When previews graduate to shared clusters long term, pair namespace automation with declarative reconciliation from GitOps workflows with Argo CD and Flux.
