Canary-релизы в Kubernetes на базе Ingress-NGINX Controller

от автора

Тема «канареечных» (canary) релизов поднималась в нашем блоге уже не раз — см. ссылки в конце статьи. Но не будет лишним напомнить, зачем они нужны.

Canary-развертывание используется, чтобы протестировать новую функциональность на отдельной группе пользователей. Группа выделяется по определенному признаку. Тест при этом не должен затрагивать работу основной версии приложения и его пользователей. Нагрузка между двумя версиями приложения должна распределяться предсказуемо. 

Canary-релизы достаточно просто реализуются на уровне Ingress-контроллеров. В статье рассмотрен практический пример настройки таких релизов в Kubernetes на базе Ingress NGINX Controller.

Примечание

Реализация применима только для приложений, к которым обращаются именно через Ingress. Если ваше приложение взаимодействует с окружением исключительно на уровне Service, рассмотренный метод не подойдет.

Готовим приложение для тестов

Для примера нам потребуется небольшое приложение. Возьмем базовый NGINX, который будет отдавать одну HTML-страницу, и Ingress-контроллер, через который будем обращаться к веб-серверу.

Получился такой чарт:

apiVersion: apps/v1 kind: Deployment metadata:   name: {{ .Chart.Name }} spec:   revisionHistoryLimit: 3   selector:     matchLabels:       app: {{ .Chart.Name }}   replicas: 1   template:     metadata:       annotations:         checksum/config: {{ include (print $.Template.BasePath "/10-nginx-config.yaml") . | sha256sum }}       labels:         app: {{ .Chart.Name }}     spec:       volumes:       - name: configs         configMap:           name: {{ .Chart.Name }}-configmap       containers:       - name: nginx         imagePullPolicy: Always         image: {{ index .Values.werf.image "nginx" }}         lifecycle:           preStop:             exec:               command: [ "/bin/bash", "-c", "sleep 5; kill -QUIT 1" ]         command: ["/usr/sbin/nginx", "-g", "daemon off;"]         ports:         - containerPort: 80           name: http           protocol: TCP         volumeMounts:         - name: configs           mountPath: /etc/nginx/nginx.conf           subPath: nginx.conf         resources:           requests:             cpu: 50m             memory: 128Mi           limits:             memory: 128Mi --- apiVersion: v1 kind: Service metadata:   name: {{ .Chart.Name }} spec:   clusterIP: None   selector:     app: {{ .Chart.Name }}   ports:   - name: http     port: 80  --- apiVersion: v1 kind: ConfigMap metadata:   name: {{ .Chart.Name }}-configmap data:   nginx.conf: |     error_log  /dev/stderr;     events {       worker_connections 100000;       multi_accept on;     }     http {         charset utf-8;          server {             listen 80;             index index.html;             root /app;             error_log /dev/stderr;             location / {                 try_files $uri /index.html$is_args$args;             }         }     }  --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: {{ .Chart.Name }} spec:   rules:   - host: "canary-example.flant.com"     http:       paths:       - path: "/"         pathType: Prefix         backend:           service:             name: {{ .Chart.Name }}             port:               number: 80

В корень проекта добавим страницу, которую будет отдавать веб-сервер. Назовем ее index.html:

<!DOCTYPE html> <html> <body> Hi! I'm another one typical nginx! </body> </html>

Также для деплоя нашего приложения потребуется CI*.

* Примечание

В примере рассмотрен деплой на базе GitLab CI + werf.

Создадим в корне проекта файл конфигурации werf — werf.yaml:

project: canary-example configVersion: 1 deploy:  helmRelease: '[[ project ]]"'  namespace: "[[ project ]]"  --- image: nginx from: nginx:stable git: - add: /   to: /app   excludePaths:   - .helm   - werf.yaml   - .getlab-ci.yml

… и файл конфигурации GitLab CI — .gitlab-ci.yml **:

stages:   - converge  .base_converge: &base_converge   stage: converge   script:     - werf converge    except:     - schedules   tags:     - werf  Converge base:   <<: *base_converge   environment:     name: canary-example   when: manual
** Примечание

Подробнее про использование werf для сборки образов и развертывания приложений можно прочитать в документации. Все исходные коды приложения и чартов можно найти в нашем репозитории.

Развернем приложение в кластере, запустив CI, и проверим, что оно работает:

$ curl canary-example.flant.com  Hi! I'm another one typical nginx!

Отлично! 

Переходим к реализации canary-релизов.

Делаем «канареечный» релиз

Подготовим две параллельно работающие версии приложения. Для этого нам понадобятся два отдельных Helm-релиза. 

Модифицируем созданный ранее CI, добавив в него отдельный Job для canary-деплоя и переменную $CANARY_DEPLOY, которую будем подставлять в название Helm-релиза. 

Внесем изменения в файлы проекта — в .gitlab-ci.yml:

stages:   - converge  .base_converge: &base_converge   stage: converge   script:     - export CI_HELM_RELEASE=${CANARY_DEPLOY}     - werf converge        --set "global.canary_deploy=${CANARY_DEPLOY:-}"   except:     - schedules   tags:     - werf  Converge base:   <<: *base_converge   environment:     name: canary-example   when: manual   variables:     CANARY_DEPLOY: ""  Converge canary::   <<: *base_converge   environment:     name: canary-example   when: manual   variables:     CANARY_DEPLOY: "-canary"

… и в werf.yaml:

project: canary-example configVersion: 1 deploy:  helmRelease: '[[ project ]]{{ env "CI_HELM_RELEASE" }}'  namespace: "[[ project ]]"  --- image: nginx from: nginx:stable git: - add: /   to: /app   excludePaths:   - .helm   - werf.yaml   - .getlab-ci.yml

Обратите внимание, что в .gitlab-ci.yml переменная $CANARY_DEPLOY используется в обоих вариантах деплоя (base и canary). Но в первом случае она содержит лишь пустую строку, а при canary — значение -canary. Соответственно, релиз основной версии будет называться nginx-example, а canary-релиз — nginx-example-canary.

Чтобы имена ресурсов в релизах не совпадали, немного модифицируем чарт. Для этого переопределим названия ресурсов по шаблону «название чарта + значение переменной global.canary_deploy»:

{{ $name := printf "%s%s" (.Chart.Name) (.Values.global.canary_deploy) }} apiVersion: apps/v1 kind: Deployment metadata:   name: {{ $name }} spec:   revisionHistoryLimit: 3   selector:     matchLabels:       app: {{ $name }}   replicas: 1   template:     metadata:       annotations:         checksum/config: {{ include (print $.Template.BasePath "/10-nginx-config.yaml") . | sha256sum }}       labels:         app: {{ $name }}     spec:       imagePullSecrets:       - name: registrysecret       volumes:       - name: configs         configMap:           name: {{ $name }}-configmap       containers:       - name: nginx         imagePullPolicy: Always         image: {{ index .Values.werf.image "nginx" }}         lifecycle:           preStop:             exec:               command: [ "/bin/bash", "-c", "sleep 5; kill -QUIT 1" ]         command: ["/usr/sbin/nginx", "-g", "daemon off;"]         ports:         - containerPort: 80           name: http           protocol: TCP         volumeMounts:         - name: configs           mountPath: /etc/nginx/nginx.conf           subPath: nginx.conf         resources:           requests:             cpu: 50m             memory: 128Mi           limits:             memory: 128Mi --- apiVersion: v1 kind: Service metadata:   name:{{ $name }} spec:   clusterIP: None   selector:     app: {{ $name }}   ports:   - name: http     port: 80  --- apiVersion: v1 kind: ConfigMap metadata:   name: {{ $name }}-configmap data:   nginx.conf: |     error_log  /dev/stderr;     events {       worker_connections 100000;       multi_accept on;     }     http {         charset utf-8;          server {             listen 80;             index index.html;             root /app;             error_log /dev/stderr;             location / {                 try_files $uri /index.html$is_args$args;             }         }     }

В завершение добавим на Ingress аннотацию, которая определяет, какой процент трафика мы хотим направить в canary-версию приложения:

{{ $name := printf "%s%s" (.Chart.Name) (.Values.global.canary_deploy) }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: {{ $name }} {{- if ne .Values.global.canary_deploy "" }}   annotations:    nginx.ingress.kubernetes.io/canary: "true"    nginx.ingress.kubernetes.io/canary-weight: "30" {{- end }} spec:   rules:   - host: "canary-example.flant.com"     http:       paths:       - path: "/"         pathType: Prefix         backend:           service:             name: {{ $name }}             port:               number: 80

Аннотация nginx.ingress.kubernetes.io/canary-weight: "30" говорит о том, что 30% запросов должны быть направлены в новую версию приложения. 

Подробнее ознакомиться с этой функциональностью можно в документации контроллера.

Проверяем работоспособность

Так как для выката мы используем werf, подразумевается, что проект должен быть Git-репозиторием. Создадим отдельную ветку и изменим в ней содержимое веб-страницы:

<!DOCTYPE html> <html> <body> Wow! I'm canary nginx! </body> </html>

Развернем новую версию приложения из созданной ветки и проверим, что получилось:

$ for ((i=1;i<=10;i++)); do   curl -s "canary-example.flant.com"; done  Hi! I'm another one typical nginx! Hi! I'm another one typical nginx! Hi! I'm another one typical nginx! Hi! I'm another one typical nginx! Hi! I'm another one typical nginx! Wow! I'm canary nginx! Wow! I'm canary nginx! Hi! I'm another one typical nginx! Hi! I'm another one typical nginx! Wow! I'm canary nginx!

Как видно, в трех случаях из десяти мы получили ответ от новой версии приложения. При этом остальные семь запросов были обработаны базовой версией приложения.

Отлично, но можно лучше!

Пробуем альтернативный вариант — с header’ом

Зачастую требуется управлять балансировкой трафика между версиями приложения более гибко, нежели просто процентным соотношением. Реализовать это можно с помощью специального header’а или cookie в клиентском запросе. Способы практически не отличаются по реализации, поэтому рассмотрим вариант с header’ом.

Передадим в CI ключ и значение для header’а и немного изменим аннотации в Ingress-контроллере:

{{- if ne .Values.global.canary_deploy "" }}   annotations:     nginx.ingress.kubernetes.io/canary: "true"     nginx.ingress.kubernetes.io/canary-by-header: {{ $.Values.global.canary_header | quote }}     nginx.ingress.kubernetes.io/canary-by-header-value: {{ $.Values.global.canary_header_value | quote }} {{- end }}

Добавим нужные переменные в CI:

.base_converge: &base_converge   stage: converge   script:     - export CI_HELM_RELEASE=${CANARY_DEPLOY}     - werf converge        --set "global.canary_deploy=${CANARY_DEPLOY:-}"       --set "global.canary_header=${CANARY_HEADER:-}"       --set "global.canary_header_value=${CANARY_HEADER_VALUE:-}"   except:     - schedules   tags:     - werf  Converge to canary:   <<: *base_converge   environment:     name: canary-example   when: manual   variables:     CANARY_DEPLOY: "-canary"     CANARY_HEADER: "x-version"     CANARY_HEADER_VALUE: "canary"

Развернем приложение и проверим, что получилось:

$ curl canary-example.flant.com  Hi! I'm another one typical nginx!

Всё работает. 

Теперь передадим в запросе нужный header:

$ curl -H "x-version: canary" canary-example.flant.com  Wow! I'm canary nginx!

Тоже всё работает.

Как развернуть «канареечные» релизы внутри кластера

В статье мы рассмотрели лишь один из вариантов canary-релиза. Главный недостаток такого подхода — необходимость использовать Ingress. Это отлично работает для frontend-приложений. В то время как у backend зачастую только Service, и обращаются к нему уже внутри кластера Kubernetes.

Для таких приложений задачу canary-развертывания отлично решает Service Mesh наподобие Istio — мы используем его, например, в нашей Kubernetes-платформе Deckhouse (познакомиться с функциями, которые решает Istio в рамках Deckhouse, можно в документации). Другие примеры реализации можно найти в официальной документации Istio и в нашем переводе.

P.S.

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/ru/company/flant/blog/697030/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *