Что такое deployment strategy?

Deployment strategy — это способ доставки новой версии приложения. Звучит просто, но на практике за этим скрывается довольно много инженерных вопросов:

  • Будет ли простой?

  • Что произойдёт, если новая версия сломана?

  • Как быстро можно сделать rollback?

  • Можно ли обновлять сервис постепенно?

  • Что увидят пользователи во время обновления?

Представим обычный backend-сервис за Nginx или HAProxy без разницы. У нас есть несколько инстансов приложения, и нужно выкатить новую версию. Самый очевидный вариант — остановить старый процесс и запустить новый. Но в этот момент пользователи могут получить ошибки или вообще остаться без доступа к сервису. Можно обновлять инстансы постепенно — тогда часть пользователей даже не заметит обновление. А можно сначала поднять новую версию отдельно, проверить её, и только потом переключить трафик.

Все эти подходы и есть стратегии деплоя. Если упростить, то любая стратегия пытается найти баланс между:

  1. Скоростью релиза

  2. Стабильностью системы

  3. Стоимостью инфраструктуры

  4. Риск поломки

Recreate Deployment — выключили и включили

Самая простая стратегия деплоя — Recreate Deployment. По сути, это тот самый подход, с которого начинается почти любая инфраструктура: останавливаем старую версию приложения, запускаем новую.

Несмотря на то, что в современном DevOps принято обсуждать прогрессивную доставку, canary release и service mesh, в реальности огромное количество сервисов до сих пор обновляются именно через recreate-подход. Особенно это заметно в legacy-инфраструктурах, на обычных VPS, VM или bare metal-серверах, где за reverse proxy стоит один или несколько backend-процессов.

Выглядит это все максимально дефолтно:

docker compose up -d --force-recreate

или вообще так:

systemctl restart myapp

То есть старая версия приложения полностью останавливается, после чего запускается новая. Пока новый процесс стартует, сервис либо недоступен полностью, либо часть запросов начинает получать ошибки. Если приложение поднимается быстро — пользователь может даже ничего не заметить. Но как только startup-time начинает измеряться десятками секунд, появляются вполне реальные простои и недовольные сообщения в чатах вида: кто опять уронил API.

Особенно весело становится, когда приложение во время старта:

  • прогревает кеш,

  • выполняет миграции БД,

  • переподключается к очередям,

  • ждёт внешние сервисы.

При этом у Recreate Deployment есть одно огромное преимущество — простота. Ему не нужны отдельные маршруты, не нужна балансировка между версиями приложения, не требуется сложная инфраструктура. Один сервер, один процесс, один рестарт.

В некоторых случаях это абсолютно нормальный выбор. Нет большого смысла строить Blue/Green Deployment с автоматическим переключением трафика ради внутренней админ панели, которой пользуются три землекопа пару раз в неделю.

Rolling Update — обновляем приложение постепенно

Когда простой становится проблемой, а фраза-сервис будет недоступен пару минут начинает вызывать нервный тик, инфраструктура обычно переходит к следующему этапу эволюции — Rolling Update.

Идея здесь довольно простая: вместо того чтобы одновременно убить все инстансы приложения и запустить новые, мы обновляем их постепенно. Пока часть серверов или процессов уже работает на новой версии, остальные продолжают обслуживать пользователей на старой. В результате сервис остаётся доступным практически всё время деплоя.

Именно Rolling Update сегодня можно считать дефолтной стратегией почти для любого сервиса. Причина довольно очевидна: Rolling Update даёт хороший баланс между сложностью инфраструктуры и доступностью сервиса. Не нужно держать вторую полноценную копию, как в Blue/Green, не нужен сложный traffic splitting, как в Canary Deployment, но при этом приложение уже можно обновлять практически без простоя.

На практике всё обычно выглядит так: у нас есть несколько backend-инстансов за Nginx upstream.

upstream backend {
    server app1:8080;
    server app2:8080;
    server app3:8080;
}

Nginx распределяет трафик между тремя инстансами приложения. Если просто сделать docker compose down сразу на всех серверах — получим downtime. Вместо этого Rolling Update обновляет их по одному.

Сценарий деплоя выглядит примерно так:

  1. убираем один инстанс из upstream

  2. обновляем его

  3. проверяем healthcheck

  4. возвращаем обратно в балансировку

  5. переходим к следующему инстансу

В Kubernetes Rolling Update является стратегией по умолчанию для Deployment. Параметры maxUnavailable и maxSurge позволяют гибко управлять процессом — например, указать, что в любой момент должно быть недоступно не более одного пода и может быть запущено не более одного дополнительного. Пока один backend обновляется, остальные продолжают обслуживать пользователей.

В Ansible rolling deploy будет выглядеть так:

serial: 1

Параметр serial говорит Ansible обновлять сервера по одному. Пока один хост находится в deploy-состоянии, остальные продолжают принимать трафик. Именно такой подход очень долго был стандартом ещё до массового появления Kubernetes, но есть несколько важных нюансов.

Первый — healthcheck. Если после обновления приложение ещё не готово принимать трафик, но балансировщик уже начал отправлять на него запросы, пользователи начинают получать ошибки. Поэтому нормальный rolling deploy почти всегда включает:

  • readiness endpoint

  • healthcheck

  • graceful shutdown

  • ожидание завершения активных соединений

Например, backend должен сначала:

  1. полностью стартовать,

  2. подключиться к БД,

  3. открыть соединения,

  4. и только потом возвращать 200 OK на /health.

Второй важный момент — совместимость версий. Во время Rolling Update часть серверов работает на старой версии приложения, а часть — на новой. Это означает, что:

  • API должны быть совместимы,

  • миграции БД не должны ломать старую версию.

Blue/Green Deployment

Если Rolling Update решает проблему простоя, то Blue/Green Deployment в первую очередь пытается решить проблему риска.

Представим ситуацию: мы выкатили новую версию приложения через Rolling Update. Первый инстанс обновился успешно, второй тоже, а вот на третьем выяснилось, что в релизе есть критическая ошибка. Формально сервис всё ещё работает, но часть пользователей уже сталкивается с проблемами. Чем больше инстансов успело обновиться, тем больше становится зона поражения.

Blue/Green Deployment предлагает другой подход. Вместо постепенного обновления существующих серверов мы создаём две независимые среды. Одна из них обслуживает пользователей прямо сейчас — условный Blue. Вторая, Green, получает новую версию приложения и существует параллельно с текущим.

С точки зрения пользователей ничего не меняется. Весь трафик продолжает идти на старую версию, пока новая полностью не развернётся, не пройдёт проверки и не будет признана готовой к работе.

Получается примерно такая схема:

Текущий трафик
      │
      ▼

Load Balancer
      │
      ▼
   Blue v1

После подготовки новой версии:

             ┌──── Blue v1
             │
Load Balancer
             │
             └──── Green v2

А затем происходит переключение:

Текущий трафик
      │
      ▼
Load Balancer
      │
      ▼
  Green v2

Главное преимущество этой модели становится заметно в момент аварии. Если после релиза обнаруживается проблема, откат практически мгновенный. Не нужно пересобирать контейнеры, ждать завершения Rolling Update или запускать сложные процедуры восстановления. Достаточно вернуть трафик на предыдущую среду. Именно поэтому Blue/Green Deployment долгое время оставался любимой стратегией для систем с высокими требованиями к доступности — банковских приложений, телеком-инфраструктуры и крупных корпоративных систем, где стоимость ошибки, значительно выше стоимости дополнительной инфраструктуры.

Реализовать такую схему можно разными способами. Самый очевидный вариант — использовать балансировщик нагрузки. Например, если приложение работает за HAProxy, можно держать два backend-пула:

backend_blue
backend_green

Во время релиза новая версия выкатывается в backend_green, проходит проверки, а затем HAProxy переключает основной маршрут. В Kubernetes Blue/Green реализуется через переключение selector у Service между двумя Deployment — blue и green. Нативной поддержки нет, но это легко автоматизируется через Argo CD. Для более сложных сценариев используют Argo Rollouts, который добавляет тип BlueGreen прямо в манифест.

Однако за удобство приходится платить.

Во-первых, инфраструктуру фактически приходится дублировать. Если контур состоит из десяти серверов, то во время деплоя потребуется ещё десять (но не всегда) серверов для новой версии. Для небольших сервисов это может быть несущественно, но для крупных систем стоимость ресурсов становится вполне заметной.

Во-вторых, появляются вопросы синхронизации состояния. Если приложение хранит пользовательские сессии локально, пишет файлы на диск или зависит от локального кеша, переключение между средами может привести к неожиданным последствиям. Поэтому Blue/Green особенно хорошо сочетается со stateless-приложениями, где состояние вынесено во внешние системы: базы данных, Redis, объектные хранилища и очереди сообщений.

Наконец, отдельного внимания заслуживают миграции базы данных. Многие команды, впервые внедряющие Blue/Green Deployment, считают, что получили практически безрисковый релиз. Но на практике новая версия приложения почти всегда использует ту же самую БД, что и старая. Если миграция схемы несовместима с предыдущим кодом, то даже мгновенный откат трафика уже не поможет.

Canary Deployment — проверяем новую версию на части пользователей

Если Blue/Green Deployment отвечает на вопрос, как быстро откатиться, то Canary Deployment отвечает на другой вопрос:

Как понять, что новая версия действительно работает, прежде чем её увидят все пользователи?

Представим ситуаци: мы подготовили новую версию приложения, протестировали её на staging, прогнали автотесты, проверили линтеры и убедились, что CI/CD pipeline светится зелёным. Казалось бы, можно выкатывать.

Проблема в том, что продовый контур — это всегда другая среда. Только там появляются реальные пользовательские сценарии, реальные объёмы трафика, неожиданные запросы и те самые баги, которые никогда не воспроизводятся на тестовом стенде.

Именно поэтому даже Blue/Green Deployment не гарантирует отсутствие проблем. Да, откат будет быстрым, но первые пользователи всё равно столкнутся с ошибками, если новая версия окажется неудачной.

Canary Deployment предлагает более осторожный подход. Вместо того чтобы переключать весь трафик на новую версию сразу, мы отправляем на неё лишь небольшую часть запросов. Например, сначала 5%, затем 10%, 25%, 50% и только после этого 100%.

Схематично это выглядит так:

           ┌─── v2 (5%)
           │
Load Balancer
           │
           └─── v1 (95%)

Если новая версия работает нормально, доля трафика постепенно увеличивается. Если появляются проблемы — трафик возвращается на старую версию, а релиз останавливается.

Название стратегии пришло из горнодобывающей практики XIX века. Перед спуском в шахту рабочие брали с собой клетку с канарейкой — и несли её вместе с собой вглубь. Если в шахте скапливались опасные газы, прежде всего угарный газ или метан, птица теряла сознание или погибала раньше, чем концентрация становилась смертельной для человека. Это давало шахтёрам несколько минут, чтобы выбраться на поверхность.

В мире разработки роль такой канарейки выполняют первые пользователи новой версии: они первыми сталкиваются с возможными проблемами, давая команде время среагировать до того, как ошибка затронет всю аудиторию.

Главное преимущество Canary Deployment заключается в том, что потенциальная ошибка затрагивает не всю аудиторию, а лишь её небольшую часть. Если после релиза внезапно вырастет количество ошибок, увеличится время ответа или начнутся проблемы с потреблением ресурсов, последствия будут значительно менее болезненными.

Представим, что после выката нового релиза выяснилось, что один из запросов создаёт утечку памяти. При Blue/Green Deployment проблема сразу окажется у всех пользователей. При Canary её сначала увидят только несколько процентов аудитории, а мониторинг успеет зафиксировать аномалию до полного переключения трафика.

Если мы отправили 5% пользователей на новую версию, необходимо понимать:

  • выросло ли количество ошибок,

  • изменилась ли задержка ответа,

  • увеличилось ли потребление CPU или памяти,

  • появились ли новые исключения в логах,

  • ухудшились ли бизнес-метрики.

Реализовать Canary Deployment можно несколькими способами. Самый простой вариант — использовать возможности балансировщика нагрузки. Например, в HAProxy можно задать разный вес backend-серверов:

backend app
  server app_v1 10.0.0.1:8080 weight 95 check
  server app_v2 10.0.0.2:8080 weight 5 check

Тогда примерно 5% запросов будут направляться на новую версию приложения.

Другой распространённый подход — использование service mesh решений вроде Istio или Linkerd, где процент распределения трафика задаётся декларативно через VirtualService и DestinationRule:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: app
spec:
  host: app
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: app
spec:
  hosts:
    - app
  http:
    - route:
        - destination:
            host: app
            subset: v1
          weight: 95
        - destination:
            host: app
            subset: v2
          weight: 5

Когда нужно увеличить долю трафика, достаточно изменить weight в VirtualService — без перезапуска подов и без ручного управления репликами. Istio также умеет анализировать метрики через интеграцию с Prometheus и запускать автоматический rollback при превышении порогов ошибок. В Kubernetes без service mesh нативный Canary требует ручного управления репликами между двумя Deployment, что неудобно. Поэтому чаще используют Argo Rollouts или Flagger — они берут на себя постепенное смещение трафика, анализ метрик и автоматический откат.

Однако сложность Canary Deployment заметно выше по сравнению с Rolling Update и Blue/Green. Появляются требования к мониторингу, анализу метрик, маршрутизации трафика и автоматизации процесса доставки. Для небольшого сервиса такие затраты могут оказаться неоправданными. Тем не менее именно Canary сегодня считается одной из самых безопасных стратегий деплоя для высоконагруженных систем. Вместо того чтобы доверять тестам и надеяться на лучшее, команда получает возможность наблюдать поведение новой версии на реальных пользователях, постепенно расширяя аудиторию только после подтверждения её стабильности.

Traffic Mirroring (Shadow Deployment) — когда новая версия получает трафик, но не отвечает пользователям

До этого мы рассматривали стратегии, в которых новая версия приложения постепенно начинает обслуживать реальных пользователей. Но возникает логичный вопрос: можно ли проверить работу новой версии на настоящем трафике так, чтобы ни один пользователь её даже не увидел?

Для этого существует Traffic Mirroring, который также часто называют Shadow Deployment. Идея довольно проста. Пользовательский запрос отправляется в текущую версию приложения как обычно, а его копия параллельно направляется в новую версию сервиса. Схематично это выглядит так:

                ┌────── Production v2
                │
User ──► Proxy ─┤
                │
                └────── Shadow v3

При этом ответ пользователю всегда возвращает только реальная версия. Ответ shadow-инстанса не возвращается пользователю, а логируется или сравнивается с ответом продовой версии.

Для пользователя ничего не меняется:

  • он получает ответ от стабильной версии,

  • бизнес-логика работает как раньше.

Но новая версия приложения уже получает реальные запросы и обрабатывает их в реальных условиях. По сути, это максимально безопасный способ проверить новую версию перед полноценным релизом. Представим, что мы переписали часть сервиса с Python на Go или заменили старую ORM на новую. Локальные тесты проходят успешно, staging тоже выглядит хорошо, но остаётся главный вопрос: как система поведёт себя под реальной нагрузкой?

Shadow Deployment раскрывает свой потенциал в связке с компонентом сравнения ответов (diffing). Рядом с прокси ставится сервис, который сопоставляет ответы прода и shadow версий и записывает расхождения в логи или метрики. Именно это позволяет поймать регрессии до того, как новая версия увидит реальных пользователей.

На практике реализация может выглядеть довольно просто. Например, Nginx поддерживает зеркалирование запросов через модуль mirror:

location / {
    mirror /shadow;
    proxy_pass http://production;
}

location /shadow {
    internal;
    proxy_pass http://new-version;
}

Важный нюанс: Nginx не ждёт ответа от shadow-бэкенда. Если shadow упал или отвечает медленно, пользователь этого не почувствует. Это хорошо с точки зрения безопасности, но означает, что таймауты shadow-инстанса нужно настраивать отдельно. Если инфраструктура построена на Kubernetes, mirroring удобнее настраивать через service mesh — например, Istio. Там это делается декларативно, и процент зеркалируемого трафика задаётся одной строкой конфигурацией.

Однако у mirroring есть важные ограничения. Первое и самое очевидное — нагрузка. Фактически каждый запрос начинает выполняться два раза. Если обрабатывается 10 000 RPS, то shadow-система должна быть готова принять те же самые 10 000 RPS. На практике необязательно зеркалировать весь трафик сразу. Можно начать с 1–10% запросов — это одновременно снижает нагрузку и уменьшает риск побочных эффектов. В Nginx частичное зеркалирование реализуется через split_clients

Второе ограничение связано с побочными эффектами. Представим запрос:

POST /api/payment

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

  • провести платёж второй раз,

  • отправить повторное письмо,

  • создать дубликат заказа,

  • записать лишние данные в БД.

Поэтому полноценный Shadow Deployment обычно требует специальной подготовки:

  • отключения внешних интеграций,

  • использования тестовых очередей,

  • записи данных в отдельные БД,

  • фильтрации определённых типов запросов.

Какую стратегию выбрать?

Универсального ответа нет — выбор всегда зависит от конкретного сервиса. Но есть несколько ориентиров. Если посмотреть на все стратегии по степени риска, получится интересная последовательность:

Recreate
    ↓
Rolling Update
    ↓
Blue/Green
    ↓
Traffic Mirroring
    ↓
Canary Deployment

Каждый следующий этап требует больше инфраструктуры и автоматизации, но позволяет всё раньше обнаруживать проблемы и всё меньше рисковать. Если сервис внутренний и простой в пару минут не критичен — Recreate вполне достаточно. Если нужна доступность без лишней инфраструктуры — Rolling Update закроет большинство задач. Когда на первом месте скорость отката — Blue/Green. Когда важна уверенность в новой версии на реальном трафике — Canary. Ну а Shadow Deployment — инструмент для особых случаев: миграции критичных систем, переписывания под нагрузкой, проверки производительности.