Релизный цикл в Mindbox
Релизный цикл в Mindbox

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

Мы в 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 может пользоваться любой сервис в компании

Если при включении фичи обнаружилась проблема, с помощью 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 и перезапускается. Либо проблема в функционале, которым редко пользуются, и не сразу замечают баг.

Чтобы минимизировать такие ошибки, мы искусственно замедляем конвейер и откладываем раскатывание релиза на всех клиентов — мы называем это «маринадом» ????:)

Каждая команда настраивает «маринад», как посчитает нужным. В одном из наших сервисов это выглядит следующим образом:

  1. Релиз выкатывается на staging-окружение.

  2. Автоматика выжидает полчаса — за это время мониторинг может отловить разные проблемы.

  3. Создается релиз для beta-окружения и выкатывается на него.

  4. На beta выжидаем еще два часа.

  5. Создается релиз для stable-окружения и выкатывается на него.

Пока релиз «маринуется», мы отлавливаем накопительные и неочевидные проблемы на примере небольшого сегмента клиентов или даже на тестовом окружении.

Canary deployment — плавная раскатка релиза

Мы не переключаем сразу весь трафик на проверенный и «промаринованный» релиз, а делаем это плавно. Постепенный деплой уменьшает возможный ущерб: если баг все же пролез, мы заметим и остановим его до того, как обновление распространится на всех клиентов. 

Для работы canary deployment необходима метрика, которая будет анализироваться в момент выкладки, чтобы убедиться, что с сервисом все хорошо. Иногда разработчики не задумываются, какая метрика является показательной для работы сервиса, а canary deployment заставляет задуматься и сформулировать ее.

Недостаток этого подхода — долгий деплой релиза. Зависит от сервиса и настроенной политики, но в среднем время релиза увеличивается с 5 до 20–30 минут.

Вся работа автоматизирована:

  1. В момент деплоя рядом с текущим работающим сервисом поднимается его новая версия.

  2. На новую версию перенаправляется часть трафика (например, 10%), основная часть трафика все еще работает на старой версии.

  3. Выжидаем 5 минут. В это время следим за метрикой сервиса, например apdex или error rate.

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

  5. Если все хорошо, повторяем шаги 2–3, чтобы поэтапно переключить оставшийся трафик. 

  6. После полного переключения трафика на новый релиз сервисы с предыдущей версией отключаются. 

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, чтобы срочно устранить ошибки, минуя стандартный релизный цикл.

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


  1. DesertEagle
    30.11.2023 05:41
    +2

    Здравствуйте.

    Rollback — это ручной откат до предыдущей версии сервиса.

    Можно и не на предыдущую. После того, как в моей практике был случай, что решили править хотфиксом, а он не помог/сломал сильнее, а откатиться можно было только на предыдущую версию, то там для нас роллбэк готовил два стула, а точнее один, но с эффектами обоих.

    После этого я переписал пайплайны выкатки так, чтобы количество ревизий было 10(на всякий случай, но можно и 5). Есть/пить они не просят, а вот откатиться после неудачных хотфиксов можно было далеко назад.

    Ну и переписал пайплайны на роллбэк так, чтобы можно было выбрать на сколько ревизий откатиться. По умолчанию можно жать на кнопку, и он стандартно откатит на предыдущую ревизию.

    По окружениям: если со stage все понятно (крутятся e2e, интеграционные тесты), то на этапе чтения про

    beta, stable и foreign

    Возник вопрос, а, может, canary закроет потребности? Так и canary на месте. Не выходит ли это слишком дорого? Почему canary в данном случае не может закрыть вопрос 3х предыдущих окружений?


    1. strayker1206 Автор
      30.11.2023 05:41
      +1

      Здравствуйте и спасибо за вопросы!
      По поводу роллбэка - вы всё верно написали! Сломать хотфиксом и получить два стула - легко :) Возможность откатиться на более поздние релизы у нас имеется, причём тоже на 10 последних.

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

      Мы регулярно отщипляем микросервисы от монолита и так же движемся в сторону мультитенантного единого процесса монолита, в следующем году планируем некоторые деплоймент группы сделать едиными. Перевести весь монолит на единый процесс - долгий путь, который нам предстоит.

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

      Третьей причиной вижу то, что staging окружение не способно отловить проблемы больших нагруженных проектов и мы рискуем угодить в ситуацию, когда на стейдже всё ок, а при деплое на прод canary частенько будет откатываться, блокировать деплой - и все вытекающие последствия.

      Что касается десятка других микросервисов, там действительно в критических сервисах применяем canary - например, публичный API Gate, сервис триггерных механик.


      1. DesertEagle
        30.11.2023 05:41

        Таким образом, canary становится достаточно дорогим удовольствием

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

        Возможно, существуют решения, которые в этом способны помочь. Например, у нас схожая архитектура, и я приглядел полноценный кенари вариант с Argo Rollouts (не смотрел, можно ли ссылки, поэтому без них). Но его только собираемся внедрять.

        staging окружение не способно отловить проблемы больших нагруженных проектов

        Здесь подразумевается, что на больших данных непредсказуемо поведение, потому что стейдж != прод по количеству и качеству данных, или что именно сам фактор нагрузки может повлиять (например, лавинообразный рост трафика)?

        Спасибо за ответ!


  1. Tarasov-Front-Dev
    30.11.2023 05:41

    Очень интересно было прочитать, спасибо! Я бы сказал, что это хороший задел для выступления на конференции. Был на конфе в Альфа месяц назад, ваш материал бы зашёл.


    1. strayker1206 Автор
      30.11.2023 05:41
      +1

      Спасибо за высокую оценку статьи! Про выступление подумаем обязательно :)