DevOps

Canary-деплой на Kubernetes с автоматическим откатом через CI/CD и метрики Prometheus

Ruslan Ismailov Опубликовано 12 мин чтения
C

Введение: что такое canary-деплой и зачем он нужен

Canary deployment (канареечный деплой) — стратегия выкатки новой версии приложения, при которой небольшой процент реального трафика направляется на новый релиз, пока основная нагрузка остаётся на стабильной версии. Название отсылает к практике шахтёров, которые брали канарейку в шахту: если птица погибала — значит, в воздухе газ. Аналогично, если новая версия сервиса деградирует на 5% трафика — она не попадёт к 100% пользователей.

Чем canary отличается от других стратегий деплоя:

  • Rolling update — постепенно заменяет поды старой версии на новые, трафик распределяется по мере замены. Нет возможности точно контролировать процент трафика на новую версию.
  • Blue-green — поддерживает две полные копии окружения, переключение происходит разом. Дорого по ресурсам, нет плавного перехода.
  • Canary — даёт точный контроль над долей трафика, позволяет валидировать метрики перед полным переходом и автоматически откатиться при проблемах.

Canary-деплой особенно актуален для Microservices-архитектур, где сервисы деплоятся независимо и цена ошибки на продакшене высока. В связке с Kubernetes, CI/CD-пайплайнами и Prometheus это превращается в полноценный автоматизированный процесс безопасного деплоя.

Архитектура canary-деплоя в Kubernetes

Стандартная архитектура включает три компонента: два Deployment-объекта (stable и canary), один Service и Ingress с весовой маршрутизацией.

Deployment: stable и canary

Запускаются два Deployment с разными метками (track: stable и track: canary), но одинаковым лейблом приложения. Service выбирает поды по общему лейблу, а весовое распределение трафика задаётся на уровне Ingress.

Весовая маршрутизация через NGINX Ingress

NGINX Ingress Controller поддерживает canary через аннотации. Отдельный Ingress-ресурс помечается как canary и получает вес (например, 10%). Gateway API (новый стандарт Kubernetes) предоставляет более выразительный способ через ресурс HTTPRoute с явным указанием весов бэкендов.

Пошаговая настройка canary через Kubernetes манифесты

Шаг 1: Stable Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-service-stable
  labels:
    app: go-service
    track: stable
spec:
  replicas: 4
  selector:
    matchLabels:
      app: go-service
      track: stable
  template:
    metadata:
      labels:
        app: go-service
        track: stable
    spec:
      containers:
      - name: go-service
        image: registry.example.com/go-service:v1.4.2
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"

Шаг 2: Canary Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-service-canary
  labels:
    app: go-service
    track: canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-service
      track: canary
  template:
    metadata:
      labels:
        app: go-service
        track: canary
    spec:
      containers:
      - name: go-service
        image: registry.example.com/go-service:v1.5.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"

Шаг 3: Service

apiVersion: v1
kind: Service
metadata:
  name: go-service
spec:
  selector:
    app: go-service
  ports:
  - port: 80
    targetPort: 8080

Service выбирает поды по лейблу app: go-service, то есть и stable, и canary поды. При 4 stable + 1 canary реплике нагрузка распределится примерно 80/20. Для точного контроля используем Ingress.

Шаг 4: Ingress с весовой маршрутизацией

# Основной Ingress (stable)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-service-stable
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-service-stable-svc
            port:
              number: 80
---
# Canary Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-service-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-service-canary-svc
            port:
              number: 80

Аннотация nginx.ingress.kubernetes.io/canary-weight: "10" направляет 10% трафика на canary-версию. Значение можно изменять динамически через kubectl annotate без пересоздания ресурса.

Интеграция с CI/CD-пайплайном: GitHub Actions

Ниже — пример GitHub Actions workflow, который реализует canary-деплой с поэтапным увеличением трафика и проверкой метрик на каждом шаге.

name: Canary Deploy

on:
  push:
    branches: [main]

env:
  IMAGE: registry.example.com/go-service
  NAMESPACE: production

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push image
        run: |
          docker build -t $IMAGE:${{ github.sha }} .
          docker push $IMAGE:${{ github.sha }}

  canary-deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure kubectl
        uses: azure/setup-kubectl@v3

      - name: Deploy canary (10%)
        run: |
          kubectl set image deployment/go-service-canary \
            go-service=$IMAGE:${{ github.sha }} \
            -n $NAMESPACE
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=10 \
            --overwrite -n $NAMESPACE
          kubectl rollout status deployment/go-service-canary -n $NAMESPACE

      - name: Wait and check metrics (10%)
        run: bash scripts/check_metrics.sh 10

      - name: Increase to 30%
        run: |
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=30 \
            --overwrite -n $NAMESPACE

      - name: Wait and check metrics (30%)
        run: bash scripts/check_metrics.sh 30

      - name: Full rollout (100%)
        run: |
          kubectl set image deployment/go-service-stable \
            go-service=$IMAGE:${{ github.sha }} \
            -n $NAMESPACE
          kubectl rollout status deployment/go-service-stable -n $NAMESPACE
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=0 \
            --overwrite -n $NAMESPACE
          kubectl scale deployment/go-service-canary --replicas=0 -n $NAMESPACE

Мониторинг метрик через Prometheus

Prometheus — ключевой инструмент для оценки качества canary-релиза. Настраиваем два ключевых критерия успешности: error rate и latency (p99).

Настройка ServiceMonitor для Prometheus Operator

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: go-service-canary
  namespace: production
spec:
  selector:
    matchLabels:
      track: canary
  endpoints:
  - port: http
    path: /metrics
    interval: 15s

PromQL-запросы для оценки качества

Error rate (5xx за последние 5 минут):

sum(rate(http_requests_total{job="go-service-canary",status=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="go-service-canary"}[5m]))

P99 latency:

histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{job="go-service-canary"}[5m]))
  by (le)
)

Автоматический откат при превышении порогов ошибок

Скрипт check_metrics.sh опрашивает Prometheus API и инициирует откат, если метрики выходят за допустимые пределы.

#!/bin/bash
# check_metrics.sh
# Аргумент: текущий вес canary (для логирования)
CANARY_WEIGHT=$1
PROMETHEUS_URL="http://prometheus.monitoring.svc.cluster.local:9090"
ERROR_THRESHOLD=0.02   # 2% ошибок
LATENCY_THRESHOLD=0.5  # 500ms p99
WAIT_SECONDS=120

echo "Ожидаем ${WAIT_SECONDS}с накопления метрик при весе canary=${CANARY_WEIGHT}%..."
sleep $WAIT_SECONDS

# Запрос error rate
ERROR_RATE=$(curl -sf "${PROMETHEUS_URL}/api/v1/query" \
  --data-urlencode 'query=sum(rate(http_requests_total{job="go-service-canary",status=~"5.."}[5m])) / sum(rate(http_requests_total{job="go-service-canary"}[5m]))' \
  | jq -r '.data.result[0].value[1] // "0"')

# Запрос p99 latency
LATENCY=$(curl -sf "${PROMETHEUS_URL}/api/v1/query" \
  --data-urlencode 'query=histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="go-service-canary"}[5m])) by (le))' \
  | jq -r '.data.result[0].value[1] // "0"')

echo "Error rate: ${ERROR_RATE} (порог: ${ERROR_THRESHOLD})"
echo "P99 latency: ${LATENCY}s (порог: ${LATENCY_THRESHOLD}s)"

# Сравниваем с порогами через awk
ERROR_EXCEEDED=$(awk "BEGIN {print (${ERROR_RATE} > ${ERROR_THRESHOLD}) ? 1 : 0}")
LATENCY_EXCEEDED=$(awk "BEGIN {print (${LATENCY} > ${LATENCY_THRESHOLD}) ? 1 : 0}")

if [ "$ERROR_EXCEEDED" = "1" ] || [ "$LATENCY_EXCEEDED" = "1" ]; then
  echo "КРИТИЧНО: метрики превысили допустимые пороги. Инициируем откат..."
  kubectl annotate ingress go-service-canary \
    nginx.ingress.kubernetes.io/canary-weight=0 \
    --overwrite -n production
  kubectl scale deployment/go-service-canary --replicas=0 -n production
  echo "Откат выполнен. Canary-деплой прерван."
  exit 1
fi

echo "Метрики в норме. Продолжаем деплой."
exit 0

Скрипт вызывается из пайплайна на каждом этапе перед увеличением трафика. При выходе с кодом 1 GitHub Actions останавливает workflow и следующие шаги не выполняются — откат уже произошёл.

Реальный кейс: canary-деплой Go-сервиса с нулевым даунтаймом

Рассмотрим типичный сценарий: Go-сервис обработки платежей, ~500 RPS на продакшене. Команда выкатывает новую версию с оптимизацией SQL-запросов. Последствия ошибки критичны — даже 1% 5xx на платёжном сервисе недопустим.

  1. Сборка образа: GitHub Actions собирает Docker-образ Go-сервиса и пушит в registry с тегом SHA коммита.
  2. Деплой canary 5%: Запускается 1 новый под, Ingress настраивается на 5% трафика. Ждём 2 минуты.
  3. Проверка метрик: Prometheus фиксирует error rate 0.1% (норма) и p99 latency 120ms (норма ≤500ms). Пайплайн продолжается.
  4. Увеличение до 20%: Добавляем реплики, меняем вес. Новая проверка через 3 минуты — всё в норме.
  5. Полный переход: Обновляем stable Deployment новым образом через rolling update, убираем canary-вес, масштабируем canary до 0. Даунтайм — 0 секунд.

Весь процесс занял около 15 минут, автоматически, без ручного вмешательства. GitOps-подход (манифесты в Git, изменения только через пайплайн) обеспечивает аудит каждого изменения.

Типичные ошибки и как их избежать

  • Слишком короткое окно наблюдения: 30 секунд недостаточно для статистически значимых метрик при низком RPS. Минимум — 2-5 минут в зависимости от трафика.
  • Игнорирование прогрева JVM/Go-рантайма: Первые секунды после старта пода латентность будет выше нормы. Используйте readinessProbe и startupProbe, чтобы трафик пошёл только на готовый под.
  • Canary без изоляции sticky sessions: Если пользователи должны оставаться на одной версии (например, A/B-тест), используйте nginx.ingress.kubernetes.io/canary-by-cookie или canary-by-header.
  • Отсутствие алертов при застрявшем canary: Если пайплайн упал, а canary-Ingress остался с весом 20% — это нештатная ситуация. Добавьте Alertmanager-правило на длительно активный canary-Ingress.
  • Единый Service для обоих Deployment: Если нужна точная весовая маршрутизация — используйте отдельные Services для stable и canary, а распределение трафика делайте только через Ingress/Gateway API.
  • Нет ограничений ресурсов на canary: Canary-под без limits может «съесть» ресурсы соседних подов. Всегда задавайте requests и limits.

Заключение и чеклист

Canary deployment на Kubernetes — это зрелый подход к безопасному деплою, который при правильной настройке полностью исключает ручной контроль. Связка Kubernetes + NGINX Ingress + Prometheus + CI/CD даёт полный цикл: деплой → наблюдение → автоматическое решение (продолжить или откатить).

Безопасный деплой — это не героизм дежурного инженера в 3 ночи, а правильно настроенная автоматика, которая решает проблему раньше, чем её заметят пользователи.

Чеклист для внедрения canary-деплоя:

  • Созданы отдельные Deployment для stable и canary версий
  • Настроен Ingress с аннотациями canary-weight
  • ServiceMonitor или Pod-аннотации для сбора метрик Prometheus
  • Определены пороги error rate и latency (например, <2% ошибок, p99 <500ms)
  • Скрипт проверки метрик интегрирован в CI/CD-пайплайн
  • Откат реализован как явный шаг пайплайна с кодом выхода 1
  • Readiness и Liveness пробы настроены на canary-поде
  • Настроен алерт на «застрявший» canary-Ingress
  • Манифесты хранятся в Git (GitOps-подход)
  • Проведено тестовое прохождение с намеренной ошибкой для проверки отката

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →