Хотелось бы начать с того, что оказалось «не лучшими практиками» в контексте предыдущей статьи:
- Необходимость изменять структуру файлов в репозитории
- Использование FPM. Если мы хотим производительности от своих приложений то, пожалуй, одним из лучших решений ещё на стадии выбора технологий будет отказ от него в пользу чего-то более быстрого и «приспособленного» к тому, что память может утекать. RoadRunner тут оказывается как никогда кстати
- Отдельный образ с исходниками и ассетами. Не смотря на то, что используя такой подход мы можем реюзать один и тот же образ для построения более сложной маршрутизации входящих запросов (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
Наглядное внедрение нового подхода
Для наглядной демонстрации — весь процесс разобью на несколько этапов, изменения в рамках которых будут оформлены в виде отдельных MR (после слияния все бранчи останутся на своих местах; ссылки на MR в заголовках «шагов»). Отправная точка — это скелетон Laravel приложения созданный с помощью composer create-project latavel/laravel
:
$ 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.*"
Шаг 1 — Докеризация + RR
Первым делом необходимо научить приложение запускаться в контейнере. Для этого нам нужны Dockerfile
, docker-compose.yml
для описания «как поднимать и линковать контейнеры», и Makefile
для того, чтобы свести и без того упрощенный процесс к одной-двум командам.
Dockerfile
Базовый образ использую php:X.X.X-alpine
как наиболее легкий и содержащий то, что надо для запуска. Более того — все последующие обновления интерпретатора сводятся к тому, чтобы просто изменить значение в этой строчке (обновить PHP теперь проще некуда).
Composer и бинарный файл RoadRunner доставляются в контейнер с помощью multistage и COPY --from=...
— это очень удобно, да и все значения связанные с версиями не «разбросаны», а находятся в начале файла. Работает это быстро, и без зависимостей от curl
/ git clone
/ make build
. Образы 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
Тут смысл следующий — отдельным слоем в образ доставляются файлы composer.lock
и composer.json
, после чего выполняется установка всех зависимостей, описанных в них. Делается это для того, чтобы при последующих сборках образа с использованием --cache-from
, если состав и версии установленных зависимостей не изменились, то composer install
не выполнялся, взяв этот слой из кэша, тем самым экономя время сборки и трафик (за идею спасибо jetexe).
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
.
Для того, чтобы была возможность использовать
dump(..)
иdd(..)
и всё при этом работало, есть другой пакет —avto-dev/stacked-dumper-laravel
. Всё, что потребуется — это добавлять пефикс к этим хэлперам, а именно\dev\dd(..)
и\dev\dump(..)
соответственно. Без этого будете наблюдать ошибку вида:worker error: invalid data found in the buffer (possible echo)
После всех манипуляций выполняем docker-compose up -d
и вуа-ля:
База данных PostgeSQL, redis и воркеры RoadRunner успешно запущены в контейнерах.
Шаг 2 — Makefile и тесты
Как уже писал ранее, Makefile — очень недооцененная штука. Зависимые цели, свой синтаксический сахар, 99% вероятность того, что на linux/mac машине разработчика он уже стоит, автокомплит «из коробки» — малый список его преимуществ.
Добавив его в наш проект и выполнив make
без параметров, мы можем наблюдать:
Для запуска юнит-тестов мы можем как выполнить make test
, так и получив шелл внутрь контейнера с приложением (make shell
) выполнить composer phpunit
. Для получения coverage отчета достаточно выполнить make test-cover
, и перед запуском тестов в контейнер доставится xdebug с его зависимостями, и запустятся тесты (так как эта процедура выполняется не часто и не силами CI — это решение кажется лучшим, чем держать отдельный образ со всеми dev-примочками).
Git Hooks
Хуки в нашем случае будут выполнять 2 важные роли — не позволять пушить в origin код, тесты которого не выполняются успешно; и автоматически ставить все необходимые зависимости, если стянув изменения себе на машину окажется, что composer.lock
изменился. В Makefile
для этого существует отдельный target:
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
вместо того, чтоб просто нажимать хоткей прямо во время написания теста или кода, с ним связанного.
Нажимаем File > Settings > Languages & Frameworks > PHP > CLI interpreter > [...] > [+] > From Docker, Vargant, VM, Remote
. Выбираем Docker compose, и имя сервиса app.
Второй шаг — это указание phpunit-у необходимость использовать интерпретатор из контейнера: File > Settings > Test frameworks > [+] > PHPUnit by remote interpreter
и выбрать ранее созданный удаленный интерпретатор. В поле Path to script
указываем /app/vendor/autoload.php
, а в Path mappings
указываем корневую директорию проекта как монтируемую в /app
.
И теперь мы можем запускать тесты прямо из IDE используя интерпретатор внутри образа с приложением, нажимая (по дефолту, Linux) Ctrl + Shift + F10.
Шаг 3 — Автоматизация
Всё, что нам остается сделать — это автоматизировать процесс запуска тестов и сборки образа. Для этого создаем файл .gitlab-ci.yml
в корневой директории приложения, наполняя его примерно следующим содержанием. Основная идея данной конфигурации — быть максимально простой, но не терять в функциональности при этом.
Сборка образа производится на каждом бранче, на каждом коммите. Используя --cache-from
сборка образа при повторном коммите производится очень быстро. Необходимость пересборки обусловлена тем, что на каждом бранче у нас есть образ с теми изменениями, которые были в рамках этого бранча сделаны, а как следствие — ничего нам не мешает его раскатать на swarm/k8s/etc для того, что бы «вживую» убедиться в том, что всё работает, и работает как надо ещё до мерджа с master
-веткой.
После сборки — запускаем unit-тесты и проверяем запуск приложения в контейнере, отправляя на health-check endpoint запросы curl-ом (данное действие опционально, но несколько раз данный тест меня очень выручал).
Для «выпуска релиза» — просто публикуем тег вида vX.X.X
(если вы ещё и будете придерживаться семантического версионирования — будет очень круто) — CI соберет образ, прогонит тесты, и выполнит действия, что вы укажете в deploy to somewhere
.
Не забудьте в настройках проекта (если это возможно) ограничить возможность публикации тегов только лицам, которым разрешено «выпускать релизы».
CHANGELOG.md
и ENVIRONMENT.md
Перед тем, как принять тот или иной MR — проверяющий должен в обязательном порядке проверить на соответствие файлы CHANGELOG.md
и ENVIRONMENT.md
. Если с первым всё более и менее понятно, то вот относительного второго дам пояснения. Данный файл служит для описания всех переменных окружения, на которые реагирует контейнер с приложением. Т.е. если разработчик добавляет или удаляет поддержку той или иной переменной окружения — это обязательно должно быть отражено в этом файле. И в момент, когда возникает вопрос «Нам нужно срочно переопределить то-то и то-то» — никто судорожно не начинает копаться в документации или исходниках — а смотрит в одном-единственном файле. Очень удобно.
Заключение
А данной статье мы рассмотрели довольно безболезненный процесс переноса разработки и запуска приложения в Docker-окружение, интегрировали RoadRunner и используя простой CI сценарий автоматизировали сборку и тестирование образа с нашим приложением.
Разработчикам остается после клонирования репозитория выполнить make git-hooks && make install && make up
и начать писать полезный код. Товарищам *ops-ам — брать образ с нужным тегом и раскатывать его на своих кластерах.
Естественно — данная схема тоже является упрощенной, и на «боевых» проектах накручиваю ещё много всего, но если изложенный в статье подход поможет кому-то — я буду знать, что потратил время не зря.