Хабрахабр

Docker + Laravel + RoadRunner = ❤

picture

Давать ссылку на ранее написанный пост уже не хочется, так как взгляды относительно того, как следует решать поставленную задачу, довольно сильно изменились. Данный пост написан по заявкам трудящихся, которые с завидной периодичностью спрашивают о том "Как запустить Illuminate / Symfony / MyOwnPsr7 приложение в докере".

Всё, что будет написано ниже, является субъективным опытом, который (как и всегда) не претендует на право считаться единственно верным решением, но некоторые подходы и решения, возможно, тебе покажутся интересными и полезными.

Адаптировать под другие PSR-7-based фреймворки/компоненты возможно, но этот рассказ не про это. В качестве приложения так же буду использовать Laravel, так как он мне наиболее знаком и довольно широко распространен.

Работа над ошибками

Хотелось бы начать с того, что оказалось "не лучшими практиками" в контексте предыдущей статьи:

  • Необходимость изменять структуру файлов в репозитории
  • Использование FPM. Если мы хотим производительности от своих приложений то, пожалуй, одним из лучших решений ещё на стадии выбора технологий будет отказ от него в пользу чего-то более быстрого и "приспособленного" к тому, что память может утекать. RoadRunner by lachezis тут оказывается как никогда кстати
  • Отдельный образ с исходниками и ассетами. Не смотря на то, что используя такой подход мы можем реюзать один и тот же образ для построения более сложной маршрутизации входящих запросов (nginx на фронте для отдачи статики; запросы на динамику обслуживает другой контейнер, в который прокинут volume с теми-же исходниками — для лучшего масштабирования) — данная схема показала себя довольно сложной в продуктовой эксплуатации. И более того — RR сам прекрасно отдает статику, а если статики много (или ресурс умеет загружать и отображать пользовательский контент) — выносим её в CDN (связка S3 + CloudFront + CloudFlare работает отлично) и забываем об этой проблеме в принципе
  • Сложный CI. Это стало реальной проблемой, когда начался период активного "наращивания мяса" на этапы сборки и автоматического тестирования. Чуваку, который ранее не поддерживал этот CI, становится очень сложно вносить в него правки без боязни что-либо поломать.

Набор "инструментов разработчика" у нас не изменился — это всё тот-же docker-ce, docker-compose и могучий Makefile. Теперь, зная какие проблемы необходимо устранить и с пониманием как это сделать — предлагаю приступить к их устранению.

В результате мы получим:

  • Самостоятельный контейнер с приложением без необходимости монтирования дополнительных volume
  • Пример использования git-hooks — будем ставить нужные зависимости после git pull автоматически и запретим пушить код, если тесты не проходят (хуки будут храниться под гитом, естественно)
  • Обработкой HTTP(s) запросов будет заниматься RoadRunner
  • Разработчики смогут как и раньше выполнять dd(..) и dump(..) для отладки, и при этом ничего не будет крашиться в их браузере
  • Тесты можно будет запускать прямо из IDE PHPStorm, при этом запускаться они будут в контейнере с приложением
  • CI будет собирать для нас образы при публикации нового тега версии приложения
  • Возьмем для себя строгое правило ведения файлов CHANGELOG.md и ENVIRONMENT.md

Наглядное внедрение нового подхода

Отправная точка — это скелетон Laravel приложения созданный с помощью composer create-project latavel/laravel: Для наглядной демонстрации — весь процесс разобью на несколько этапов, изменения в рамках которых будут оформлены в виде отдельных MR (после слияния все бранчи останутся на своих местах; ссылки на MR в заголовках "шагов").

$ docker run \ --rm -i \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ composer composer create-project --prefer-dist laravel/laravel \ /src/laravel-in-docker-with-rr "5.8.*"

Для этого нам нужны Dockerfile, docker-compose.yml для описания "как поднимать и линковать контейнеры", и Makefile для того, чтобы свести и без того упрощенный процесс к одной-двум командам. Первым делом необходимо научить приложение запускаться в контейнере.

Dockerfile

X. Базовый образ использую php:X. Более того — все последующие обновления интерпретатора сводятся к тому, чтобы просто изменить значение в этой строчке (обновить PHP теперь проще некуда). X-alpine как наиболее легкий и содержащий то, что надо для запуска.

Работает это быстро, и без зависимостей от curl / git clone / make build. Composer и бинарный файл RoadRunner доставляются в контейнер с помощью multistage и COPY --from=... — это очень удобно, да и все значения связанные с версиями не "разбросаны", а находятся в начале файла. Образы 512k/roadrunner поддерживаются мною, если хотите — можете собирать бинарный файл самостоятельно.

Интересная история приключилась с переменной окружения PS1 (отвечает за prompt в шелле) — оказывается, использовать в ней emoji можно, и всё локально работает, но стоит попытаться запустить образ с переменной содержащей emoji в, скажем, rancher — он будет крашиться (в swarm всё работает без проблем).

Естественно — ничего не мешает использовать "нормальный" сертификат. В Dockerfile я запускаю генерацию самоподписанного SSL сертификата для того, что бы его использовать для входящих HTTPS запросов.

Отдельно хочется сказать про:

COPY ./composer.* /app/ RUN set -xe \ && composer install --no-interaction --no-ansi --no-suggest --prefer-dist \ --no-autoloader --no-scripts \ && composer install --no-dev --no-interaction --no-ansi --no-suggest \ --prefer-dist --no-autoloader --no-scripts

Делается это для того, чтобы при последующих сборках образа с использованием --cache-from, если состав и версии установленных зависимостей не изменились, то composer install не выполнялся, взяв этот слой из кэша, тем самым экономя время сборки и трафик (за идею спасибо jetexe). Тут смысл следующий — отдельным слоем в образ доставляются файлы composer.lock и composer.json, после чего выполняется установка всех зависимостей, описанных в них.

composer install выполняется дважды (второй раз с --no-dev) для "прогрева" кэша dev-зависимостей, чтобы когда мы на CI для запуска тестов поставили все зависимости, они ставились из кэша composer-а что уже есть в образе, а не тянулись из далеких галактик.

Последний инструкцией RUN мы выводим версии установленного ПО и состав модулей PHP как для истории в логах сборки, так и для того, чтобы убедиться, что "оно как минимум есть и как-то запускается".

Entrypoint использую тоже свой, так как перед тем как запустить приложение где-то в кластере очень хочется проверить доступность зависимых сервисов — БД, redis, rabbit и прочих.

RoadRunner

Для интеграции RoadRunner с Laravel-приложением был написан пакет, который сводит всю интеграцию к паре команд в шелле (выполнив docker-compose run app sh):

$ composer require avto-dev/roadrunner-laravel "^2.0"
$ ./artisan vendor:publish --provider='AvtoDev\RoadRunnerLaravel\ServiceProvider' --tag=rr-config

Добавляем APP_FORCE_HTTPS=true в файл ./docker/docker-compose.env, и указываем путь до SSL сертификата в контейнере в файлах .rr*.yaml.

Всё, что потребуется — это добавлять пефикс к этим хэлперам, а именно \dev\dd(..) и \dev\dump(..) соответственно. Для того, чтобы была возможность использовать dump(..) и dd(..) и всё при этом работало, есть другой пакет — avto-dev/stacked-dumper-laravel. Без этого будете наблюдать ошибку вида:

worker error: invalid data found in the buffer (possible echo)

После всех манипуляций выполняем docker-compose up -d и вуа-ля:

screenshot

База данных PostgeSQL, redis и воркеры RoadRunner успешно запущены в контейнерах.

Зависимые цели, свой синтаксический сахар, 99% вероятность того, что на linux/mac машине разработчика он уже стоит, автокомплит "из коробки" — малый список его преимуществ. Как уже писал ранее, Makefile — очень недооцененная штука.

Добавив его в наш проект и выполнив make без параметров, мы можем наблюдать:

screenshot

Для получения coverage отчета достаточно выполнить make test-cover, и перед запуском тестов в контейнер доставится xdebug с его зависимостями, и запустятся тесты (так как эта процедура выполняется не часто и не силами CI — это решение кажется лучшим, чем держать отдельный образ со всеми dev-примочками). Для запуска юнит-тестов мы можем как выполнить make test, так и получив шелл внутрь контейнера с приложением (make shell) выполнить composer phpunit.

Git Hooks

В Makefile для этого существует отдельный target: Хуки в нашем случае будут выполнять 2 важные роли — не позволять пушить в origin код, тесты которого не выполняются успешно; и автоматически ставить все необходимые зависимости, если стянув изменения себе на машину окажется, что composer.lock изменился.

cwd = $(shell pwd) git-hooks: ## Install (reinstall) git hooks (required after repository cloning) -rm -f "$(cwd)/.git/hooks/pre-push" "$(cwd)/.git/hooks/pre-commit" "$(cwd)/.git/hooks/post-merge" ln -s "$(cwd)/.gitlab/git-hooks/pre-push.sh" "$(cwd)/.git/hooks/pre-push" ln -s "$(cwd)/.gitlab/git-hooks/pre-commit.sh" "$(cwd)/.git/hooks/pre-commit" ln -s "$(cwd)/.gitlab/git-hooks/post-merge.sh" "$(cwd)/.git/hooks/post-merge"

Их исходники можно посмотреть по этой ссылке. Выполнение make git-hooks просто сносит имеющиеся хуки, и ставит на их место те, что находятся в директории .gitlab/git-hooks.

Запуск тестов из PhpStorm

Не смотря на то, что это довольно просто и удобно — сам довольно долго пользовался ./vendor/bin/phpunit --group=foo вместо того, чтоб просто нажимать хоткей прямо во время написания теста или кода, с ним связанного.

Выбираем Docker compose, и имя сервиса app. Нажимаем File > Settings > Languages & Frameworks > PHP > CLI interpreter > [...] > [+] > From Docker, Vargant, VM, Remote.

screenshot

В поле Path to script указываем /app/vendor/autoload.php, а в Path mappings указываем корневую директорию проекта как монтируемую в /app. Второй шаг — это указание phpunit-у необходимость использовать интерпретатор из контейнера: File > Settings > Test frameworks > [+] > PHPUnit by remote interpreter и выбрать ранее созданный удаленный интерпретатор.

screenshot

И теперь мы можем запускать тесты прямо из IDE используя интерпретатор внутри образа с приложением, нажимая (по дефолту, Linux) Ctrl + Shift + F10.

Для этого создаем файл .gitlab-ci.yml в корневой директории приложения, наполняя его примерно следующим содержанием. Всё, что нам остается сделать — это автоматизировать процесс запуска тестов и сборки образа. Основная идея данной конфигурации — быть максимально простой, но не терять в функциональности при этом.

Используя --cache-from сборка образа при повторном коммите производится очень быстро. Сборка образа производится на каждом бранче, на каждом коммите. Необходимость пересборки обусловлена тем, что на каждом бранче у нас есть образ с теми изменениями, которые были в рамках этого бранча сделаны, а как следствие — ничего нам не мешает его раскатать на swarm/k8s/etc для того, что бы "вживую" убедиться в том, что всё работает, и работает как надо ещё до мерджа с master-веткой.

После сборки — запускаем unit-тесты и проверяем запуск приложения в контейнере, отправляя на health-check endpoint запросы curl-ом (данное действие опционально, но несколько раз данный тест меня очень выручал).

X. Для "выпуска релиза" — просто публикуем тег вида vX. X (если вы ещё и будете придерживаться семантического версионирования — будет очень круто) — CI соберет образ, прогонит тесты, и выполнит действия, что вы укажете в deploy to somewhere.

Не забудьте в настройках проекта (если это возможно) ограничить возможность публикации тегов только лицам, которым разрешено "выпускать релизы".

CHANGELOG.md и ENVIRONMENT.md

Если с первым всё более и менее понятно, то вот относительного второго дам пояснения. Перед тем, как принять тот или иной MR — проверяющий должен в обязательном порядке проверить на соответствие файлы CHANGELOG.md и ENVIRONMENT.md. Т.е. Данный файл служит для описания всех переменных окружения, на которые реагирует контейнер с приложением. И в момент, когда возникает вопрос "Нам нужно срочно переопределить то-то и то-то" — никто судорожно не начинает копаться в документации или исходниках — а смотрит в одном-единственном файле. если разработчик добавляет или удаляет поддержку той или иной переменной окружения — это обязательно должно быть отражено в этом файле. Очень удобно.

Заключение

А данной статье мы рассмотрели довольно безболезненный процесс переноса разработки и запуска приложения в Docker-окружение, интегрировали RoadRunner и используя простой CI сценарий автоматизировали сборку и тестирование образа с нашим приложением.

Товарищам *ops-ам — брать образ с нужным тегом и раскатывать его на своих кластерах. Разработчикам остается после клонирования репозитория выполнить make git-hooks && make install && make up и начать писать полезный код.

Естественно — данная схема тоже является упрощенной, и на "боевых" проектах накручиваю ещё много всего, но если изложенный в статье подход поможет кому-то — я буду знать, что потратил время не зря.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть