Всем привет! На связи Юрий Шахов, 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

Предположим, что у нас есть приложение и мы хотим его задеплоить. Делать это будем в два этапа: 

  1. Деплой самого приложения (deploy_app), например Deployment и Service.

  2. Смена версии: деплой 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"

Что мы в итоге сделали:

  1. Скорректировали Helm-шаблоны для Deployment, Service и Ingress, добавив к ним «цвет» нашей версии.

  2. Написали 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.

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

Комментарии (1)


  1. BATAZOR
    10.08.2024 20:45

    Не лучше ли использовать какое либо специализированное ПО - например argo rollout или flagger? Тогда можно будет и более сложные сценарии деплоя делать (например с промежуточными проверками), да и другие паттерны выкатки заюзать - например канареечное обновление + обычно там же есть интеграция с разными Nginx/Istio/etc из коробки, не нужно будет это руками описывать, особенно если таких сервисов десятки

    есть конечно минусы: +1 компонент в системе и время на освоение, но последнее это разовая затрата