Релизный цикл ПО для самых маленьких
В большей степени этот рассказ про микросервисную архитектуру, где отдельные сервисы изолированы. Статья ориентирована на джунов, которые еще не сталкивались с масштабной разработкой.
Многое из того, о чем можно говорить в контексте релизного цикла, регламентируется внутренними правилами компании — как аналитики взаимодействуют с проджект-менеджерами при формулировании задачи, сколько нужно этапов тестирования, какие используются инструменты и т.п. Поэтому мы пройдемся только по самому высокому уровню абстракции, на котором шаги в разных компаниях примерно одинаковы. Но примеры приведем из реальной жизни.
Предыстория
Итак, у бизнеса — назовем его заказчиком, не определяя, где именно он находится, внутри компании или снаружи — появляется некая идея по доработке функционала. Традиционно она проходит через проджект-менеджеров и аналитиков, превращаясь в конкретную фичу для разрабатываемого софта или сервиса и, в конечном итоге, в задачу для команды разработки. Команда может немного подкорректировать задачу с технической точки зрения, после чего берет ее в реализацию.
Как правило, новые фичи разрабатываются в отдельных ветках и лишь после тестирования и ревью эта ветка кода объединяется с мастер-веткой, откуда впоследствии будет собран прод. Эта статья — как раз о том, как до этого прода добраться.
Что такое CI/CD
Предположим, разработчики написали код. Остается доставить его до пользователя в работающем состоянии. Загвоздка в том, что сделать это надо с наименьшими рисками и потерями — чтобы в процессе это можно было мониторить, оповещать кого следует и в случае чего откатиться к последней работающей версии. Особенно это важно, если различных сервисов много, например если удалось достичь дзена в процессе распиливания монолита на микросервисы. Так случилось на одном из наших проектов (здесь и далее примеры будем приводить только из него) — в общей сложности у нас более 70 сервисов и если бы мы тратили большое количество времени на раскатывание новых версий, то на все остальное ресурсов бы уже не хватило.
Процесс, который придумали для решения этой задачи сводится по сути к двум аббревиатурам:
- CI (Continuous Integration) — непрерывная интеграция — автоматическая проверка кода при внесении постоянных небольших изменений;
- CD (Continuous Delivery) — непрерывная доставка — автоматическое развертывание этого кода.
Как правило, проверкой и развертыванием кода в крупных проектах занимаются уже не разработчики, а отдельная команда. Они настраивают непрерывность процесса, чтобы после завершения одного этапа работы над фичей сразу же запускался другой, автоматически создавались необходимые задачи в Jira или ее аналоге, назначались ответственные с соответствующими уведомлениями и т.п. А от разработчика требуется минимальное участие — заявить свой коммит в соответствии со всеми правилами. Ну может быть пару конфигов написать. На большее зачастую и прав доступа-то нет.
При чем тут Kubernetes
Стандарт де факто развертывания — Kubernetes. Им пользуются практически все. Он позволяет не держать пять однотипных серверов для проверки и тестирования написанного кода, а на имеющейся аппаратной архитектуре выделять обособленные «пространства» и экспериментировать уже внутри них.
К примеру, на нашем проекте в Kubernetes реализованы три среды, далее я расскажу о них подробнее:
- Ревью-контур — изменения выкатываются в него автоматически сразу после создания merge-реквеста. По сути это тестовый экспериментальный контур, собранный под конкретную ветку, где можно проверить те вещи, которые невозможно было увидеть локально. Код фичи проходит через линтер, сборку контейнера и деплоится в Kubernetes. В этом контуре осуществляется основное тестирование, поскольку здесь очень просто что-либо исправить и обновить. Бывает, что в нашей инфраструктуре существует несколько экземпляров одного и того же сервиса в ревью-контурах с разной функциональностью.
- Develop-контур — после того, как код вливается в мастер-ветку, он выкатывается в следующий контур. Здесь уже не может быть нескольких экземпляров одного и того же сервиса с разными возможностями. Это всегда более-менее стабильная мастер-версия, которую можно указывать в конфигурациях других сервисов-клиентов. В этом контуре запускается автотестирование, да и в целом финальные тесты перед выкатыванием на продакшн.
- Preproduction-контур — это последний контур перед продакшеном, где все контрагенты уже относительно реальны. По сути это полная эмуляция прода. Тут нет «грязных» данных, есть высокие нагрузки, чтобы команда могла провести последние этапы проверок.
Конфигурации этих пространств можно хранить непосредственно в Kubernetes, а конкретные настройки для каждого отдельного сервиса — в текстовых файлах вместе с кодом или в специальных инструментах, типа Consul. При этом сам факт того, что сервис до прода пройдет через целую цепочку сред, необходимо учитывать на этапе разработки — те переменные, которые могут меняться в средах, следует делать параметризованными, чтобы спокойно задавать через конфиги.
Выкатываемся на прод
В продакшн фичи зачастую отправляются не сразу после окончания тестирования. Перед тем, как новую версию смогут пощупать все пользователи, придется пройти ряд бюрократических процедур. Все это связано с критичностью сервисов и глубиной внесенных изменений. Если сервис действительно критичен или были внесены масштабные изменения, например в схему данных, в момент раскатывания все должны быть наготове, чтобы оперативно отреагировать на возможные нештатные ситуации. А это значит, что релизу предшествует масса коммуникаций. Команды по-разному пытаются их упростить, создавая отдельные каналы во внутренних инструментах общения, продумывая графики релизов с окнами для фич или разрабатывая простые опросники, которые классифицировали бы очередную версию как требующую (или не требующую) пристального надзора. Насколько все это успешно — зависит от конкретной команды и специфики задач.
Как правило, продакшн — это два изолированных пространства в Kubernetes, разнесенные на разные физические ноды. Обслуживает пользователей основное пространство, а второе служит своего рода бекапом. Количество экземпляров сервисов на каждой из нод зависит от ожидаемой нагрузки. Это требует довольно больших ресурсов, которые фактически простаивают большую часть времени. Зато экономит много нервов во время релиза (и помогает продолжать зарабатывать деньги, несмотря ни на что).
Новая версия сначала выкатывается на основное пространство и живет там какое-то время — в нашем случае порядка 40 минут (это время, необходимое на финальные тесты).
Если после обновления основного пространства проявились какие-то проблемы, прод переключается на бекап, где на тот момент еще живет старая версия, а команда функционального сопровождения разбирается, с чем возникли неполадки. Так сервис в любом случае останется работоспособным.
Если же после обновления основного пространства по метрикам, за которыми следит команда функционального сопровождения, все хорошо, новая версия накатывается и на второе. Фактически, на этом заканчивается цикл релиза новой функциональности.
Как долго все это тянется
Обозначить среднее время прохождения по описанному процессу сложно. Доставка кода пользователю включает очень много этапов, на каждом из которых все может пройти быстро или застрять надолго. А кроме того задержать выход в прод могут уже упомянутые выше графики релизов и наличие в них свободных окон для новых фич.
Для понимания порядка величин: несколько дней — это хорошая скорость доставки для крупного проекта. Но зачастую при сложных процессах или в ответственных секторах, где за релизом наблюдает отдельная команда, ресурсы которой не безграничны, новая версия едет до продакшена неделю и более (это уже с учетами нюансов бюрократии). Разработчикам это, конечно, доставляет изрядные неудобства — они уже сделали что-то новое и полезное, допустим, поменяли логирование. Но на продакшене еще пока этого не видят.
Для каких-то срочных изменений — исправления критичных багов, которые все-таки просочились на продакшн — многие команды применяют обходные пути. Тут не до церемоний. Но в нашем случае столько «слоев» предусмотрено как раз для того, чтобы не приходилось в мыле отступать от принятых процессов.
Оптимизация этих процессов — отдельная большая часть выстраивания работы команды. И здесь встречаются самые разные подходы. Кто-то пытается сократить этап тестирования, считая, что выигрыш по скорости окупит возможные баги, просочившиеся на прод. Кто-то копается в деталях системы сборки. И примененные решения зачастую сложно перетащить из команды в команду, потому что все это очень зависит от специфики проекта.
Версии релизов
Отдельный разговор в ракурсе релизного цикла — версионирование. Здесь конкретно мы идем по проторенной другими дорожке. Версия релиза получает мажорную или минорную нумерацию в зависимости от масштаба и критичности изменений. А для определения, насколько они критичны, создан простой опросник, который заполняет разработчик после окончания работы над кодом (менялась ли схема данных, логирование и т.п.). По ответам рассчитывается скоринг релиза и его относят к одной из категорий:
- S — минимальные изменения, как правило, исправление багов (изменение третьей цифры номера версии);
- M — более серьезные изменения, в частности, правка БД (изменение второй цифры номера версии);
- L — действительно масштабные изменения (изменение первой цифры номера версии).
Принципы этого скоринга, как правило, едины либо для большого проекта, либо для всей компании.
Итоговые замечания
Разбиение на среды в Kubernetes, да и в целом детали релизного процесса здорово зависят от специфики проекта. В нашем случае это, во-первых, хоть и огромный проект, но микросервисная архитектура, а не монолит. Во-вторых, отдельные микросервисы — это изолированные инструменты. Поэтому контура получаются довольно простые и друг другу не мешают — сервису с новой фичей, развернутому на ревью-контуре, достаточно стучаться запросами на dev-контур и т.п. При этом не возникает никакой несогласованности данных, поскольку прийти другим сервисом к тем же самым данным нельзя. Это плюс по-настоящему микросервисной архитектуры, где каждый сервис работает со своей БД.
Если бы все наши микросервисы работали с одними и теми же данными или мы бы дорабатывали огромный неповоротливый монолит, структуру сред в Kubernetes пришлось бы усложнять, чтобы разные части команды не мешали друг другу, параллельно работая в разных «углах» проекта. Или пришлось бы прорабатывать какие-то руководства в стиле «как правильно добавить поле в таблицу» с учетом обратной совместимости изменений (об этом мы уже как-то писали).
В целом все эти тонкости, как и будущие настройки сервиса для разных сред, должны учитываться на этапе разработки. Поэтому самому разработчику важно, если не уметь настраивать Kubernetes в деталях, то хотя бы представлять, как выглядит процесс на проекте, где он трудится.
Текст подготовлен по внутреннему докладу Владимира Евтерева, Максилект.
P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на нашу страницу в VK или на Telegram-канал, чтобы узнавать обо всех публикациях и других новостях компании Maxilect.