При построении релизного цикла приходится балансировать между двумя крайностями. С одной стороны, хочется по максимуму отлавливать баги и дотошно проверять каждый релиз. С другой — быстрее выпускать обновления и приносить клиентам больше пользы.
Мы в 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, чтобы срочно устранить ошибки, минуя стандартный релизный цикл.
ссылка на оригинал статьи https://habr.com/ru/articles/777036/
Добавить комментарий