Мы не раз разбирали в своём блоге теорию и практику по разным вариантам развёртывания новой версии приложения в Kubernetes. Сегодня я хочу рассмотреть ещё одну реализацию canary-деплоя: с пассивными healthcheck и использованием Argo Rollouts и Istio. Этот вариант подойдёт, если к вашему приложению обращаются не через Ingress, а ходят внутри кластера по адресу service.

Преимущества и нюансы канареечного деплоя
Перед тем как мы перейдём к делу, коротко напомню, что «канарейки» хороши тем, что пользовательский трафик перенаправляется на новую версию приложения постепенно. Это позволяет дополнительно протестировать новую функциональность на небольшой группе пользователей до того, как катить её на всех. У стратегии canary-деплоя есть и другие преимущества:
Экономия ресурсов. Переход на новую версию происходит плавно, поэтому можно постепенно наращивать рабочую нагрузку с новой версией и, наоборот, сокращать количество экземпляров стабильной версии. Так что нет необходимости сразу поднимать аналогичное по ресурсам окружение.
Простой откат на стабильную версию. Он выполняется переключением трафика на версию, в которой вы уверены.
Беспростойное обновление. Переключение пользовательского трафика между версиями происходит постепенно, поэтому для пользователя прерываний в работе приложения не будет.
Стоит отметить, что canary-деплой подразумевает одновременную работу двух разных версий приложения, что влечёт за собой некоторые нюансы.
Если приложение работает с базой данных, то нужно учитывать, что при выкатке новой версии у вас будут одновременно работать две версии приложения. Поэтому важно, чтобы схема базы данных не ломалась при деплое и была совместима с обеими версиями ПО. Эту проблему можно решить несколькими способами: например, использовать последовательные миграции (одна миграция — для подготовки схемы к работе с новой версией, а после успешного деплоя следующая миграция — для зачистки устаревшей схемы) либо не выполнять изменений в БД, которые ломают обратную совместимость.
Также необходимо реализовать привязку пользовательских сессий к одной и той же версии ПО, иначе запросы пользователя будут попадать на разные версии приложения. Одним из вариантов решения может быть использование Hash / Consistent Hashing.
Как я уже говорил, в этой статье мы разберём реализацию канареечного деплоя приложения с пассивными healthcheck с использованием Argo Rollouts и Istio service mesh. О варианте с Ingress NGINX Controller (далее также — Ingress NGINX) для переключения части трафика между версиями мы рассказывали на Хабре несколько лет назад. Он подходит, если к приложению обращаются через ingress. Если же к сервису ходят внутри кластера по адресу Service, то с помощью Ingress NGINX справиться уже не получится. Тут-то нам и поможет Istio. К тому же Istio предоставляет больше возможностей как с точки зрения Observability, так и с точки зрения Security.
Так как чаще всего приложение принимает трафик извне, то сначала рассмотрим кейс с переключением трафика на уровне Ingress NGINX, а затем покажем, как работает обновление на примере запросов по адресу внутреннего сервиса.
Подготовка окружения
Мы будем настраивать канареечный деплой на базе Open Source-платформы Deckhouse Kubernetes Platform Community Edition (DKP CE). Нам понадобятся следующие компоненты:
Prometheus в лице Deckhouse Prom++
Для развёртывания кластера с DKP CE вы можете использовать быстрый старт. Когда платформа будет готова, нужно будет выполнить ещё три подготовительных шага.
Первым делом создаём Ingress NGINX Controller, через который пользователи будут получать доступ к нашему приложению. Обязательно включаем параметр enableIstioSidecar, чтобы контроллер NGINX был под контролем Istio:
apiVersion: deckhouse.io/v1
kind: IngressNginxController
metadata:
name: nginx
spec:
ingressClass: nginx
inlet: HostPort
enableIstioSidecar: true
hostPort:
httpPort: 80
httpsPort: 443
nodeSelector:
node-role.kubernetes.io/worker: ""
tolerations:
- effect: NoSchedule
key: node-role.kubernetes.io/worker
operator: Exists
Далее включаем модуль Istio. Здесь d8 — это утилита для управления Kubernetes-платформой Deckhouse:
# d8 system module enable istio
Необходимо дождаться, пока выполнятся задачи по включению модуля. В DKP за этим можно следить, просматривая очередь задач:
# d8 system queue list
Summary:
- 'main' queue: empty.
- 124 other queues (0 active, 124 empty): 0 tasks.
- no tasks to handle.
Очередь задач пуста, можно двигаться дальше.
Устанавливаем Argo Rollouts. При настройке реальных стендов установка утилит производится через Git с использованием GitOps-подхода, но сейчас для простоты сделаем всё через применение манифестов напрямую:
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
Реализация canary-деплоя
Рассмотрим основные компоненты стенда:
NGINX Ingress Controller — контроллер на базе nginx для приёма пользовательского трафика и проксирования на наше целевое приложение.
Istio — реализация технологии сервис-меш (service mesh). В данном случае позволяет нам выполнять умную маршрутизацию трафика между разными версиями приложения.
Argo Rollouts — оператор и набор CRD для реализации более сложных стратегий деплоя, чем Kubernetes изначально предоставляет«из коробки» (canary, blue/green).
Deckhouse Prom++ — встроенное в DKP решение для сбора метрик.
В качестве примера бизнес-приложения напишем небольшой сервис на Golang в трёх версиях. Первая будет являться стабильной, иметь тег v1
и отдавать код 200 на каждый запрос. Вторая будет имитировать релиз с багами, на каждый второй запрос отдавать 500, таким образом имитируя баг, и иметь тег v2
. Третья версия будет иметь тег v3
, она будет играть роль пофикшенной второй версии и отдавать статус код 200.
В конце у нас получится реализация вот такой схемы трафика и основных компонентов:

Пишем манифесты
За распределение пользовательского трафика у нас отвечает Istio. Применим необходимые манифесты для нашего приложения:
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: backend
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: app-vsvc
spec:
hosts:
- app
http:
- name: primary
route:
- destination:
host: app
subset: stable
weight: 100
- destination:
host: app
subset: canary
weight: 0
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: app-destrule
spec:
host: app
subsets:
- name: stable
labels:
app: backend
- name: canary
labels:
app: backend
Обратите внимание на манифест DestinationRule
. Istio позволяет выделить так называемые subsets — наборы эндпойнтов одного и того же Deployment с выборкой по лейблам. В данном случае мы выделяем subset для стабильной версии с именем stable
и для новой версии с именем canary
.
В VirtualService
мы указываем два destination, причём для stable-версии вес указываем 100, а для canary — 0. Именно эти значения будет менять оператор Argo Rollouts в процессе канареечного деплоя, увеличивая таким образом долю трафика, приходящегося на новую версию приложения.
Создаём Rollout:
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: app
spec:
strategy:
canary:
analysis:
templates:
- templateName: success-rate
startingStep: 1
args:
- name: service-name
value: app.default.svc.cluster.local
trafficRouting:
istio:
virtualService:
name: app-vsvc
routes:
- primary
destinationRule:
name: app-destrule
canarySubsetName: canary
stableSubsetName: stable
steps:
- setWeight: 20
- pause: {duration: 1m}
- setWeight: 40
- pause: {duration: 1m}
- setWeight: 60
- pause: {duration: 1m}
- setWeight: 80
- pause: {duration: 1m}
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: rinamuka/canary:v1
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
memory: 5Mi
cpu: 5m
limits:
memory: 10Mi
cpu: 10m
Rollout — это CRD, который приезжает вместе с Argo Rollouts и представляет собой обёртку над объектом Deployment, но с дополнительными настройками стратегии деплоя. Во время деплоя оператор патчит объекты DestinationRule
, меняя там набор лейблов, и VirtualService
, меняя проценты распределения трафика между subset’ами.
Как видим, манифест почти ничем не отличается от ванильного Deployment, за исключением раздела canary. Тот состоит из трёх подразделов:
analisys
— информация о том, какой шаблон healthcheck использовать. Тут стоит отметить параметрstartingStep
, который отвечает за то, с какого шага начинать делать анализ новой версии — те самые запросы в Prometheus. В данном случае проверка происходит в фоновом режиме, но есть возможность выполнять inline-анализ, то есть запускать проверку как самостоятельный step.trafficRouting
— описание, к какимdestinationRule
иvirtualService
привязан наш rollout.steps
— описание самого процесса канареечного деплоя: какими частями переключать трафик и сколько ждать, прежде чем перенести следующую порцию трафика на новую версию.
Чтобы наше приложение было частью сервис-меша, нужно повесить лейбл istio-injection: enabled
на пространство имён, в котором оно находится. После этого в под с нашим приложением будет подселён sidecar-контейнер с Istio-агентом.
Дальше создадим объект Ingress, чтобы открыть наше приложение миру:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/service-upstream: "true"
nginx.ingress.kubernetes.io/upstream-vhost: app.default.svc
spec:
tls:
- hosts:
- app.31.184.210.137.sslip.io
secretName: app-tls
rules:
- host: app.31.184.210.137.sslip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
Обращу внимание на две аннотации для Ingress:
nginx.ingress.kubernetes.io/service-upstream: "true"
— с этой аннотацией Ingress-контроллер будет отправлять запросы на ClusterIP сервиса (из диапазона Service CIDR) вместо того, чтобы слать их напрямую в поды приложения. Sidecar-контейнер istio-proxy перехватывает трафик только в сторону диапазона Service CIDR, остальные запросы отправляются напрямую.nginx.ingress.kubernetes.io/upstream-vhost: app.default.svc
. В Istio вся маршрутизация осуществляется на основе заголовка Host. Чтобы не сообщать Istio о существовании внешнего доменаapp.31.184.210.137.sslip.io
, мы просто используем внутренний домен, о котором Istio осведомлён.
Следующим шагом создадим манифест с AnalysisTemplate. Это описание того, как оценить работоспособность свежей версии приложения. В данном случае будет выполняться запрос в кластерный Prometheus, который входит в DKP «из коробки». Он будет оценивать долю 500-х ответов от общего числа. Если этот показатель оказывается более 5 %, деплой останавливается и весь трафик переключается обратно на стабильную версию приложения. Если он меньше, то мы продолжаем наращивать часть трафика, который идёт на новую версию:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
- name: api-token
valueFrom:
secretKeyRef:
name: rollout-token
key: token
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.95
failureLimit: 2
provider:
prometheus:
address: https://prometheus.d8-monitoring:9090
insecure: true
headers:
- key: Authorization
value: "Bearer {{ args.api-token }}"
query: |
sum(irate(istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[5m])) /
sum(irate(istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]))
Встроенный в DKP Prometheus закрыт авторизацией, поэтому при обращении к нему необходимо передавать кубовый токен. Чтобы получить его, создадим ServiceAccount, Role, RoleBinding и Secret, в котором будет храниться токен от нашего ServiceAccount:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rollout
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: app:prometheus-access
rules:
- apiGroups: ["monitoring.coreos.com"]
resources: ["prometheuses/http"]
resourceNames: ["main", "longterm"]
verbs: ["get","create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app:prometheus-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: app:prometheus-access
subjects:
- kind: ServiceAccount
name: rollout
namespace: default
---
apiVersion: v1
kind: Secret
metadata:
name: rollout-token
annotations:
kubernetes.io/service-account.name: rollout
type: kubernetes.io/service-account-token
Обновляем приложение
Приступим к обновлению нашего приложения. За переключением трафика удобно наблюдать в веб-интерфейсе Isio — Kiali. Откроем его и запустим процесс выкатки новой версии v2.
После изменения тега в Rollout происходит следующее:
1. Поднимется новая реплика с новой версией образа — revision 2.
root@master-0:~# kubectl argo rollouts get rollout app
Name: app
Namespace: default
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/8
SetWeight: 20
ActualWeight: 20
Images: rinamuka/canary:v1 (stable)
rinamuka/canary:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ app Rollout ॥ Paused 3m28s
├──# revision:2
│ └──⧉ app-655bb4c96c ReplicaSet ✔ Healthy 14s canary
│ └──□ app-655bb4c96c-p2lsv Pod ✔ Running 14s ready:2/2
└──# revision:1
└──⧉ app-84975c75b ReplicaSet ✔ Healthy 3m28s stable
└──□ app-84975c75b-tfkqv Pod ✔ Running 3m28s ready:2/2
2. Далее начнут выполняться шаги из манифеста Rollout. Argo Rollout патчит объекты DestinationRule
и VirtualService
.
В DestinationRule он добавляет выборку по лейблу с хешем ReplicaSet. Один subset указывает на старую версию приложения (stable), а второй — на новую (canary):
subsets:
- labels:
app: backend
rollouts-pod-template-hash: 84975c75b
name: stable
- labels:
app: backend
rollouts-pod-template-hash: 655bb4c96c
name: canary
А в VirtualService меняются веса у destination:
spec:
hosts:
- app
http:
- name: primary
route:
- destination:
host: app
subset: stable
weight: 80 # Изначально weight имел значение 100.
- destination:
host: app
subset: canary
weight: 20 # Изначально weight имел значение 0.
Также Argo Rollout-оператор создаёт объект с типом AnalysisRun
. Именно он отвечает за проверку метрик в Prometheus. В describe объекта можно увидеть статусы проверок:
Status:
Completed At: 2025-08-23T11:59:39Z
Dry Run Summary:
Message: Metric "success-rate" assessed Failed due to failed (3) > failureLimit (2)
Metric Results:
Count: 3
Failed: 3
Measurements:
Finished At: 2025-08-23T11:57:39Z
Phase: Failed
Started At: 2025-08-23T11:57:39Z
Value: [0.9033333333333333]
Finished At: 2025-08-23T11:58:39Z
Phase: Failed
Started At: 2025-08-23T11:58:39Z
Value: [0.7958333333333333]
Finished At: 2025-08-23T11:59:39Z
Phase: Failed
Started At: 2025-08-23T11:59:39Z
Value: [0.8058333333333334]
Две из трёх проверок завершились некорректно: доля 500-х ошибок была больше 5 %. А в Kiali мы можем увидеть, как произошло переключение трафика:

Так как проверка не прошла, будет выполнено переключение на stable-версию:
Normal RolloutResumed 2m29s rollouts-controller Rollout is resumed
Normal Updated VirtualService 2m29s rollouts-controller VirtualService `app-vsvc` set to desiredWeight '40'
Normal TrafficWeightUpdated 2m29s rollouts-controller Traffic weight updated from 20 to 40
Normal RolloutStepCompleted 2m29s rollouts-controller Rollout step 3/8 completed (setWeight: 40)
Normal AnalysisRunRunning 2m29s rollouts-controller Background Analysis Run 'app-655bb4c96c-2' Status New: 'Running' Previous: ''
Warning AnalysisRunFailed 29s rollouts-controller Background Analysis Run 'app-655bb4c96c-2' Status New: 'Failed' Previous: 'Running'
После отката под с canary-репликой будет удалён, а весь трафик будет переключён на stable:

С обновлением на релиз с багом и последующим откатом разобрались. Теперь обновим приложение на релиз, работающий корректно, — на версию v3. Выставляем в Rollout тег образа v3:
Name: app
Namespace: default
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/8
SetWeight: 20
ActualWeight: 20
Images: rinamuka/canary:v1 (stable)
rinamuka/canary:v3 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ app Rollout ॥ Paused 11m
├──# revision:3
│ └──⧉ app-76dfffd666 ReplicaSet ✔ Healthy 52s canary
│ └──□ app-76dfffd666-6tk68 Pod ✔ Running 51s ready:2/2
├──# revision:2
│ ├──⧉ app-655bb4c96c ReplicaSet • ScaledDown 7m46s delay:passed
│ └──α app-655bb4c96c-2 AnalysisRun ✖ Failed 4m38s ✖ 3
└──# revision:1
└──⧉ app-84975c75b ReplicaSet ✔ Healthy 11m stable
└──□ app-84975c75b-tfkqv Pod ✔ Running 11m ready:2/2
Смотрим на результаты проверки:
Status:
Dry Run Summary:
Metric Results:
Consecutive Success: 3
Count: 3
Measurements:
Finished At: 2025-08-23T12:04:33Z
Phase: Successful
Started At: 2025-08-23T12:04:33Z
Value: [1]
Finished At: 2025-08-23T12:05:33Z
Phase: Successful
Started At: 2025-08-23T12:05:33Z
Value: [1]
Finished At: 2025-08-23T12:06:33Z
Phase: Successful
Started At: 2025-08-23T12:06:33Z
Value: [1]
Все три проверки выполнились успешно, поэтому трафик будет постепенно переключён на новую версию:
Name: app
Namespace: default
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 5/8
SetWeight: 60
ActualWeight: 60
Images: rinamuka/canary:v1 (stable)
rinamuka/canary:v3 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ app Rollout ॥ Paused 19m
├──# revision:3
│ ├──⧉ app-76dfffd666 ReplicaSet ✔ Healthy 9m1s canary
│ │ └──□ app-76dfffd666-6tk68 Pod ✔ Running 9m ready:2/2
│ └──α app-76dfffd666-3 AnalysisRun ◌ Running 5m53s ✔ 6
├──# revision:2
│ ├──⧉ app-655bb4c96c ReplicaSet • ScaledDown 15m delay:passed
│ └──α app-655bb4c96c-2 AnalysisRun ✖ Failed 12m ✖ 3
└──# revision:1
└──⧉ app-84975c75b ReplicaSet ✔ Healthy 19m stable
└──□ app-84975c75b-tfkqv Pod ✔ Running 19m ready:2/2

Напоследок проверим тот же самый кейс, но обращаясь не через Ingress NGINX, а напрямую к сервису. Это тот самый случай, где Ingress NGINX не справится.
Запустим под, имитирующий клиента, и будем выполнять запросы по имени service(app.default)
, а не по хосту Ingress. И да, в этом случае переключение и балансировка трафика также работают, так как весь роутинг контролируется Istio sidecar:

Итог
Можно настроить канареечный деплой вашего приложения в Kubernetes, используя всего пару инструментов — Argo Rollouts и Istio. В статье я пользовался только базовыми возможностями Argo Rollouts. Помимо упомянутых функций, он поддерживает интеграцию с HPA и VPA, работу с различными балансировщиками нагрузки для роутинга трафика между разными версиями приложений, интеграцию с различными источниками метрик и так далее. Так что можно настроить всё под себя.
Плюс описанного подхода в том, что всё происходит в автоматическом режиме: переключение трафика, проверка состояния нового релиза, откат. Также этот метод не требует сильного изменения манифестов вашего приложения, ведь объект Rollout практически аналогичен ванильному Deployment. Использование же Istio даёт хороший фундамент для дальнейшего развития процесса деплоя в случае необходимости.
Ну и на всякий случай напомню, что каждая стратегия деплоя имеет свои преимущества и недостатки. Выбирайте её, отталкиваясь от особенностей вашего приложения.
Полезные ссылки
Литература по теме
Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation by David Farley, Jez Humble
Cloud Native Patterns: Designing change-tolerant software First Edition by Cornelia Davis
P. S.
Читайте также в нашем блоге: