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.

GitHub Actions · deploy and teardown ephemeral namespace
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=true

Guardrails: 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).

YAML · ResourceQuota and LimitRange for preview namespaces
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: 2Gi

Operational 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.