ускорить обратную связь по PR без очереди на staging

Эфемерные namespace Kubernetes для preview pull request: автоматизация, изоляция и удаление

11 минут

Общий staging превращается в очередь и дрейф конфигурации. В материале — один namespace на pull request через Helm и GitHub Actions, квоты ресурсов, маршрутизация preview-трафика и удаление окружения при закрытии PR.

Почему общий staging становится узким местом поставки

Команды с одним или двумя долгоживущими staging-кластерами рано или поздно упираются в одни и те же проблемы: разработчики ждут свободное окружение, конфигурация расходится с production, а дефекты воспроизводятся нестабильно. Локальные тесты помогают на уровне unit-слоя, но не проверяют ingress, общие зависимости и кластерные политики. Масштабировать статические окружения клонированием целых кластеров дорого и тяжело в эксплуатации. Эфемерные preview-окружения меняют постоянство на изоляцию: каждый pull request получает свой срез общего кластера, а ресурсы исчезают после merge или закрытия PR.

Эфемерные namespace: жизненный цикл, изоляция и выбор инструментов

Эфемерное окружение — короткоживущий namespace Kubernetes, создаваемый под одну задачу изменения. CI собирает неизменяемые образы с тегом commit SHA, разворачивает их в namespace, запускает smoke- или интеграционные проверки и отдаёт preview URL ревьюерам. Удаление срабатывает на событии pull_request closed и дублируется janitor-задачами для осиротевших namespace. Есть два распространённых пути. CI-driven деплой через Helm или kubectl — самый быстрый старт, он совпадает с workflow ниже. GitOps-контроллеры вроде Argo CD или Flux могут управлять теми же namespace через Application или Kustomization на PR с automated prune — это удобно, если production уже синхронизируется из Git и preview должны следовать тому же контракту.

Эталонный workflow: GitHub Actions, Helm и один namespace на PR

Держите стабильное имя namespace на pull request (например pr-42), а не вшивайте хэш коммита в имя. Тогда helm upgrade --install на каждом synchronize обновляет окружение без осиротевших namespace. Аутентифицируйте workflow в целевой кластер, помечайте namespace метками для последующей очистки, применяйте манифесты квот до установки chart и публикуйте preview URL в PR. Job teardown удаляет namespace по точному имени при закрытии PR — kubectl не принимает wildcard в имени namespace, поэтому janitor по меткам остаётся резервным путём при сбоях CI.

GitHub Actions · deploy и teardown эфемерного 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

Ограничения: ResourceQuota, LimitRange и очистка осиротевших namespace

Preview-кластеры по определению мультитенантны. Без guardrails один ошибочный chart может запланировать неограниченное число pod и вытеснить соседние workload. ResourceQuota ограничивает суммарный CPU, память и число pod в namespace. LimitRange задаёт дефолты для контейнеров и не даёт запросить больше, чем разрешает квота. Если CI не удалил окружение, запускайте cron или политику Kyverno, которая удаляет namespace с меткой pr-number старше заданного TTL (часто начинают с семи дней).

YAML · ResourceQuota и LimitRange для preview namespace
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

Операционные практики для платформенных команд

Маршрутизируйте preview-трафик через wildcard DNS и ingress-контроллер (NGINX, Traefik или gateway service mesh), чтобы pr-N.preview.example.com указывал на Service нужного namespace без ручных DNS-заявок на каждый PR. Изолируйте данные: предпочитайте схему БД на PR, эфемерный Postgres или Testcontainers в CI вместо одной изменяемой staging-базы с перекрёстными конфликтами тестов. Повторяйте production security gates — сканирование образов, NetworkPolicy и admission-проверки — чтобы preview не стали неконтролируемым каналом для уязвимых образов. Ограничьте RBAC так, чтобы CI service account мог создавать namespace только в разрешённых preview-кластерах. Отслеживайте стоимость preview-кластера по меткам namespace и включайте выводы в FinOps-процессы, чтобы скорость разработки не раздувала облачный счёт незаметно.

Эфемерные preview логично встраиваются в единый слой деплоя — подход разобран в гайде по internal developer platform.

Когда preview надолго остаются в общем кластере, namespace-автоматизацию стоит сочетать с декларативной синхронизацией из материала про GitOps с Argo CD и Flux.