CI для Laravel

Как подружить Jenkins, Laravel, Dusk

CI(Continuous Integration) - это что о чем говорят большевики, и то что обходят стороной многие разработчики, когда речь заходит о непрерывной итерации между рабочим окружением, тестированием написанного добра и непосредственным релизом. Существует ни одна платформа для построения pipeline'ов, но исторически сложилось, что команда использует Jenkins для сборок и проведения тестирования.

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

На сегодняшний день Laravel - это экосистема, в состав которой входит неплохой пакет для тестирования интерфейса - Laravel Dusk. Конечно можно поспорить об инструментах для проведения данного вида тестирование, то тем не менее, как говорится "что имеем".

Итак, на сухом остатке по условиям: Jenkins, как отдельный сервер, Docker, Laravel+Dusk.

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


Как хотелось, чтоб это все работало?

Jenkins отслеживает изменения в репозитории проекта Laravel в определенной ветке, после чего начинается сборка, выполняются тесты и после удачного завершения предыдущих шагов выполняется релиз на какую-нибудь ноду(production, dev). И если первую часть с отслеживанием изменений выполнить достаточно просто, не выходя из настройки pipeline'а в интерфейсе Jenkins, то со второй все гораздо сложнее(по крайней мере для меня).

Изначально было понятно, чтобы отвязаться от окружения Jenkins сервера необходимо использовать Docker-контейнеры и именно там устраивать танец с саблями, чтобы не положить в случае чего саму машину. Но до конца не понимал, сколько нужно контейнеров для данной сборки: один, который будет иметь в себе все нужное ПО для запуска приложения и последующего тестирования, либо же несколько, чтобы каждый контейнер выполнял только свою ограниченную функциональность? Но после 100+ неудачных сборок, при использовании единственного контейнера стало понятно, что салат Бородино(все смещалось: кони, люди) лучше оставить на потом, а функционал разделить на независимые единицы приложения:

  • Контейнер приложения;
  • Selenium hub;
  • Chrome Browser.

При таком подходе все встает на места, и можно выносить описание образа под контейнер приложения в отдельный репозиторий и уже переходить к самому pipeline'у.


Логика сборки

Так как ничего общего между контейнерами у нас нет, то единственной связующей нитью является сервер Jenkins, порты которого и будут использованы при сборке. Конечно по-хорошему можно все контейнеры объединить в одну сеть, но я пошел другим путем(возможно и неправильным). Вся соль данной реализации заключается в том, что хоть и все отвязано от использования ресурсов Jenkins-машины, но тем не менее приложение будет запускаться на одном из свободных портов сервера, а также использовать порты для Selenium Hub'а.

Сборка сводится к следующим шагам:

  • Выполнить GitSCM для необходимых репозиторией;
  • Запустить браузер, связанный с Selenium Hub'ом для последующего использования;
  • Запустить контейнер с приложением;
  • Запустить выполнение тестов с последующим релизов в случае успешного завершения;
  • Чистка и удаление ресурсов после сборки.

От слов к делу, привожу пример реализации Jenkinsfile.


Реализация

Из всех шагов сборки ключевым является build:app, где и описывается процесс запуска контейнеров. Хаб и браузер объединены в отдельную сетку, где имеют доступ к друг другу, а вот запуск приложения следует рассмотреть чуть детальнее.

Пробрасывание свободного порта в контейнер необходимо просто для того, чтобы браузер смог выполнить запрос к необходимому ресурсу, а команда sed по замене переменной окружения APP_URL необходима для Dusk'а чтобы поддерживать данную схему работы приложения и Selenium'а


Почему node, а не pipeline?

Тут стоит сказать, что при работе с директивой pipeline, контейнеры, создаваемые условием удаляются после сборки, а продолжают висеть:

С чем это связано не совсем понятно, но альтернативный синтаксис node, такой фичи не имеет. Постоянно итерирование cd это не мое глубокое убеждение, а не рабочее поведение команды dir() в контексте docker.inside или docker.withRun.


Резюме

Отведя N-е количество дней на то, чтобы разобраться как все должно работать на Jenkins'е, получив 500+ провалов в сборке проекта, в итоге удалось достичь желаемого результата, реализация которого далека от идеала, и никоим образом не претендует на must have, но она работает и такие слова как CI ольше не кажутся такими далекими и сложными.