Более того, если вносимое мной в пользовательский сервис незначительное изменение испортит сборку, то, пока неисправность не будет устранена, вносить какие-либо иные изменения в другие сервисы будет невозможно. И подумайте о сценарии, при котором эту гигантскую сборку совместно используют сразу несколько команд разработчиков. Какая из них будет главной?
Разновидность этого подхода (рис. 6.2) предусматривает наличие единого дерева для всего исходного кода и нескольких CI-сборок, отображаемых на части этого дерева. При четко заданной структуре можно без особых усилий отобразить сборки на конкретные части дерева исходного кода. Вообще-то я не сторонник данного подхода, поскольку эта модель может вызывать смешанные чувства. С одной стороны, проверочный или наладочный процесс может упроститься, поскольку беспокоиться приходится только об одном хранилище. С другой — проверка исходного кода сразу для нескольких сервисов может легко войти в привычку, что может так же легко перейти в привычку внесения изменений сразу в несколько сервисов. Но этот подход по сравнению с подходом, предусматривающим создание единой сборки для нескольких сервисов, я считаю предпочтительным.
Рис. 6.2. Единое хранилище исходного кода с подкаталогами, отображаемыми на независимые сборки
А есть ли еще одна альтернатива? Предпочитаемый мною подход предполагает наличие одной CI-сборки для каждого микросервиса, что позволяет быстро вносить изменение и проверять его работоспособность еще до развертывания в производственной среде (рис. 6.3). Здесь у каждого микросервиса имеется собственное хранилище исходного кода, отображаемое на его собственную CI-сборку. При внесении изменения запускается и тестируется только нужная мне сборка. И для развертывания я получаю только один артефакт. Группировка по командной принадлежности также становится понятнее. Кто владеет сервисом, тот владеет и хранилищем и сборкой. Правда, при этом немного усложняется внесение изменений, затрагивающих сразу несколько хранилищ, но я считаю, что с этим справиться намного проще (например, с помощью сценариев командной строки), чем с теми недостатками, которые присущи монолитному процессу управления исходным кодом и сборками.
Рис. 6.3. Использование для каждого микросервиса одного хранилища и одной CI-сборки
Наряду с исходным кодом микросервисов в системе управления версиями должны находиться тесты для конкретного микросервиса, чтобы мы всегда знали, какие тесты для какого конкретного сервиса должны запускаться.
Итак, у каждого микросервиса будут собственное хранилище исходного кода и собственный процесс CI-сборки. Этот процесс в полностью автоматическом режиме будет использоваться и для готовых к развертыванию артефактов. Теперь отвлечемся от CI, чтобы увидеть, как в общую картину вписывается непрерывная доставка.
В самом начале использования непрерывной интеграции мы поняли важность наличия временами внутри сборки нескольких стадий. В качестве весьма распространенного проявления этой особенности можно назвать тесты. Может использоваться множество быстрых, весьма ограниченных по области применения тестов и небольшое количество широкомасштабных медленных тестов. Если запустить сразу все тесты, то в режиме ожидания завершения широкомасштабных медленных тестов можно не дождаться быстрого получения ответного результата на сбой при проведении быстрых тестов. А если быстрые тесты не будут пройдены, то запускать медленные тесты не будет никакого смысла! Решением этой проблемы является использование различных стадий сборки путем создания того, что называется сборочным конвейером. Одна стадия будет для быстрых тестов, а другая — для медленных.
Концепция конвейерной сборки предоставляет отличный способ отслеживания хода обработки программы по мере прояснения ситуации на каждой стадии, помогая выяснить качество программы. Мы осуществляем сборку нашего артефакта, и этот артефакт используется на всем протяжении конвейера. По мере прохождения артефакта через все его стадии растет уверенность в том, что программа будет работать в производственной среде.
Непрерывная доставка (CD) построена на этой и некоторых других концепциях. Как подчеркивается в книге с таким же названием, авторами которой являются Джез Хамбл и Дэйв Фарли (Dave Farley), непрерывная доставка представляет собой такой подход, при котором мы получаем постоянный отклик о производственной готовности абсолютно каждой проверяемой сборки и, кроме того, каждую проверяемую сборку рассматриваем в качестве сдаточной версии.
Чтобы полностью усвоить суть данной концепции, нужно промоделировать все процессы, вовлеченные в перевод программы из проверочного в производственное состояние, и знать, на какой стадии находится каждая заданная версия программы с точки зрения уверенности в ее готовности к выпуску. При применении CD это делается путем расширенного толкования идеи многоступенчатого конвейера сборки для моделирования каждой стадии, через которую должна пройти программа, как в ручном, так и в автоматическом режиме. Простой, возможно, уже знакомый вам конвейер показан на рис. 6.4.
Рис. 6.4. Стандартный процесс выпуска, смоделированный в виде сборочного конвейера
И теперь нам очень нужен инструмент, включающий в себя CD в качестве одной из главных концепций. Мне встречались многие, кто пытался внедриться в CI и заняться ее расширением до получения CD, что часто заканчивалось созданием сложных систем, которые не могли считаться простыми в использовании средствами, встраиваемыми в CD с самого начала. Средства, полностью поддерживающие CD, позволяют определять и визуализировать конвейеры и моделировать весь путь программы к производству. Во время продвижения версии кода по конвейеру, пройдя одну из автоматизированных стадий верификации, она переходит к следующей стадии. Остальные стадии могут запускаться вручную. Например, для моделирования процесса ручного тестирования на приемлемость для пользователя (UAT) при его наличии можно будет воспользоваться CD-средством. Я смогу увидеть следующую доступную сборку, готовую к развертыванию в UAT-среде, осуществить ее развертывание и, если она пройдет ручные проверки, отметить эту стадию как успешно пройденную, позволив двигаться дальше.
Благодаря моделированию всего пути программы к производству мы существенно улучшаем возможность оценить ее качество, а также можем значительно сократить время между выпусками, поскольку наблюдение за процессом сборки и выпуска ведется из одного места и у нас имеется вполне определенный координационный центр для внесения улучшений.
В мире микросервисов, где требуется обеспечить возможность выпуска сервисов независимо друг от друга, получается, что, как и в случае с CI, нужен отдельный конвейер для каждого сервиса. В наших конвейерах находится создаваемый артефакт, который нужно провести по всему пути к производству. Как всегда, оказывается, что артефакты могут иметь разнообразные размеры и формы. Вскоре мы рассмотрим некоторые наиболее распространенные и доступные варианты.
Неизбежные исключения. Как и в каждом хорошем правиле, здесь существуют исключения, также требующие рассмотрения. Подход «один микросервис на каждую сборку» должен стать вашей абсолютной целью, но есть ли случаи, когда имеет смысл воспользоваться чем-то другим? Когда команда приступает к разработке нового проекта, особенно в новом для нее ключе, когда работа начинается с чистого листа бумаги, высока вероятность того, что на пути встретится большая мешанина с точки зрения работы за пределами проложенных границ сервиса. Есть весьма веские причины задавать сервисам в начальной стадии их разработки больший размер, пока не появится понимание того, что они приобрели достаточную стабильность.
Пока все перемешано, высока вероятность внесения изменений, не соблюдающих границ сервисов, а также необходимости частого изменения того, что входит или не входит в сервисы. В этот период может быть целесообразнее содержать все сервисы в одной сборке, чтобы сократить затраты на те изменения, которые не соблюдают границ сервисов.
Но из этого не следует, что нужно позволить всем сервисам собраться в пучок. Этот период должен иметь сугубо переходный характер. Как только стабилизируются API сервисов, нужно приступать к их перемещению в собственные сборки. Если по истечении нескольких недель (или в крайнем случае пары месяцев) вам не удастся добиться стабильности в отношении границ сервисов, определяемых с целью их приемлемого разделения, снова объедините их в более монолитный сервис, сохраняя модульное разделение в пределах границ, и выделите время для того, чтобы разобраться с областью их применения. В таком подходе отражается опыт нашей собственной команды SnapCI, уже упоминавшийся в главе 3.
У многих технологических стеков имеются как своеобразные высококачественные артефакты, так и инструментальные средства, поддерживающие их создание и установку. У Ruby есть Gem-пакеты, у Java — JAR- и WAR-файлы, а у Python — egg-пакеты. Разработчики с опытом работы с одним из стеков будут неплохо разбираться в работе с этими артефактами (и, надеюсь, в их создании).
Но с точки зрения создателя микросервисов, зависящей от выбранного вами технологического стека, этот артефакт может быть несамодостаточным. Наряду с тем, что в Java JAR-файл может быть создан исполнительным и способным запускать встроенный HTTP-процесс, для Ruby- и Python-приложений ожидается применение диспетчера процессов, запускаемого внутри Apache или Nginx. Поэтому нам может понадобиться некий способ установки и настройки других программ, необходимых для развертывания и запуска наших артефактов. В этом случае можно обратиться за помощью к таким средствам автоматизированного управления настройками, как Puppet и Chef.
Другим недостатком в данном случае является то, что эти артефакты относятся только к определенному технологическому стеку, что может еще больше усложнить развертывание при задействовании набора технологий. Оцените складывающуюся ситуацию с точки зрения того, кто пытается развернуть сразу несколько сервисов. Вполне возможно, что какому-нибудь разработчику или тестировщику захочется протестировать работу некоторых функций или же понадобится кто-то, берущий на себя управление развертыванием производственных сборок. А теперь представьте, что в имеющихся сервисах используются три совершенно разных механизма развертывания. Возможно, это Ruby Gem, JAR-файл и nodeJS NPM-пакет. Кто-нибудь скажет вам спасибо?