Pull to refresh
128.11
Mindbox
Автоматизация маркетинга

Релизный цикл без компромиссов: надежно для клиентов, удобно для разработки

Level of difficultyMedium
Reading time11 min
Views3.1K
Релизный цикл в 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, чтобы срочно устранить ошибки, минуя стандартный релизный цикл.

Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments5

Articles

Information

Website
www.mindbox.ru
Registered
Employees
201–500 employees
Location
Россия