Всем привет! На связи Юрий Шахов, DevOps-инженер компании «Флант». Недавно мне нужно было организовать бесшовный деплой клиентских приложений. Я изучил различные подходы для этого и остановился на стратегии blue-green-деплоя. Но проблема была в том, что я не мог найти материалов с практическими примерами, в найденных статьях описывались только теоретические аспекты. Поэтому мне пришлось изучать подход blue-green-деплоя самостоятельно. И теперь мне захотелось поделиться этим опытом.
В статье я задеплою приложение в blue-green, а также покажу, как работает смена между blue и green на практике. Я не буду рассматривать различные стратегии деплоя, а также их преимущества. Для ознакомления с теорией blue-green и других вариаций рекомендую почитать наш материал про различные стратегии деплоя в Kubernetes.
Эту же статью я поделил на две части: сначала рассмотрим реализацию деплоя приложения в blue-green-стратегии, а затем попробуем werf bundle для деплоя нескольких приложений из одного репозитория. Есть разные способы реализовать эту стратегию, можно использовать дополнительные инструменты, такие как Service Mesh, Argo CD и другие. Я же буду деплоить с помощью werf, все ресурсы описывать как Helm-шаблоны, а для развёртывания использовать GitLab. Предполагается, что читатель знаком с этими технологиями. Особенность здесь в том, что применяются нативные сущности и механизмы в виде лейблов для Kubernetes. Далее green и blue будем называть «версиями» приложения. Также в этой статье не будем рассматривать вопросы миграции баз данных, хотя для некоторых приложений это может быть необходимо.
Простой blue-green
Предположим, что у нас есть приложение и мы хотим его задеплоить. Делать это будем в два этапа:
Деплой самого приложения (
deploy_app
), например Deployment и Service.Смена версии: деплой Ingress с нужным именем Service (
deploy_ingress
). В этой стадии будет переключение трафика между версиями приложения.
Представим это в виде пайплайна:
Для реализации определим переменную deploy_version
(значение будем брать из GitLab CI, который рассмотрим позже), которая будет равна blue
или green
и будет подставляться в Helm-шаблоны. Для Deployment и Service добавляем лейблы:
{{ $deploy_version := "" }}
{{ if .Values.werf.deploy_version }}
{{ $deploy_version = print "-" .Values.werf.deploy_version }}
{{ end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}{{ $deploy_version }}
labels:
app: {{ .Chart.Name }}{{ $deploy_version }}
...
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}{{ $deploy_version }}
spec:
selector:
app: {{ .Chart.Name }}{{ $deploy_version }}
...
Чтобы трафик дошёл до пода, нужно создать Ingress. Для обращения к определённой версии приложения мы будем указывать Service с нужным именем (blue
или green
). В таком случае шаблон Ingress будет выглядеть следующим образом:
{{ $deploy_version := "" }}
{{ if .Values.werf.deploy_version }}
{{ $deploy_version = print "-" .Values.werf.deploy_version }}
{{ end }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
labels:
deploy-version: {{ .Values.werf.deploy_version | quote }}
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Chart.Name }}{{ $deploy_version }}
port:
name: http
tls:
- hosts:
- example.com
secretName: {{ .Chart.Name }}-tls
Вместо изменений на Ingress можно изменять Service, направляя трафик на blue
или green
Deployment по лейблам, но делать этого не рекомендуется. В таком случае мы потеряем возможность обращаться к Deployment по имени Service в кластере. Это может быть проблемой для проверки правильности обновления, так как не получится обратиться к новой версии до того, как на неё направится весь трафик.
Ещё можно создать второй Ingress, ведущий на неактивную версию, с другим доменом для тестирования. В таком случае нужно закрыть его за авторизацией для ограничения доступа.
Теперь рассмотрим пайплайн. При деплое приложения необходимо объявлять переменную deploy_version
— версию, на которую будет производиться деплой. Для werf это можно сделать так:
werf converge --set "werf.deploy_version=${DEPLOY_VERSION}"
Также во время деплоя будем проверять, что версия не является активной, то есть на неё не идёт трафик, и наш выкат не повлияет на пользователей. Сама проверка реализована следующим образом: мы получаем информацию о том, на какой Service указывает работающий в кластере Ingress, и находим там blue
или green
.
Ниже пример полного gitlab-ci.yml
:
stages:
- deploy_app
- deploy_ingress
.check_upstreams: &check_upstreams
- APP_CURRENT_ACTIVE=$(werf kubectl -n ${WERF_NAMESPACE} get ingress example --output=custom-columns='SVCs:..service.name' --no-headers --ignore-not-found | awk -F '-' {'print $NF'})
.deploy_app:
stage: deploy_app
script:
- *check_upstreams
- if [[ ${KUBE_CURRENT_ACTIVE} == ${UPSTREAM} ]];
then
tput setaf 9 && echo "Обнаружена попытка деплоя на активную версию, деплой будет остановлен!" && exit 1;
else
werf converge \
--release example-${UPSTREAM} \
--set "werf.deploy_version=${UPSTREAM}";
fi;
allow_failure: false
.deploy_ingress:
stage: converge_ingresses
script:
- *check_upstreams
- if [ ${APP_CURRENT_ACTIVE} == ${DEPLOY_VERSION} ];
then
tput setaf 9 && echo "Обнаружена попытка переключения на активную версию, деплой будет остановлен!" && exit 1;
else
werf converge
--set "werf.deploy_version=${DEPLOY_VERSION}"
fi;
Deploy to blue:
extends: .deploy_app
environment:
name: production
variables:
UPSTREAM: "blue"
Deploy to green:
extends: .deploy_app
environment:
name: production
variables:
UPSTREAM: "green"
Switch to blue:
extends: .deploy_ingress
environment:
name: production
variables:
DEPLOY_VERSION: "blue"
Switch to green:
extends: .deploy_ingress
environment:
name: production
variables:
DEPLOY_VERSION: "green"
Что мы в итоге сделали:
Скорректировали Helm-шаблоны для Deployment, Service и Ingress, добавив к ним «цвет» нашей версии.
-
Написали CI, который:
деплоит приложение в
blue
иgreen
;деплоит Ingress, который переключает трафик на нужную версию;
проверяет, что деплои не происходят на активную версию.
А теперь перейдём к части с бандлами.
Деплой нескольких приложений с помощью werf bundle
Зачем могут понадобиться бандлы? Допустим, клиенту необходимо деплоить несколько приложений вместе, соответственно, делать это удобнее из одного репозитория. Механизм бандлов позволяет опубликовать чарт приложения и деплоить его в дальнейшем без доступа к конкретному Git-репозиторию. Всё, что требуется, — это доступ к container registry, где хранится бандл. Такой подход позволяет упростить процесс доставки чарта приложений.
Упаковывать приложения мы будем с помощью werf bundle. Подробно на описании инструмента останавливаться не будем, с его преимуществами и кейсами использования можно ознакомиться в документации.
Создание бандлов происходит в основном репозитории приложения, здесь же сконцентрируемся только на развёртывании. В CI-файле укажем названия приложений и соответствующие переменные для каждого из них: репозиторий, тег бандла и название Ingress:
variables:
FIRST_REPO_BUNDLE: registry.gitlab.awesome.ru/frontend/first
FIRST_TAG: "0.1"
FIRST_INGRESS: first
...
# apps_for_matrix & apps_for_bash должны содержать одинаковые значения!
.apps_for_matrix: &apps_for_matrix
["FIRST", "SECOND", "THIRD", "FOURTH", "FIFTH"]
.apps_for_bash: &apps_for_bash
APPLICATIONS=("FIRST", "SECOND", "THIRD", "FOURTH", "FIFTH")
По сравнению с первой частью в пайплайне будет уже три стадии. Поскольку мы деплоим из одного репозитория несколько приложений, нам необходимо убедиться, что все они находятся в одном состоянии. Для этого реализуем джобу проверки состояния версий, назовём её check_upstream
:
На этой стадии должны выполняться следующие условия:
Активная версия у всех приложений одинаковая.
У приложения нет активных версий (при условии, что оно ещё не было задеплоено в кластер).
stages:
- check_upstreams
- deploy_apps
- deploy_ingresses
.base_werf: &base_werf
- set -x
- type trdl && source $(trdl use werf 2)
- werf version
- type werf && source $(werf ci-env gitlab --verbose --as-file)
.check_upstreams: &check_upstreams
- *base_werf
- *apps_for_bash
- |
GREEN=false
BLUE=false
EMPTY=0
for APP in ${APPLICATIONS[@]}
do
REPOSITORY_INGRESS=${APP}_INGRESS
APP_CURRENT_ACTIVE=$(werf kubectl -n ${WERF_NAMESPACE} get ingress ${!REPOSITORY_INGRESS} --output=custom-columns='SVCs:..service.name' --no-headers --ignore-not-found | awk -F '-' {'print $NF'})
EMPTY=$((EMPTY+1))
if [[ ${APP_CURRENT_ACTIVE} == "green" ]];
then GREEN=true;
elif [[ ${APP_CURRENT_ACTIVE} == "blue" ]];
then BLUE=true;
elif [[ -z ${APP_CURRENT_ACTIVE} ]];
then EMPTY=$((EMPTY-1));
else
tput setaf 9 && echo "Что-то пошло не так! Статус версий некорректен" && exit 1;
fi;
done
if [[ ${GREEN} != ${BLUE} ]];
then
if [[ ${GREEN} ]]
COLOR="green"
then tput setaf 14 && echo "Статус версий для приложений одинаков — green, можно продолжать деплой";
elif [[ ${BLUE} ]]
COLOR="blue"
then tput setaf 14 && echo "Статус версий для приложений одинаков — blue, можно продолжать деплой";
fi;
elif [[ ${EMPTY} = 0 ]]
then tput setaf 14 && echo "Ingress для данных приложений в кластере не обнаружено, можно продолжать деплой";
else
tput setaf 9 && echo "Статус версий для приложений отличается, деплой будет остановлен!!!" && exit 1;
fi;
Check_upstreams:
stage: check_upstreams
script:
- *check_upstreams
environment:
name: production
when: always
allow_failure: false
Деплой приложения будет происходить с применением бандла. В эту команду мы передаём все необходимые аргументы и не забываем указать разное имя релиза (флаг --release
) для разных приложений, иначе деплой одного будет перезаписывать деплой предыдущего. Далее с помощью parallel:matrix
на стадии деплоя будет автоматически создано нужное количество джоб деплоя, исходя из количества приложений:
Ниже пример реализации деплоя приложений в CI:
.deploy_apps: &deploy_apps
stage: deploy_apps
before_script:
- *base_werf
- REPOSITORY_BUNBLE=${REPOSITORY_NAME}_REPO_BUNDLE
- REPOSITORY_TAG=${REPOSITORY_NAME}_TAG
- REPOSITORY_INGRESS=${REPOSITORY_NAME}_INGRESS
- APP_CURRENT_ACTIVE=$(werf kubectl -n ${WERF_NAMESPACE} get ingress ${!REPOSITORY_INGRESS} --output=custom-columns='SVCs:..service.name' --no-headers --ignore-not-found | awk -F '-' {'print $NF'})
- |
if [[ ${APP_CURRENT_ACTIVE} = ${DEPLOY_VERSION} ]];
then tput setaf 9 && echo "Обнаружена попытка деплоя на активную версию, деплой будет остановлен!!!" && exit 1;
fi;
script:
- werf cr login -u nobody -p ${BUNDLE_PULLER_PASSWORD} ${!REPOSITORY_BUNBLE}
- werf bundle apply
--release $(echo ${!REPOSITORY_BUNBLE} | cut -d / -f4)-${DEPLOY_VERSION}-${CI_ENVIRONMENT_SLUG}
--repo ${!REPOSITORY_BUNBLE}
--tag ${!REPOSITORY_TAG}
--set "werf.deploy_version=${DEPLOY_VERSION}"
when: manual
Deploy to Green:
extends: .deploy_apps
stage: deploy_apps
environment:
name: production
parallel:
matrix:
- REPOSITORY_NAME: *apps_for_matrix
variables:
DEPLOY_VERSION: "green"
Так мы получили пайплайн, который позволяет из одного репозитория деплоить разные приложения из ранее опубликованных бандлов.
Заключение
Blue-green помогает надёжно и быстро выкатить обновления своих приложений. Эта стратегия упрощает процесс и даёт возможность тестировать новую версию перед её полноценным запуском. А бандлы особенно полезны для деплоя нескольких приложений одновременно. Это делает управление и обновление более наглядными и централизованными, что особенно важно для больших проектов.
В статье мы рассмотрели деплой приложений в стратегии blue-green с помощью GitLab-CI и модифицировали наш CI для деплоя нескольких приложений из одного репозитория. Это руководство помогает написать CI и развернуть своё приложение из GitLab. Надеюсь, оно будет полезным.
P. S.
Читайте также в нашем блоге:
BATAZOR
Не лучше ли использовать какое либо специализированное ПО - например argo rollout или flagger? Тогда можно будет и более сложные сценарии деплоя делать (например с промежуточными проверками), да и другие паттерны выкатки заюзать - например канареечное обновление + обычно там же есть интеграция с разными Nginx/Istio/etc из коробки, не нужно будет это руками описывать, особенно если таких сервисов десятки
есть конечно минусы: +1 компонент в системе и время на освоение, но последнее это разовая затрата