Выпуск изменений фронтенда без поломок

Вы только что запушили новую версию фронтенда. Сборка прошла, тесты зелёные, пайплайн деплоя говорит «успех». Но когда вы смотрите на метрики продакшена, что-то не так. Некоторые пользователи видят сломанную вёрстку. Другие сообщают, что кнопка не работает. А несколько пользователей всё ещё сидят на старой версии, потому что их браузер не подтянул новые файлы.

Это реальность фронтенд-релизов. В отличие от бэкенд-сервисов, где можно перезапустить сервер и мгновенно переключить версии, фронтенд-код живёт в браузере пользователя. Вы не можете заставить каждого перезагрузить страницу. Вы не контролируете, когда истечёт их кеш. И если что-то пошло не так, вы не можете просто нажать кнопку отката и ожидать, что все сразу получат исправление.

Почему фронтенд-релизы — это другое

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

Для статического фронтенда, раздаваемого через CDN, сложность в том, что вы деплоите не на сервер, который контролируете. Вы загружаете файлы в бакет и позволяете CDN их распространять. CDN может отдавать старые файлы одним пользователям и новые — другим, в зависимости от заголовков кеша и поведения граничных узлов.

Для серверного рендеринга (SSR) проблема иная. Вы имеете дело с реальными серверами, которые нужно обновлять без разрыва активных соединений. Пользователь, заполняющий форму или переходящий между страницами, не должен терять сессию из-за смены версий.

Следующая блок-схема поможет выбрать правильную стратегию релиза в зависимости от типа фронтенда и допустимого риска:

flowchart TD A[Тип фронтенда?] --> B[Статический] A --> C[SSR] B --> D[Staged rollout через CDN] D --> E[Мониторинг ошибок] E --> F{Ошибок мало?} F -->|Да| G[Увеличить трафик] F -->|Нет| H[Откат: переключить указатель] G --> I[100% rollout] C --> J{Допустимый риск?} J -->|Низкий| K[Canary release] J -->|Очень низкий| L[Blue-green deployment] K --> M[Развернуть на нескольких инстансах] M --> N[Мониторинг здоровья] N --> O{Всё хорошо?} O -->|Да| P[Постепенно добавлять инстансы] O -->|Нет| Q[Удалить новые инстансы] L --> R[Развернуть green-окружение] R --> S[Переключить балансировщик] S --> T{Проблемы?} T -->|Да| U[Вернуться на blue] T -->|Нет| V[Оставить green]

Staged Rollouts для статического фронтенда

Самый безопасный способ выкатить статический фронтенд — контролировать, сколько пользователей видят новую версию. Большинство CDN поддерживают взвешенное распределение, когда можно направить процент запросов на новые файлы, а остальные — на старые.

Вот как это обычно работает:

  1. Загрузите новую версию в бакет с уникальным путём, например app-v2/ или используйте имена файлов с хешем содержимого.
  2. Настройте CDN так, чтобы 5% запросов шли к новым файлам.
  3. Мониторьте частоту ошибок, время загрузки страниц и сообщения пользователей о проблемах.
  4. Если всё хорошо, постепенно увеличивайте процент до 25%, затем до 50% и наконец до 100%.

Также можно использовать feature flags на стороне клиента. Отдавайте всем одинаковую оболочку приложения, но условно загружайте новые компоненты или функции на основе куки, параметра URL или ID пользователя. Это даёт более тонкий контроль над тем, кто что видит, без необходимости разделения трафика на уровне CDN.

Ключевое преимущество staged rollout — вы можете остановиться в любой момент. Если частота ошибок резко выросла на 10%, вы снижаете процент до нуля. Пользователи, уже загрузившие новую версию, всё ещё могут видеть проблемы, но радиус поражения ограничен.

Canary Releases для SSR-фронтенда

Для SSR-фронтенда staged rollout работает почти как canary-деплой на бэкенде. Вы запускаете несколько инстансов приложения за балансировщиком нагрузки. Когда нужно выкатить новую версию, вы деплоите её на один-два инстанса, а остальные продолжают работать со старой.

Балансировщик настраивается так, чтобы направлять небольшой процент трафика на новые инстансы. Если проверки здоровья не проходят или частота ошибок растёт, эти инстансы убираются из ротации. Откат сводится к остановке новых инстансов и возврату всего трафика на старые.

Этот подход хорошо работает, потому что SSR-приложения хранят состояние на сервере. Сессия пользователя привязана к конкретному инстансу, поэтому нужно убедиться, что данные сессии хранятся в общем месте — например, в Redis или базе данных. Иначе переключение трафика между версиями приведёт к выходу пользователей из системы.

Чтобы автоматизировать этот процесс, можно определить canary rollout в конфигурации деплоя. Вот пример с Argo Rollouts на Kubernetes:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: frontend-ssr
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100
      analysis:
        templates:
          - templateName: success-rate
        startingStep: 0
        args:
          - name: service-name
            value: frontend-ssr
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: error-rate
      successCondition: result < 0.01
      provider:
        prometheus:
          query: |
            sum(rate(http_requests_total{status=~"5.."}[5m])) /
            sum(rate(http_requests_total[5m]))

Эта конфигурация направляет 10% трафика на новую версию, ждёт 5 минут для мониторинга частоты ошибок, затем переходит к 50% и наконец к 100%, только если частота ошибок остаётся ниже 1%.

Blue-Green Deployments для нулевого downtime

Если нужно гарантировать, что ни один пользователь не испытает downtime во время релиза, blue-green deployment — надёжный выбор. Вы поддерживаете два идентичных окружения: blue (текущая версия) и green (новая версия). Весь трафик идёт на blue. Когда green готов и проходит все проверки здоровья, вы переключаете балансировщик так, чтобы весь трафик пошёл на green.

Если после переключения что-то пошло не так, вы возвращаетесь на blue. Весь процесс занимает секунды, и пользователи не замечают перерыва, если состояние сессии обработано правильно.

Оборотная сторона — нужно вдвое больше инфраструктуры. Для небольших команд или приложений с низким трафиком это может быть избыточно. Но для высоконагруженных продакшен-систем, где даже несколько секунд простоя стоят денег или доверия, blue-green оправдывает затраты.

Настоящая проблема: откат статического фронтенда

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

Решение — никогда не перезаписывать файлы. Каждая версия должна иметь уникальный путь, обычно на основе хеша содержимого. При деплое вы загружаете новые файлы рядом со старыми. CDN отдаёт ту версию, на которую указывает приложение. Чтобы откатиться, вы меняете указатель с новой версии на старую. Пользователи, у которых уже закеширована новая версия, продолжат её использовать до очистки кеша, но новые пользователи и те, кто обновит страницу, получат старую версию.

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

Практический чек-лист для фронтенд-релизов

Перед тем как запушить следующий релиз, пробегитесь по этому короткому чек-листу:

  • Все пути к файлам уникальны для каждой версии (хеш содержимого или версионирование)?
  • Можете ли вы направить небольшой процент трафика на новую версию?
  • Есть ли у вас способ мониторить частоту ошибок и время загрузки страниц в реальном времени?
  • Ваш план отката протестирован, а не просто задокументирован?
  • Для SSR: хранится ли состояние сессии вне сервера приложения?
  • Для статики: есть ли механизм переключения указателя версии без повторного деплоя?

Что важнее всего

Staged rollout и стратегии отката — это не функции, которые можно добавить после того, как пайплайн построен. Их нужно проектировать в процесс деплоя с самого начала. Если ваш пайплайн умеет только деплоить одну версию в одно место, вы будете бороться с canary-релизами или blue-green. Если у вас нет управления трафиком на уровне CDN или балансировщика, у вас нет способа ограничить влияние плохого релиза.

Цель — не предотвратить все проблемы. Проблемы будут. Цель — сделать так, чтобы при возникновении проблемы вы могли отреагировать без паники. Хорошо спроектированная стратегия релиза даёт вам контроль над радиусом поражения, чёткий путь отката и уверенность, чтобы выкатывать изменения часто. Без неё каждый релиз — это игра в рулетку.