Canary-деплой на Kubernetes с автоматическим откатом через CI/CD и метрики Prometheus
Введение: что такое 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 на платёжном сервисе недопустим.
- Сборка образа: GitHub Actions собирает Docker-образ Go-сервиса и пушит в registry с тегом SHA коммита.
- Деплой canary 5%: Запускается 1 новый под, Ingress настраивается на 5% трафика. Ждём 2 минуты.
- Проверка метрик: Prometheus фиксирует error rate 0.1% (норма) и p99 latency 120ms (норма ≤500ms). Пайплайн продолжается.
- Увеличение до 20%: Добавляем реплики, меняем вес. Новая проверка через 3 минуты — всё в норме.
- Полный переход: Обновляем 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. Подробнее обо мне →