
При построении релизного цикла приходится балансировать между двумя крайностями. С одной стороны, хочется по максимуму отлавливать баги и дотошно проверять каждый релиз. С другой — быстрее выпускать обновления и приносить клиентам больше пользы.
Мы в Mindbox долго решали это противоречие и, кажется, добились баланса: презентуем несколько релизов в день и каждый проходит несколько этапов проверки, чтобы баги не дошли до клиентов. Пайплайн удобен и для команды: больше сотни разработчиков работают одновременно и не блокируют действия друг друга, а обновления происходят автоматически.
О том, какой путь проходит релиз и какие инструменты обеспечивают его надежность, расскажет engineering manager Mindbox, Бадал.
Путь релиза: от задачи до раскатки на клиентов
В Mindbox релизы раскатываются поэтапно.
1-й этап. Работа в ветке
На этапе CI релиз проходит три этапа. Первый — работа в ветке.
В Mindbox принято под каждую задачу или даже под подзадачу заводить отдельную ветку. У такого правила несколько причин:
небольшие пул-реквесты проще ревьюить;
изменения более предсказуемые;
если обновление ломает основную версию приложения, часть с ошибкой проще локализовать.
Все это позволяет быстрее доставлять ценность на продакшн.
Ветка создается из актуальной версии main-ветки, после чего разработчик пишет код — здесь все стандартно, не будем на этом останавливаться.
2-й этап. Сборка релиза и тесты
Для каждого мерджа в main-ветку создаем релиз, который затем деплоится на продакшн. Соответственно, каждое изменение в коде хорошо бы протестировать.
Тестировать каждый релиз вручную слишком трудозатратно: в компании около ста разработчиков, в основном репозитории продукта в день мерджится примерно 20 пул-реквестов. Если делегировать эту задачу, понадобится несколько десятков тестировщиков. Вместо этого мы остановились на автотестах, которые пишут сами разработчики.
Сборка релиза из ветки и его проверка запускаются после создания пул-реквеста, а также после каждого нового коммита в этом пул-реквесте: приложение и база поднимаются в докере, запускаются тесты. Мы используем GitHub Actions, но в других хранилищах кода есть альтернативные решения — главное, чтобы сервис мог запускать шаги проверки при сборке.
На этапе CI прогоняем unit-тесты и специфические чеки. Например, проверяем:
GraphQL-схемы на обратно несовместимые изменения;
отсутствие кириллицы в коде;
наличие переводов по добавленным ключам локализаций.
После зеленых чеков разработчик приглашает коллег на ревью.
3-й этап. Ревью
Почти каждый релиз проходит этап ревью. В критичных репозиториях настроена политика: мерджить без апрува нельзя. В некритичных — следуем командным соглашениям. Мы в команде договорились, что без ревью не мерджим пул-реквест. Мердж без ревью — скорее исключение, к которому прибегаем, когда изменение незначительное.
У нас действует правило кросс-ревью — любой может ревьюить любого. Когда пул-реквест готов к проверке, разработчик призывает коллег посмотреть его. Заглянуть может любой желающий, в том числе кто-то из джуниоров: лишняя пара глаз всегда полезна, к тому же бывает, что менее опытный разработчик знает доменную область лучше более опытного коллеги. Кросс-ревью помогает не ждать конкретных экспертов и обмениваться знаниями внутри команды.
На этапе ревью смотрим не только на качество кода, но и на тесты: достаточно ли их, тестируется ли критическая логика. Если тестов недостаточно, релиз не получит апрув — тогда разработчик уйдет дописывать.
Когда релиз получил апрув, разработчик мерджит изменения в main-ветку.
4-й этап. Мердж в main-ветку
После мерджа ветки в main собирается итоговый релиз. При этом этап сборки и тестов запускается по новой. Дополнительная проверка помогает избежать конфликтов и ошибок в main-ветке. Например, в ветке разработчика устаревшая версия кода — за время, пока он работал, main изменили. По отдельности оба изменения работают, но вместе конфликтуют — релиз красный.
Когда мы убедились, что релиз собирается и прошел все проверки, начинается этап раскатки на клиентов — этап CD.
5-й этап. Раскатка на тестовые сайты — staging-окружение
Несмотря на то что мы уже проверили релиз и прогнали тесты, раскатывать его сразу на всех клиентов небезопасно. Чтобы не пропустить баги, мы постепенно раскатываем обновление на промежуточные окружения.
В Mindbox есть несколько десятков микросервисов под каждый продукт или его часть: CDP, рассылки, программа лояльности, мобильные пуши, триггерные механики и т. д. У каждого микросервиса своя команда разработки, которая управляет своими релизами на окружения: staging, beta, stable и foreign.

Первое окружение, на которое раскатывается релиз, — staging. Здесь нет клиентов, только тестовые сайты. Это аналоги реальных клиентских проектов, которые мы создали самостоятельно. Они лишь дублеры: в них запущены тестовые механики, которые работают с тестовыми пользователями.
На этом этапе продакт-менеджеры подключаются к приемке задач: вручную проверяют, что изменения работают так, как они задумали.
Также, пока релиз находится на staging, запускаются автоматические тесты: интеграционные и end-to-end.
Интеграционные тесты. Когда мы выпускаем микросервис или монолит, обязательно нужно проверить, как он взаимодействует с остальными ключевыми частями продукта.
Например, при выкладке микросервиса триггерных механик запускается тест, который проверяет распространенный бизнес-сценарий. Тест вызывает интеграцию, которая настроена на тестовом проекте и доступна по публичному API. При этом в системе генерируется событие. На это событие подписана тестовая триггерная механика, которая должна обработать это событие и переместить клиента, для которого вызвали интеграцию, из одного сегмента в другой — именно это перемещение тест и проверяет.
End-to-end-тесты. End-to-end-тесты при прогоне занимают много времени: один тест может длиться минуту-две. Для ускорения запускаем их параллельно. Но если тестов будет слишком много, это тормозит весь релизный цикл. Поэтому мы стараемся прогонять через end-to-end-тесты только самые критичные пользовательские пути — выстроить приоритеты помогают продакт-менеджеры.
Например, для пользователей критично, чтобы настроенная триггерная механика сохранялась с первого раза. Для этого пишем тест, который имитирует работу маркетолога: вот он заходит на страницу редактирования, перетаскивает блок на Canvas, открывает его настройки, заполняет тип события в системе и сохраняет — все должно пройти без ошибок.

Тест создания и сохранения блока события на Cypress
import { QASelectorConstants } from '../../constants';
import { getDefaultNodePosition } from '../utils';
const selectFilterRules = () => {
// Select first trigger in list for save settings and exit
cy.get('.selectR-choice')
.click()
.get('.selectR-results > .selectR-result')
.should('have.length.above', 1)
.get('.selectR-results > .selectR-result:first-child > .selectR-label')
.click();
};
describe('Event block tests', () => {
beforeEach(() => {
cy.visitCreateScenario();
cy.selectBrand();
cy.wait(5000);
});
after(() => {
cy.tryRemoveScenario();
});
it('should be a created and saved', () => {
// Case: block should be creater and saved
cy.dropBlockOnCanvas(QASelectorConstants.block.event, getDefaultNodePosition()).waitBlockSynced().as('blockData');
cy.getBlockFromCanvas({ blockDataAlias: '@blockData' }).click({ force: true });
selectFilterRules();
cy.saveBlockSettings();
cy.removeBlockFromCanvas({ blockDataAlias: '@blockData' });
cy.waitScenarioSaved();
});
});
6-й этап. Beta-окружение — часть клиентов
После тестового окружения релиз почти сразу раскатывается на beta. Здесь собрана часть клиентов, примерно 10%. Стараемся включать в сегмент клиентов с разными наборами модулей, чтобы в случае проблем отлавливать их на раннем этапе в каждом сервисе.
Клиенты из beta-окружения обслуживаются микросервисами из того же окружения. Инстанс основного сервиса-монолита знает из своего конфига, в каком окружении он находится и дискаверит сервисы из своего окружения. Это позволяет в микросервисах выстроить такой же пайплайн и раскатывать изменения плавно. Таким образом, при раскатывании релиза в микросервисах на beta изменения затрагивают клиентов из того же окружения.
7-й этап. Stable-окружение
Затем релиз выкладывается на stable-окружение — сегмент, в который входит большая часть клиентов.
На stable релизы выкладываются по расписанию. Оно настраивается ответственной за сервис командой. Например, в 11:00 и 15:00 каждого буднего дня за исключением пятницы :) В пятницу, как правило, поздние релизы стараемся не делать — редкие баги могут появиться на выходных, когда оперативно их починить не получится.
На этом этапе всплывает наименьшее количество ошибок — все предыдущие этапы были направлены на то, чтобы выложить стабильный проверенный релиз.
8-й этап. Foreign-окружение
Staging, beta и stable — стандартные окружения, которые используются в продуктах с отлаженным циклом релизов. У нас есть еще одно окружение — foreign, в которое входят зарубежные клиенты. Продукт для зарубежных клиентов хостится в облаке за пределами России. Требование о зарубежном облаке следует из законодательных ограничений, например GDPR.
В нашем пайплайне раскатка на foreign происходит после stable.
Инструменты надежного деплоя
Сам по себе постепенный релиз не поможет защитить от всех багов и конфликтов. Чтобы изменения, которые мы делаем для клиентов, были еще надежнее, мы используем несколько инструментов.
Хоттестинг для фронтенда
Релизный цикл фронтенд-репозиториев совпадает с циклом на бэкенде, который описан выше. Однако есть дополнительный этап перед мерджем в main-ветку — хоттестинг.
После открытия пул-реквеста GitHub Action собирает бандл, отправляет его в докер-образ и запускает в Kubernetes. В качестве бэкенда выступают сервисы и база тестового проекта. В пул-реквесте появляется ссылка на хоттестинг-сайт — это позволяет протестировать изменения из пул-реквеста сразу в админке продукта, что ускоряет приемку задачи и делает разработку более надежной.
Features — переключение функционала в рантайме
Конвейер релизов работает по расписанию, и мы не хотим его останавливать. При этом бывают ситуации, когда мы не хотим выкладывать релиз по расписанию: например, ждем приемку — когда продакт проверит новый функционал или разработчик убедится, что его изменение не ломает критически важный функционал.
Чтобы в такой ситуации не останавливать конвейер и не тормозить выпуск новых релизов, мы используем механизм фич. Для этого в самописном сервисе скрываем отображение функционала, который нужно притормозить. Релиз проходит по расписанию, но клиенты не видят скрытого функционала. В момент проверки состояния фичи приложение вызывает через клиент наш сервис, кэширует состояние фичи (например, в Redis) и при дальнейших вызовах использует состояние из кэша. Политика кэширования гибко настраивается с точки зрения хранилища и времени экспирации. Когда продакт проверил функционал и мы готовы включить их для клиентов, разработчик изменит состояние фичи для реальных клиентов, и они смогут начать пользоваться новым функционалом.

Если при включении фичи обнаружилась проблема, с помощью Features мы можем в рантайме откатить поведение сервиса. Для этого достаточно зайти в админку Features и переключить состояние фичи: новое состояние появится через пару минут, когда сбросится кэш.
Пример объявления фичи:
public class MyFeatureComponent : FeatureComponent
{
public Feature MyNewFeature { get; }
public NexusFeatureComponent(FeatureServices featureStateStorage)
: base(featureStateStorage)
{
MyNewFeature = Add("MyNewFeature");
}
}
Использование фичи в коде:
private readonly MyFeatureComponent _myFeatureComponent;
public MyClass(MyFeatureComponent myFeatureComponent)
{
_myFeatureComponent = myFeatureComponent;
}
public async Task MyMethod()
{
if (await _myFeatureComponent.MyNewFeature.IsEnabledAsync())
{
// включить новый функционал
}
}
«Маринад» — выдержка релиза перед деплоем
Есть ошибки, которые всплывают только через какое-то время или имеют накопительный эффект. Например, утечка памяти: выкладываем релиз, на первый взгляд все хорошо, но через час начинаются проблемы — приложение периодически падает с OutOfMemoryException и перезапускается. Либо проблема в функционале, которым редко пользуются, и не сразу замечают баг.
Чтобы минимизировать такие ошибки, мы искусственно замедляем конвейер и откладываем раскатывание релиза на всех клиентов — мы называем это «маринадом» ?:)
Каждая команда настраивает «маринад», как посчитает нужным. В одном из наших сервисов это выглядит следующим образом:
Релиз выкатывается на staging-окружение.
Автоматика выжидает полчаса — за это время мониторинг может отловить разные проблемы.
Создается релиз для beta-окружения и выкатывается на него.
На beta выжидаем еще два часа.
Создается релиз для stable-окружения и выкатывается на него.
Пока релиз «маринуется», мы отлавливаем накопительные и неочевидные проблемы на примере небольшого сегмента клиентов или даже на тестовом окружении.
Canary deployment — плавная раскатка релиза
Мы не переключаем сразу весь трафик на проверенный и «промаринованный» релиз, а делаем это плавно. Постепенный деплой уменьшает возможный ущерб: если баг все же пролез, мы заметим и остановим его до того, как обновление распространится на всех клиентов.
Для работы canary deployment необходима метрика, которая будет анализироваться в момент выкладки, чтобы убедиться, что с сервисом все хорошо. Иногда разработчики не задумываются, какая метрика является показательной для работы сервиса, а canary deployment заставляет задуматься и сформулировать ее.
Недостаток этого подхода — долгий деплой релиза. Зависит от сервиса и настроенной политики, но в среднем время релиза увеличивается с 5 до 20–30 минут.
Вся работа автоматизирована:
В момент деплоя рядом с текущим работающим сервисом поднимается его новая версия.
На новую версию перенаправляется часть трафика (например, 10%), основная часть трафика все еще работает на старой версии.
Выжидаем 5 минут. В это время следим за метрикой сервиса, например apdex или error rate.
Если метрика показывает, что начались проблемы, весь трафик переключается на старую версию, а текущий деплой отменяется.
Если все хорошо, повторяем шаги 2–3, чтобы поэтапно переключить оставшийся трафик.
После полного переключения трафика на новый релиз сервисы с предыдущей версией отключаются.
Rollback — откат и блокировка деплоя
Если критичный баг все-таки доходит до клиентов — его необходимо срочно устранить. Для этого может использоваться rollback.
Rollback — это ручной откат до предыдущей версии сервиса. При этом отменяются все текущие деплои, выкладывается предыдущий релиз. Также блокируется деплой — это необходимо, чтобы проблемный релиз случайно снова не выкатился на клиентов.
Недостаток отката обновления в том, что он тормозит разработку и поставку ценностей: пока одна команда исправляет свои изменения, другие команды ждут ее и не могут запустить релиз. Чтобы восстановить работу конвейера, необходимо пройти все этапы CI/CD и докатить релиз, в котором устранены ошибки. После этого можно разблокировать деплой, чтобы он снова пошел по расписанию.
Раздельный пайплайн миграций и кода
Мы используем раздельные пайплайны для миграции базы данных и кода. К этому мы пришли после инцидента на stable. Нельзя было сделать rollback, потому что в этот релиз попали миграции базы данных. В то время пайплайн запрещал делать rollback, если в релизе есть миграции, так как автоматически откатить код на предыдущую версию можно, а вот изменения в базе данных — нельзя, нужно писать скрипт.
Запрет rollback при наличии миграций — слишком жесткое правило. Не каждая миграция базы данных несовместима с предыдущей версией кода — иногда можно откатить код на предыдущий релиз, а базу не откатывать. Например, если добавили новую колонку, но в коде ее еще не используют.
После этой истории мы разделили пайплайны миграций базы и кода. Сначала всегда идет деплой, который накатывает скрипты миграций, затем — релиз с новым кодом.
Теперь при выкладке нового кода всегда есть возможность сделать rollback. Если в релизе есть миграции, всплывет предупреждение: «Убедись, что миграции обратно совместимы». Проверка миграций на обратную совместимость — ручная работа, всегда остаются риски. Поэтому кнопка подтверждения доступна лишь опытным разработчикам из команды поддержки пайплайна и архитекторам.
Еще одно преимущество раздельного пайплайна миграций и кода — явное декларирование того, что миграция должна быть совместима с версией кода, которая сейчас выложена на окружение.
Hotfix — экстренное исправление проблемы
Если предыдущий релиз не сможет работать с текущей версией базы, то делать rollback нельзя. В таком случае остается лишь делать hotfix.
Hotfix — это внеочередной релиз, который устраняет проблемы предыдущего. Чтобы сделать hotfix, разработчик создает новую ветку, вносит изменения, которые устраняют ошибку. Этот релиз проходит все этапы — от тестов на этапе CI до раскатки на необходимое окружение, но в ускоренном формате — не дожидаемся раскатки по расписанию, не проводим «маринад», можем выкатить сразу на необходимое окружение.
Hotfix используем, если нет возможности сделать rollback и ошибка критичная. Ведь пока мы готовим и катим новый релиз стандартным способом, клиенты страдают. А hotfix сокращает время решения проблемы.
Шпаргалка: практики для надежного цикла релиза
Отдельные ветки для каждой задачи. Так вы упростите ревью, сделаете изменения предсказуемыми и сможете легко локализовать ошибки.
Автоматизированное тестирование каждого релиза. Используйте unit-тесты и прочие проверки — это упростит и ускорит процесс.
Ревью с обязательным апрувом и кросс-проверкой. Это делает релиз более надежным и помогает обмениваться знаниями.
Staging для приемки нового функционала. Выделите этап и тестовые проекты, чтобы проводить интеграционные и end-to-end-тесты перед раскаткой на клиентов.
Beta-окружение с реальным сегментом клиентов, чтобы получать ранний фидбэк от реальных клиентов.
Выкладка релизов по расписанию — это минимизирует ручную работу.
Отдельное окружение для зарубежных клиентов, чтобы соответствовать международным законодательным требованиям.
Хоттестинг и features для безопасности и контроля над процессом релиза.
Время на «маринад» релиза для отлова накапливающихся или неочевидных ошибок.
Canary deployment для критически важных сервисов, чтобы переключать весь трафик лишь на стабильный релиз.
Rollback, чтобы при критичных ошибках быстро восстановить работу сервисов.
Hotfix, чтобы срочно устранить ошибки, минуя стандартный релизный цикл.