Привет всем! Перед деплоем нового релиза приложения в продакшен хорошей практикой является оценка его работы с запросами от реальных пользователей. Если разом заменить текущий релиз приложения на новый, есть вероятность в случае ошибки в программном обеспечении повредить данные большого числа пользователей или получить непрогнозируемое поведение нового релиза под нагрузкой. Чтобы избежать указанных трудностей используют Canary‑релизы — это когда рядом с актуальным релизом приложения в продакшене развертывают релиз с новой версией приложения и направляют на него часть запросов от реальных пользователей. В данной статье мы расскажем о нашем опыте внедрения Canary‑релизов в CI/CD платформу Gitorion.
Схема стенда
Варианты реализации Canary-релизов рассмотрим на примере, приведенном на рисунке ниже. В кластере Kubernetes развернуто приложение, состоящее из микросервисов frontend и backend. В контуре production развернут текущий актуальный релиз приложения, с которым работают клиенты в данный момент. В контуре staging развернут, протестирован и продемонстрирован заказчику новый релиз приложения. Точку входа в кластер и доступ клиентов к приложению выполняет контроллер Ingress-nginx. Теперь нужно протестировать в production новый релиз приложения из контура staging.

Canary-релизы c помощью контроллера Ingress-nginx
Данный вариант подойдет для микросервисов, подключенных как бэкенд непосредственно к контроллеру Ingress‑nginx. В нашем примере это микросервис frontend. В контуре production создайте службу (Service) с именем «frontend‑canary» и Deployment с именем «frontend‑canary», который запускает модуль с контейнером, созданным из Docker‑образа «Image: frontend:30 257». Данный Docker‑образ «Image: frontend:30 257» использован для запуска контейнера с новой версией приложения в контуре staging (на картинке ниже выделили красным цветом).

Ниже приведем yaml-манефест Ingress уже созданный в production контуре, который направляет трафик для домена gitorion.ru в службу с именем «frontend», которая в свою очередь передает запросы в модуль с текущей версией приложения.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
meta.helm.sh/release-name: frontend
meta.helm.sh/release-namespace: production
generation: 1
labels:
app.kubernetes.io/managed-by: Helm
name: frontend
namespace: production
spec:
ingressClassName: nginx
rules:
- host: gitorion.ru
http:
paths:
- backend:
service:
name: frontend
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- gitorion.ru
secretName: infrastructure-tls
Добавьте в production еще один Ingress с именем «frontend‑canary», который направит 10% трафика, предназначенного тому же домену gitorion.ru, в службу с именем «frontend‑canary», которая в свою очередь направит запрос в модуль с новой версией приложения.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
meta.helm.sh/release-name: frontend-canary
meta.helm.sh/release-namespace: production
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: canary
nginx.ingress.kubernetes.io/canary-weight: "10"
generation: 1
labels:
app.kubernetes.io/managed-by: Helm
name: frontend-canary
namespace: production
spec:
ingressClassName: nginx
rules:
- host: gitorion.ru
http:
paths:
- backend:
service:
name: frontend-canary
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- gitorion.ru
secretName: infrastructure-tls
В аннотации «nginx.ingress.kubernetes.io/canary‑weight» задайте, какой процент запросов реальных пользователей контроллер Ingress‑nginx должен направить на Canary‑релиз приложения. Постепенно увеличивайте значение «nginx.ingress.kubernetes.io/canary‑weight» в ходе тестирования, наращивая количество запросов от реальных пользователей в модуль с новым релизом приложения.
Непрерывную доставку (Continuous Delivery) в CI/CD платформе Gitorion выполняет Jenkins, поэтому мы добавили параметризованный пайплайн, создающий в production контуре Service, Deployment и Ingress с именами ''frontend‑canary» и позволяющий менять значение параметра «nginx.ingress.kubernetes.io/canary‑weight» в аннотации Ingress «backend‑canary».

В случае успешного завершения тестирования, задайте значение параметра 0 в списке «Какой процент запросов направить на Canary‑релиз», и пайплайн удалит Ingress, Service и Deployment с именем «frontend‑canary». Теперь тимлид может уверенно запустить пайплайн, который развернет контейнер с новой версией приложения из Docker‑образа «Image: frontend:30 257» в контур production.
Canary-релизы с помощью Kubernetes Service Mesh
Теперь разберем, как развернуть Canary-релиз, в случае если микросервис не подключен непосредственно к контроллеру Ingress-nginx или точка входа в кластер Kubernetes реализована не с помощью Ingress-nginx. На нашем стенде это микросервис backend.
В Kubernetes вновь созданные модули получают динамический IP-адрес из подсети IP-адресов для модулей. Если Deployment или StatefulSet запускает несколько модулей приложения при горизонтальном масштабировании, точку единого входа организуют с помощью службы (Service). На модули навешивают метки (Labels), и служба с помощью селектора (Selector) направляет запросы в модули микросервиса с заданной меткой. Трафик равномерно балансируется между всеми модулями.
На рисунке ниже новая версия приложения запущена в staging контуре в контейнере, созданном из Docker‑образа «Image:backend:cad09» (выделили красным цветом). Заказчик протестировал и одобрил новую версию приложения, и пришло время протестировать его в продакшене.
В production контуре создайте еще один Deployment с именем «backend‑canary», запускающий модуль с контейнером, созданным из Docker‑образа «Image:backend:cad09», с новой версией приложения. Навесьте метку «app:backend» на модули нового релиза, точно такую же, как и на модулях старого релиза, и служба (Service) с именем «backend» направит часть запросов в модуль с новой версией приложения.

Cписок модулей сервиса backend в контуре production:
control@dc1-plane:~$ kubectl get pod -n production -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
backend-55bd4f7b68-ckttw 1/1 Running 0 30m 10.10.137.22 dc1-worker1 <none> <none>
backend-55bd4f7b68-m865n 1/1 Running 0 22s 10.10.137.14 dc1-worker1 <none> <none>
backend-55bd4f7b68-t432g 1/1 Running 0 22s 10.10.137.15 dc1-worker1 <none> <none>
backend-canary-59674db64b-rt76l 1/1 Running 0 5m52s 10.10.137.17 dc1-worker1 <none> <none>
frontend-777d84c7cd-5d2bc 1/1 Running 0 30m 10.10.137.38 dc1-worker1 <none> <none>
Описание службы «backend» в контуре production:
control@dc1-plane:~$ kubectl describe svc backend -n production
Name: backend
Namespace: production
Labels: app=backend
app.kubernetes.io/managed-by=Helm
Annotations: meta.helm.sh/release-name: backend
meta.helm.sh/release-namespace: production
Selector: app=backend
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.98.154.72
IPs: 10.98.154.72
Port: <unset> 9000/TCP
TargetPort: 9000/TCP
Endpoints: 10.10.137.22:9000,10.10.137.14:9000,10.10.137.15:9000,10.10.137.17:9000
Session Affinity: None
Events: <none>
Среди конечных точек Endpoints службы «backend» видим три IP‑адреса модулей со старой версией приложения (10.10.137.22, 10.10.137.14, 10.10.137.15) и один IP‑адрес (10.10.137.17) модуля с новой версией приложения. Cоответственно, 3/4 запросов реальных пользователей (или 75%) пойдет на старую версию приложения, 1/4 запросов (или 25%) — на новую. Добейтесь требуемого вам процентного соотношения запросов запуском соответствующего количества модулей в Deployment‑ах «backend» и «backend‑canary».
Мы добавили в Jenkins параметризованный пайплайн, в котором можно изменить количество реплик старого и нового релиза.

Покнопке «Собрать» пайплайн cоздаст в production контуре Deployment c именем «backend‑canary», запускающий в модулях контейнеры из Docker‑образа «Image: backend:cad09» с новой версией приложения. Тут же можно менять количество реплик для Deployment‑ов старого и нового релизов. После завершения удачного тестирования, задайте значение 0 в поле «Количество реплик Canary‑релиза», и пайплайн удалит Deployment с именем «backend‑canary». Теперь тимлид может запустить пайплайн, выполняющий промоушен новой версии приложения из контура staging в контур production.
Заключение
В данной статье мы рассказали о нашем опыте внедрения Canary-релизов в CI/CD платформу Gitorion. Продемонстрированные практики предназначены для CI/CD цикла, развернутого в кластере Kubernetes, и реализуют канареечные развертывания как для фронтенда, так и для бэкенда. Спасибо за внимание!