Deployment Pipeline на практике

:

Резюме: в сети появляется много теоретического материала по теме Continuous Delivery & Deployment Pipeline’ов, однако практических реализаций в примерах пока не находил. Собственно здесь хочу поделиться примером того как мы реализовали Deployment Pipeline внутри нашего open source проекта JTalks. Все исходники открыты, поэтому вы сможете не только прочесть пояснение, но и своровать реализацию.

Если вы не понимаете что такое Deployment Pipeline и зачем он нужен, вот некоторые пункты, однако для полного понимания нужно прочесть до конца:

  • Deployment Pipeline - это набор практик для автоматизации развертывания приложений на разные окружения, включая production

  • Deployment Pipeline позволяет сделать релизы частыми и уменьшить риски провалов

  • Deployment Pipeline дает возможность ускорить работу разных команд (Dev, QA, DevOps) автоматизируя рутинную работу

Выглядеть это может к примеру так

Jenkins

Первое и центральное место - это собсно наш Continuous Integration сервер. Я рекомендую использовать именно Jenkins,
потому как он имеет сотни удобных плагинов, и половины которых вам не дадут никакие коммерческие инструменты типа
Team City, Bamboo. Дополнительный пункт в пользу Jenkins - он открытый и вы сами можете дописывать его плагины. Такая
возможность обычно приходит очень кстати в особо крупных проектах, где стандартные фичи не подходят и нужно много
писать своих решений.

Какие плагины Jenkins’a стоит рассматривать для реализации Deployment Pipeline’ов:

  • Build Pipeline Plugin - собсно. Этот плагин далеко не идеален как и большинство Jenkins плагинов, с некоторым количеством багов (которые вы можете исправить). Он предоставляет возможность визуализации вашего конвеера. Визуализация очень важна чтоб все члены команды включая будущих новичков быстро сообразили какие существуют окружения и как приложение эволюционирует. Также этот плагин контролирует возможность запускать те или иные планы. Для реализации настоящего Deployment Pipeline’a вам важно, чтоб ваши артефакты не переходили в следующие фазы до того как выполнились предыдущие. Например, ваше приложение не должно проходить на UAT или PREPROD окружения до того как прошли автоматизированные системные тесты.
  • Rebuild Plugin - например вы решили зарелизить ваше приложение, а уже в PROD окружении оказалось что что-то не так. На моей практике было такое, что приложение работало недопустимо медленно на реальных данных. Тогда у вас должна быть возможность откатить ваше изменение. Откат же не должен в идеале отличаться от обычного релиза, и если вам посчастливилось оказаться в такой ситуации - просто зарелизьте предыдущую версию вашего приложения! Rebuild Plugin позволяет перезапускать предыдущие билды с теми же параметрами.
    Конечно такая простая ситуация бывает не всегда - иногда вам нужно откатить изменения в базе данных. Тогда вам конечно еще придется восстанавливать бекапы. Сделайте так чтоб любое ваше окружение использовало все те же скрипты - тогда вы их оттестируете сотни раз еще на фазе разработки.
  • SCM Sync Configuration Plugin - это не относится напрямую к вашему конвееру, однако лучше версионировать изменения в вашем Continuous Integration сервере. Ибо чем дальше в лес, тем больше дров - CI в какой-то момент становится вашим самым важным и центральным инструментом со сложной конфигурацией. Этот плагин стоит использовать с большой осторожностью, потому как бывало что он просто отказывал. В последнее время мы начали задумываться о ручном бекапе.

Артефакты (Binaries)

Следующая остановка - хранилище ваших артефактов. Одна из главных особенностей настоящих Deployment Pipeline’ов - ваши артефакты собираются лишь один раз и используются на каждом окружении. Это важно по нескольким причинам:

  • Вы получаете повторяемость окружений. Нет даже такой возможности что на разные окружения попали разные исходники. Если же вы собираете каждый раз новые артефакты, то так утверждать уже не можете. Во-первых, потому что во время сборок могут использоваться какие-то параметры окружения, во-вторых сами инструменты сборки могут выпендриться. Например, Maven может использовать другую версию артефакта если вы используете version ranges: <version>[1.5-1.7)</version>.
    Другой пример - это сами Maven Repos (Nexus, к примеру), которые могут возвращать разные артефакты в зависимости от их настроек и описания pom.xml. Это может произойти если используются Repository Groups которые ссылаются на несколько репозиториев с одними и теми же артефактами.

  • Если на каком-либо окружении был найден баг, вы всегда можете загрузить этот же артефакт на другое окружение и проверить что к чему.

  • Другие команды (QA) могут сами откатывать версии и таким образом локализовывать версию где был введен баг.

  • Ваш CI работает быстрей т.к. не нужно делать дополнительных сборок.

Хранить артефакты можно где угодно, даже тот же Jenkins позволяет их сохранять у себя и использовать Copy Artifact Plugin для передачи артефактов из плана в план. Однако намного удобней работать со специальными инструментами, такими как Sonatype Nexus, Artifactory. Есть несколько причин для этого:

  • У них есть специальные возможности для работы с артефактрами, такие как поиск и индексирование. Можно создавать внутри них несколько репозиториев и ограничивать к некоторым из них доступ. Можно даже создать репозиторий только со стабильными артефактами, а все остальные хранить отдельно.

  • Вам все равно они понадобятся для работы с Maven, Gradle - чтоб загружать из них зависимости.

  • Хранение артефактов в Jenkins’e усложняет с ним работу, например, нужно задумываться как делать бекапы эффективней.

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

Например, у нас в JTalks план сборки выглядит так:

#get rid of SNAPSHOT and add build number after version
VERSION=`mvn help:evaluate -Dexpression=project.version | grep -v "^\["| grep -v Download`
VERSION=${VERSION/%-SNAPSHOT/} #get rid of -SNAPSHOT if it's there
VERSION=$VERSION'.'$PIPELINE_NUMBER #add unique build number
mvn versions:set -DnewVersion=$version #update the versions in pom files onto unique ones
mvn clean package #run actual build and unit tests

У каждого плана в Jenkins есть специальные переменные, такие как BUILD_NUMBER (мы его переименовывали в PIPELINE_NUMBER) - такая переменная используется для уникальности, то бишь каждый артефакт получает соответствующий суффикс. Далее с этим суффиксом мы заливаем артефакт в Nexus в специальный репозиторий. Т.к. каждая сборка имеет свой уникальный номер-суффикс - мы этот номер передаем в другие планы нашего Deployment Pipeline’a, они отыскивают нужный артефакт и развертывают его на нужных окружениях.

Конфигурирование проектов

Для того, чтоб реализовать красивый и простой Deployment Pipeline, разработчики должны сделать некоторые усилия. Например, у вас ничего не получится если вы конфигурируете проект на этапе сборки. А такие есть (упаси!) - собирают по артефакту на каждое окружение, где разница лишь в конфигурации, а код - тот же. Чтоб все получилось вам нужно придерживаться одного правила: конфигурировать сборку нужно снаружи! Вы можете оставить некоторые окружения (включая общие локальные) внутри атрефактов и позволять переключаться между ними с помощью каких-то флагов: -Denvironment=UAT, однако также должна быть возможность задавать параметры снаружи. Это а) позволит разворачивать артефакт на любом окружении б) даст возможность хранить секретную конфигурацию (например, пароли от PROD базы) в отдельном репозитории без общего доступа.

Теперь о практике: в JTalks мы это реализовали расширив возможности Sping’ового PropertyPlaceholderConfigurer своим JndiAwarePropertyPlaceholderConfigurer. Идея в том, что сначала мы проверяем есть ли переменная в JNDI, и лишь потом, если не обнаружилось, смотрим в переменные среды и properties файлы внутри проекта. А определение какой-то опции может выглядеть так:

<bean class="org.jtalks.jcommune.model.utils.JndiAwarePropertyPlaceholderConfigurer">
  <property name="location" value="classpath:/org/jtalks/jcommune/model/datasource.properties"/>
</bean>
<bean ..>
  <property name="user" value="${JCOMMUNE_DB_USER:root}"/>
</bean>

Теперь как же задавать свойства снаружи? Наверно, у каждого AppServer’a есть JNDI, Tomcat не исключение. Создаем файл и кладем его в conf/Catalina/localhost/[app-namme].xml. А там пишем:
<?xml version='1.0' encoding='utf-8'?>
<Context>
  <WatchedResource>WEB-INF/web.xml</WatchedResource>
  <Environment name="JCOMMUNE_DB_USER" value="root" type="java.lang.String"/>
</Context>

Все, параметр задан в JNDI и если он там указан, то будет браться именно оттуда. Если же его там нет, то смотрим в env vars или properties файлы.

Инструмент сборки

Каждый проект конечно должен собираться специальным инструментом. Есть всякие Ant, Maven, Gradle. Здесь рассмотрим последние два. Maven к сожалению не вписывается в общую картину. Наша идея - иметь уникальный артефакт после каждого коммита, однако Maven прописывает свои версии в pom.xml и менять их после каждого коммита как-то не комильфо. Я такой вариант видел в крупных проектах когда использовались тематические ветки + валидация проходила на этапе прекоммита, при этом версия обновлялась только когда код попадал в develop ветку как это любят изображать Git фетишисты. Однако для мелких и средних проектов - это излишество, да и на крупных не факт что хороший вариант. Короче говоря мы выкрутились в JTalks тем как раз, что подменяли версию, заливали артефакт, но коммит изменений в pom файлы мы не делали.

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

Scripting Language

Для автоматизации релизов вам придется писать скрипты. Это может быть bash, python, ruby, groovy, вы вольны выбирать сами. Bash’a вам вряд ли хватит, а все остальные особо друг от друга не отличаются. Однако есть преимущества каждого из них:

  • Python установлен на большинстве Linux дистрибутивов, дополнительно не придется ставить никаких платформ.

  • Плюсы же Ruby в том, что затем нам могут понадобится инструменты для настройки окружений такие как Chef, которые используют Ruby. Вы даже можете собственно Pipeline реализовать на Chef. Однако все сервера должны быть с предустановленным soft’ом для этого.

  • Groovy удобен тем, что в качестве инструмента сборки можно использовать Gradle и тогда включить скрипты в сами исходники проекта.

Свои скрипты стоит пакетировать, например, Python имеет свой package manager - PIP. Это удобно потому как вы можете версионировать артефакты самих скриптов, да и установка будет намного более тривиальной.

Пример таких скриптов на Python вы можете посмотреть в наших открытых исходниках.

Виртуализация

Для того чтоб все окружения были еще более похожими и не возникало неожиданных ситуаций связанных с операционной системой либо железом, стоит задуматься о виртуализации ваших окружений, вплоть до виртуализации PROD’a. Это вам также даст прирост в производительности труда т.к. избавит от ручной настройки новых окружений. Дополнительным плюсом может быть то, что в определенный момент вы захотите использовать облачные решения типа Amazon EC2, тогда переход на них будет еще более прост.

Есть еще такой прекрасный инструмент как Vagrant, который сильно облегчает проблемы с повторяемыми окружениями. Вам лишь нужно описать какой образ использовать, какой софт будет установлен (это делается благодаря интеграции с Chef и Puppet) и ву а ля - готовое окружение в одну команду. Теперь даже самые малотехнические люди могут развернуть вашу систему без проблем. JTalks VM демонстрирует возможности Vagrant’a:

  • Устанавливаете Vagrant, качаете сами Vagrant скрипты из гит приведенного выше репозитория

  • vagrant up - и через некоторое время, когда все закачается и установится, вы получаете дистрибутив Ubuntu с установленными там deployment скриптами проекта, а также MySQL, Tomcat’ом.

  • Заходите на виртуалку vagrant ssh и запускаете один из проектов: jtalks deploy --environment vagrant --project jcommune --build 2280. Видите последнюю цифру? Она как раз и значит номер того артефакта, который мы разворачиваем. Еще раз ссылка на артефакты.

  • Ну и открываете браузер, вот вам полностью установленное в несколько кликов приложение: http://localhost:4444/jcommune