Непрерывная интеграция на примере Hudson
Все мы прекрасно понимаем, что тестирование является неотъемлемой частью жизненного цикла разработки ПО. Чем чаще мы тестируем наш код, тем быстрее мы сможем обнаружить ошибку, вкравшуюся в него в ходе разработки, и быстрее её исправить. При этом стоит понимать, что тестирование крайне желательно проводить в окружении, максимально близком к боевому (ОС, ПО, Hardware, Нагрузка), что бы иметь возможность обнаружить ошибки, которые не проявляются на сервере разработки, но могут появиться в бою. Компануя два вышесказанных тезиса вместе мы получаем концепцию, называемую Continuous Integration.
Суть CI заключается в постоянной (например, после каждого commit'а) сборке и тестировании разрабатываемого ПО в максимально приближенной к боевой среде с целью как можно более раннего обнаружения ошибок и оповещения о них разработчиков. Сама идея CI принадлежит Martin Fowler, подробно описавшему её в своей статье.
Для автоматизации процесса непрерывной сборки существуют готовые решения (Hudson, CruiseControl), интеграцию одного из которых (Hudson) я и опишу в этой статье.
Задача
И так, допустим у нас есть два проекта: Java-сервис (со своей БД), и PHP-клиент (со своей БД) для него. Оба проекта распространяются в виде deb-пакетов. Необходимо настроить инфраструктуру непрерывной интеграции этих проектов.
Реализация
Для того, что бы иметь представление о том, чего же мы хотим в итоге добиться — начнём с конца: рассмотрим схему, которую мы хотим реализовать:
- рабочая машина программиста — написание кода,
- сервер SVN — хранение кода,
- сервер Staging — установка и тестирование собранных проектов,
- сервер Selenium — тестирование web-интерфейса,
- сервер Repo — хранение собранных пакетов,
- сервер CI — соединение всех узлов системы в единое целое.
Разработчик вносит изменения в проект на своей машине и commit'ит их в SVN. На сервере SVN срабатывает post-commit hook, который инициирует процесс build'а соответствующего проекта на сервере CI. Сервер CI обновляет версию пакета из SVN, компилирует проект, запускает unit-тесты, выкладывает проект на staging-сервер.
Для проектов без web-интерфейса запускаются интеграционные тесты, для проектов с web-интерфейсом запускаются тесты Selenium. Сервер CI формирует отчёты и при необходимости (в случае провала на любом из этапов сборки проекта) отправляет email-уведомление пользователю.
Выкладка пакетов проекта в репозитарий для боевых серверов производится разработчиком вручную во время релиза.
Hudson
Главным и самым интересным узлом в нашей системе является сервер CI. В данном случае это будет Hudson как одно из самых популярных и распространённых бесплатных решений.
Первым делом установим его. Hudson доступен в виде пакета, поэтому его установка довольно проста. Кроме того, всю свою конфигурацию Hudson хранит в файлах (/var/lib/hudson), в виду чего не требует интеграции с какой-либо БД.
Архитектура Hudson'а построена на основе plugin'ов. То есть по сути работа Hudson'а сводится к хранению настроек проектов/плагинов и сборке проекта. В свою очередь сборка проекта заключается в запуске в определённом порядке установленных плагинов, включённых в настройках проекта.
Плагины можно разделить на несколько условных групп, образующих цикл сборки проекта именуемый также как «pipeline» (настройка плагинов проекта доступна через меню «Настройки проекта»):
- управление исходным кодом (получение/обновление кода проекта из репозитария),
- триггеры сборки (настройка времени автозапуска для сборки проекта),
- среда сборки (настройка среды сборки проекта: версия JVM),
- сборка (основной этап: запуск плагинов, осуществляющих логику сборки, интеграции и тестирования),
- послесборочные операции (формирование/публикация отчётов, нотификация).
К сожалению, Hudson позволяет изменять только порядок выполнения плагинов, входящих в группу «сборка» (порядок выполнения остальных плагинов в рамках своей группы определяется на основе значений аннотации @Execution коде плагинов). Поэтому, в случае если вам необходимо реализовать свой сценарий сборки, для которого не достаточно набора стандартных плагинов из группы «Сборка», можно пойти тремя путями:
- вызвать любой внешний исполняемый скрипт, реализующий этот сценарий (пункт ”Execute Shell” из меню ”Add build step”),
- подключить плагин системы сборки проекта (Phing, Ant, Maven) и указать необходимую цель,
- написать свой плагин.
По-умолчанию Hudson поставляется с уже установленными плагинами для работы с SVN и Maven. Этого могло бы быть вполне достаточно, если бы речь шла только о Java проектах. Однако допустим, что нам так же необходимо работать с PHP-проектами. В этом случае для сборки проекта логичнее использовать Phing, плагин для которого нужно установить отдельно. Делается это путём перехода в раздел «Настройка / Управление плагинами / Доступные обновления».
Обратите внимание, что некоторые плагины требуют запуска Hudson под Java 6. Изменить путь к JVM (ровно как и остальные конфигурационные опции) можно в файле /etc/default/hudson. В остальном все конфигурационные параметры касающиеся непосредственно работы Hudson могут быть отредактированы через браузер в web-интерфейсе.
Относительно настроек плагинов стоит также упомянуть, что плагин имеет как общие настройки («Настройка / Конфигурирование системы»), так и настройки проекта («Настройка / Имя проекта / Настроить проект»).
Теперь, когда все необходимые плагины установлены, мы можем создать новый проект/задачу, указав его имя и настройки для соответствующих плагинов: URL в репозитарии SVN и команду для сборки.
Обратите внимание, что вы можете производить сборку не по какому-то расписанию или при опросе репозитария при наличии изменений в проекте, а по коммиту в SVN. Благодаря тому, что Hudson имеет «Remote Access API», позволяющее кроме прочего инициировать сборку проекта, сделав GET-запрос, вы можете легко добавить соответствующий post-commit hook (например, с помощью svnlook) для вашего проекта.
Рассмотрим этап сборки:
На данный момент сборка пакета включает в себя получение данных из репозитария и выполнение цели Phing (сборка пакета). В принципе сюда же можно добавить запуск unit-тестов и deploy проекта на staging-сервер. Однако, тут стоит обратить внимание на несколько моментов.
Во-первых, конфиг для работы приложения на staging-сервере может отличаться от боевого конфига. В этом случае очевидным решением будет хранение в проекте конфига для staging-сервера и подмене им оригинального проекта при сборке (отдельная цель для сборки в случае Phing или профиль для Maven).
Во-вторых установить пакет на staging-сервер с помощью плагинов SCP и SSH (для работы плагина необходимо убедится, что параметр PasswordAuthentication в конфиге sshd выставлен в yes, а хост staging-сервера добавлен в known-хостов) не получится, так как плагин SSH относится этапу сборки проекта, а plugin SCP — к послесборочным операциям. Поэтому проблему deployment'a проекта на staging-сервер придётся решать с помощью Phing или Maven + AntRun.
Для того, что бы наш сценарий сборки мог производить действия на удалённых серверах необходимо сгенерировать ssh-ключи: приватный ключ оставить на сервере CI, a публичный раскидать по всем серверам, с которыми будет происходить взаимодействие: staging, repo, svn — добавив их в список известных хостов (known_hosts). Кроме того, для того, что бы Hudson смог установить пакет на удалённом сервере потребуется завести на удалённом сервере соответствующего пользователя (hudson) и дать ему sudo.
В-третьих, для успешной сборки java-приложений с помощью Maven потребуется определить настройки Maven для пользователя hudson на сервере CI (имеется ввиду каталог ~/.m2).
Следующим шагом после установки пакета на staging-сервер должен стать запуск интеграционных тестов. Они могут быть запущены на самом сервере CI, однако, предпочтительнее сделать это на staging-сервере. В первом случае всё довольно просто: вызываем соответствующую цель Phing / Maven или настраиваем плагин SeleniumHQ.
Однако, открытым остаётся вопрос, что же делать, если требуется запустить процесс тестирования на внешнем сервере — например, обратиться к серверу Selenium RC? Ответ тут как нельзя прост: Selenium RC имеет HTTP API для работы с ним, поэтому самым тривиальным решением в данном случае будет написание небольшого скрипта на любом угодном вам языке, который инициирует процесс тестирования и время от времени опрашивает удалённый сервер на предмет завершения теста. Далее этот скрипт подключается к сценарию сборки через плагин ”Execute Shell”. Добавлю ещё, что успех или неуспех работы скрипта определяется Hudson'ом на основе кода возврата вашего скрипта.
Настроив процесс сборки не забудем о самой важной части процесса — уведомлении разработчика о результатах сборки. Hudson позволяет настроить почтовые уведомления как для конкретных получателей, так и для авторов commit'ов, чьи изменения вызвали поломку.
В дополнение рекомендую всем, кто будет использовать Hudson для PHP-проектов ознакомиться с соответствующими статьями из Wiki Hudson'а.
Сервер Staging
Установка пакетов
И так, как уже говорилось выше, в нашей инфраструктуре должен присутствовать сервер по конфигурации максимально приближенный к боевому. На этот сервер Hudson будет устанавливать самые свежие пакеты проектов, собранных по trunk'у. Это даст нам возможность:
- проводить интеграционное тестирование в условиях максимально приближенных к боевым,
- позволит иметь своего рода площадку для демонстрации самой свежей функциональности.
Одной из основных проблем, которую придётся решить при настройке данного сервера, является «тихая» установка пакетов. Для того, что бы наши пакеты устанавливались без лишних диалогов (то есть могли быть установлены с помощью скриптов или плагинов Hudson'а) необходимо переконфигурировать debconf (dpkg-reconfigure debconf), указав ему уровень важности задаваемых вопросов выше, чем те, что используются в установочных скриптах вашего deb-пакета.
Кроме того, между устанавливаемыми на staging-сервер пакетами возможны зависимости. Так, например, проект «клиент» зависит от проекта «сервер». В этом случае мы должны явно следить за тем, что бы на staging-сервере при установке пакета «клиент» происходила установка необходимого ему пакета «сервер».
На первый взгляд очевидным решением, с учётом дистрибуции проекта через deb-пакеты,
будет управление зависимостями силами dpkg путём добавления в dependencies control-файла пакета «клиент» информации о пакете «сервер».
В этом случае нам также придётся выделить отдельный debian-репозитарий, в который будут сливаться все собранные сервером CI пакеты, и дополнить сценарий сборки командой копирования пакета в репозитарий. Кроме того, необходимо будет организовать механизм автообновления данных в этом репозитарии при добавлении нового пакета, организовать доступ к пулу репозитария (например, подняв веб-сервер) и добавить данный репозитарий в sources-list на staging-сервере. При организации автообновления вручную (запуском сканера пакетов после выкладки пакета в репозитарий) новый пакет можно будет ставить через apt, в случае если репозитарий будет обновляться по расписанию придётся изобретать фокусы типа dpkg -i package; apt-get -f install. Подробнее о настройке своего debian-репозитария можно почитать тут.
Однако, данный подход имеет ряд недостатков. Во-первых, зависимости могут быть установлены только на этот же сервер. Во-вторых, такой подход довольно значительно усложняет всю систему в целом, что противоречит принципу KISS (ну или «ЧМОКЕ», если по-русски :D).
Лучшим решением, на мой взгляд, тут будет использовать репозитарий только для взаимодействия с боевым сервером. При этом выкладка пакетов в репозитарий должна осуществляться не автоматически, а по решению разработчика. Что же касается staging-сервера — на нём будут устанавливаться пакеты из trunk'ов основного пакета и всех его зависимостей, что позволит значительной снизить сложность системы CI, при этом дав нам возможность иметь на staging-сервере последние актуальные версии
пакетов.
Работа с БД
Наши пакеты могут использовать БД. В этом случае БД также устанавливается на staging-сервер, а обновление структуры / данных БД производиться с помощью утилиты dbdeploy.
Интегрировать dbdeploy в проект двумя путями:
- под каждую БД выделяется отдельный проект в SVN и как следствие — в Hudson, со своим сценарием сборки, инициируемым по hook'у SVN (вариант имеет смысл, когда БД используется несколькими проектами),
- структура файлов dbdeploy становиться частью основного проекта, а вызов скрипта актуализации версии БД dbdeploy происходит в postinst-скрипте пакета.
Отдельно в данном случае встаёт вопрос об изменениях данных БД в ходе тестирования. Понятно, при написании модульных тестов мы не работаем с БД, а используем mock-объекты (мне, например, нравится Mockito).
Однако, как быть с интеграционными тестами, которым просто необходимо работать в «реальных» условиях? В случае XUnit-тестов мы можем выполнять каждый тест в рамках транзакции к БД. Такой подход на мой взгляд является более предпочтительным, так как с учётом версионирования БД через dbdeploy мы всегда знаем, какие данные у нас есть в БД на текущий момент и можем смело привязываться к ним в наших тестах. Однако, в случае тестирования web-интерфейса (например, с помощью Selenium) у нас нет возможности запускать каждый тест в рамках транзакции.
Поэтому вариантов тут на мой взгляд остаётся два: либо перед запуском тестирования web-интерфейса полностью переинициализировать данные в БД на основе имеющихся
патчей, либо строить тесты так, что бы они не привязывались к каким-то конкретным данным из БД (например, создавали необходимые для тестирования данные через web-интерфейс сами) и по возможности не оставляли после себя «мусора».
Cервер Selenium
В случае, когда приложение не имеет web-интерфейса, интеграционный тест на staging-сервере, как я уже писал выше, вполне может заключаться в запуске XUnit-тестов. Однако, при наличии пользовательского интерфейса крайне удобно провести полное тестирование всей цепочки от HTML до БД с помощью Selenuim'a.
Selenium — это мощная система тестирования web-приложений, которую условно можно разделить на две части:
- Selenuim IDE — инструмент для разработки и запусков сценариев тестирования в браузере (доступно в виде plugin'a firefox),
- Selenium RC — распределённая система из сервера Selenium и подчинённых ему клиентов, на которых запускаются тесты под разными браузерами.
По понятным причинам, нас интересует второй вариант. По скольку установка и настройка Selenuim'а — довольна большая тема, касаться её в этой статье я смысла не вижу: вся информация есть в документации.
Замечания
Стоит заметить, что CI можно проводить и в ручном режиме, каждый раз компилируя и тестируя код перед commit'ом. Однако, автоматизация данного процесса с помощью сервера CI на много целесообразней. Кроме того важно понимать, что CI и ночные сборки (nightly builds) — не одно и тоже. Последние позволяют выявить баги, но с большим запозданием, в то время как цель CI — скорейшее обнаружение ошибок. На мой взгляд ночные сборки могут послужить частичной заменой CI только в том, случае когда сборка и тестирование проекта — процесс, занимающей довольное большое количество времени. Кроме того, если в проекте есть как unit, так и интеграционные тесты, можно разбить сборку проекта на две части: первая (с unit-тестами) запускается каждый раз при commit'e, вторая с интеграционными тестами — раз в час/сутки.
Вывод
Описанное выше решение работает и приносит профит. Однако, как мы все знаем, теория, к сожалению, далеко не всегда соответствует практике. Внедрение системы CI потребовало решения ряда проблем, не все из которых были решены идеально.
Вероятность того, что для staging-сервера кто-то даст вам ресурсы, сравнимые про характеристикам с боевыми, крайне мала — скорее всего это будет средней мощности виртуальник на полузаброшенной хост-машине, что в корне подрывает один из принципов CI — тестирование в схожем окружении. Это в свою очередь влечёт за собой то, что интеграционные тесты, могут начать занимать гораздо большее время, чем планировалось изначально. Поэтому «непрерывностью» в моём случае пришлось поступиться и начать запускать тесты не по hook'ам SVN, а по расписанию.
В целом же при наличии в вашей команде некоторой культуры разработки (я имею в виду понимание того, что CI — не панацея, а лишь инструмент, который при должном умении обращения с ним может улучшить качество их работы) внедрение CI вполне оправдано.
Ну и наверное самое главное: как показала практика, интеграция системы CI — задача командная. Для её решения потребуется работа разработчиков, тестировщиков и администраторов.