Перейти к содержанию

Docker для локальной веб-разработки - часть 8

Всем привет! Эта статья продолжает серию переводов замечательных статей об использовании Docker для локальной веб-разработки.

В этой серии⚓︎

Введение⚓︎

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

Первым шагом в этом случае обычно является попытка вписать в картину привычный cron — и это часто оказывается на удивление болезненным. В лучшем случае мы получаем crontab, который импортируем и устанавливаем в контейнер через Dockerfile соответствующего образа (что-то вроде этого); чаще всего, однако, мы получаем что-то гораздо более неуклюжее.

В конце предыдущей части мы остались с планировщиком Laravel для ручного запуска, который, как мы ожидаем, будет обработан как запись в cron. Чтобы решить эту проблему в нашей установке, мы можем использовать подход, аналогичный описанному выше — для этого потребуется версия образа, в которой запущенный процесс будет демоном cron, а не PHP-FPM, что может быть достигнуто с помощью отдельного этапа сборки. Нам также понадобится сервис в docker-compose.yml, нацеленный на новый этап, чтобы собрать и использовать соответствующий образ.

Это было бы не так уж плохо, и, честно говоря, в большинстве случаев этого было бы достаточно. Но это не масштабируемо, и это не путь Docker.

Представьте, что ваше приложение становится всё сложнее, и вы решаете добавить микросервис, возможно, чтобы разбить монолит или сделать что-то совершенно другое, используя другой язык. Чем бы ни занимался этот микросервис, представьте, что ему нужно периодически выполнять собственные задачи. Следуя той же логике, что и выше, вы получите два новых сервиса в docker-compose.yml: один для запуска микросервиса обычным способом (например, как API), а другой для управления запланированными задачами с помощью cron. Теперь представьте, что вам нужен ещё один микросервис, также с собственными заданиями cron — это ещё два новых сервиса, которые нужно добавить в docker-compose.yml. Повторяйте и повторяйте — вы видите, к чему всё идет.

Одна из самых сильных сторон Docker, на мой взгляд, заключается в том, что он позволяет нам мыслить на уровне системы, а не приложения — вместо того чтобы рассматривать каждую часть системы отдельно, нам предлагается сделать шаг назад и рассмотреть её как единое целое. Поскольку планирование задач является общей потребностью для многих частей системы, должен существовать способ управлять ими на уровне системы, единым образом, вместо того чтобы решать этот вопрос для каждой части отдельно.

Решением является внедрение независимого, общесистемного планировщика.

Предполагаемой отправной точкой этого руководства является место, где мы остановились в конце предыдущей части, соответствующее ветке part-7 репозитория.

Если вы предпочитаете, вы также можете непосредственно перейти к ветке part-8, которая является конечным результатом этой статьи.

Настройка планировщика⚓︎

Существует множество планировщиков, которые мы можем использовать с Docker; все они работают примерно одинаково, но я остановился на Ofelia из-за его простоты. Ofelia написан на языке Go и позволяет нам быстро определять задачи, которые будут периодически выполняться, ориентируясь на любой из контейнеров нашей установки.

Существуют различные способы определения запланированных задач с помощью Ofelia, в основном отличающиеся тем, где находится конфигурация задач. Лично я предпочитаю подход с использованием файла config.ini, поскольку он не требует обновления других сервисов, обеспечивая минимальную связь и легкую замену в случае необходимости (другой подход к конфигурации подразумевает изменение определения целевых сервисов в docker-compose.yml, что меня устраивает меньше).

Давайте посмотрим, как это выглядит на практике. Создайте новую папку scheduler в корневой директории .docker и добавьте в нее файл config.ini со следующим содержанием:

[job-exec "Laravel Scheduler"]
schedule = @every 1m
container = demo-backend-1
command = php /var/www/backend/artisan schedule:run

Здесь мы определили одно задание — команду Laravel Artisan schedule:run — для выполнения каждую минуту в контейнере бэкенда. job-exec указывает, что мы будем использовать уже запущенный контейнер бэкенда (как вы уже догадались, есть также опция job-run для запуска свежего контейнера), имя которого мы присвоили ключу контейнера: demo-backend-1. На мой взгляд, это единственный небольшой недостаток Ofelia: она не угадывает имя контейнера на основе имени сервиса и требует, чтобы мы указали ожидаемое имя запущенного контейнера.

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

В любом случае, здесь интересно наблюдать, что вместо того, чтобы выполняться изнутри контейнера, команда оказывается открытой и выполняется извне. Именно так следует рассматривать планирование задач в Docker: как некий внутренний API для операций командной строки, централизованно управляемый планировщиком.

Теперь нам нужно определить сервис для этого планировщика, который будет добавлен в docker-compose.yml:

# Scheduler Service
scheduler:
  image: mcuadros/ofelia:latest
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - ./.docker/scheduler/config.ini:/etc/ofelia/config.ini
  depends_on:
    - backend

Обратите внимание на тот факт, что файл config.ini монтируется в контейнер, и что сначала необходимо запустить контейнер backend.

Хотите верьте, хотите нет, но мы практически закончили. Сохраните файл и запустите demo start для загрузки образа Ofelia и запуска планировщика, а затем запустите demo logs scheduler, чтобы убедиться в его работоспособности:

Через минуту или около того вы должны получить результат, подобный приведенному выше, и увидеть, что в файле storage/logs/laravel.log бэкенда регистрируется текущее время, которое является результатом выполнения задания, определенного нами в предыдущей части.

Вот и всё!

При использовании этого подхода конфигурация запланированных заданий полностью отделена от сервисов, на которые они нацелены. Не нужно перестраивать образы сервисов, чтобы изменить записи cron — достаточно обновить config.ini и перезапустить контейнер Ofelia.

Заключение⚓︎

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

Более того, использование планировщика для выполнения задач между системами удобно не только локально — именно так он работает в большинстве продакшен-сред (см., например, Scheduled Tasks от AWS или CronJob от Kubernetes).

С Docker дело обстоит иначе. Если вы раньше не имели дела с DevOps, вам ещё многому придется научиться, но это всё, что вам нужно делать — учиться. С другой стороны, людям, привыкшим работать с виртуальными машинами (локальными или нет), скорее всего, придется труднее, потому что им придется сначала отучиться от части того, что они знают. Поначалу они будут пытаться инициализировать контейнер, как ВМ, или пытаться настроить задания cron, как раньше, — они будут пытаться вставить квадратные колышки в круглые отверстия, просто потому, что привыкли к квадратным отверстиям.

Я тоже прошёл через это.

Docker — это смена парадигмы, и чем раньше мы это примем — чем раньше мы отпустим наши старые способы и примем способ Docker — тем легче нам станет. Однако не поймите меня неправильно: в этом есть существенная польза, ведь возможность мыслить на системном уровне открывает целый новый мир практически бесконечных возможностей.

Сегодняшняя статья — последняя в этой серии, но я не могу закончить её без надлежащего заключения, которое будет в следующей и последней части.


Оригинальная статья: Docker for local web development, part 8: scheduled tasks (English)

Комментарии