Официальная документация Laravel достаточно подробно описывает установку веб-приложения и сопутствующих процессов-работников, но что если я хочу развернуть продукт в среде AWS Elastic Beanstalk?
Как оказалось, об этом практически нет статей в Интернете, нет готовых пакетов на Packagist, нет упоминания в документации.
Эта статья не только покажет как можно легко и просто запустить планировщик и обработчик очередей в AWS, но также в очередной раз докажет, что Laravel очень легко расширяется.
Что такое Elastic Beanstalk?
Для тех, кто не знаком с сервисом EB от AWS, попробую объяснить в двух предложениях. Elastic Beanstalk – это готовая связка сервисов (виртуальные сервера, балансировщики нагрузки, мониторинг) для автоматического масштабирования приложений. Благодаря EB, в команде не обязательно иметь DevOps, и приложение будет само адаптироваться под любую нагрузку.
Elastic Beanstalk: особенности
Amazon предлагает отдельный, специальный вид окружения для приложений-работников – окружение 'worker'. И несмотря на то, что AWS позволяет запускать и запланированные задачи, и задачи из очередей, процесс отличается от стандартного:
В стандартном процессе, Laravel вставляет задачи в очередь, а другая копия этого же приложения опрашивает очередь периодически, надеясь получить задачу. Запланированные задачи обрабатываются внутренним планировщиком Laravel, который в свою очередь запускается каждую минуту через стандартный UNIX cron tab.
А вот в среде AWS EB, мы уже не сможем устанавливать свои cron файлы или работать с очередью напрямую:
Вместо этого, внутренний процесс AWS будет слать нам POST запросы, оповещая наши копии приложений о запланированных задачах, готовых к выполнению, или о новых задачах в очереди. Звучит достаточно просто, но Laravel (текущая версия – 5.2) не поддерживает ни то, ни другое – планировщик запускается только из консоли, а обработчик очередей хочет доступа в очередь напрямую.
Реализация
Планировщик
Начнем с планировщика. Мы хотим, чтобы происходило тоже самое, что происходит при запуска в консоли php artisan schedule:run
, но из web-запроса (web-хука). Создавать отдельные хуки (некоторые разработчики выбирают этот путь) не хочется, так как:
- Хочется полагаться на встроенный планировщик Laravel – синтаксис удобнее для чтения, разрабтчикам не требуются знания UNIX, бизнес-логика остается в приложении, а не за его пределами;
- Другие среды, в которых приложение работает (локальная, development) могут быть не в AWS, и мы не хотим иметь два разных способа работы для AWS и не-AWS вариантов;
- Не хочется создавать кучу методов-хуков, которые будут использоваться лишь AWS.
Так выглядит финальная версия метода контроллера, который запускает планировщик. Метод очень похож на встроенный в Laravel ScheduleRunCommand::class:
/**
* @param Container $laravel
* @param Kernel $kernel
* @param Schedule $schedule
* @return array
*/
public function schedule(Container $laravel, Kernel $kernel, Schedule $schedule)
{
$events = $schedule->dueEvents($laravel);
$eventsRan = 0;
$messages = [];
foreach ($events as $event) {
if (! $event->filtersPass($laravel)) {
continue;
}
$messages[] = 'Running: '.$event->getSummaryForDisplay();
$event->run($laravel);
++$eventsRan;
}
if (count($events) === 0 || $eventsRan === 0) {
$messages[] = 'No scheduled commands are ready to run.';
}
return $this->response($messages);
}
Пожалуй, самая важная строка. Как мы знаем, Laravel попытается предоставить все указанные в списке параметров зависимости, но в данном случае нам нужен побочный эффект от этого:
public function schedule(Container $laravel, Kernel $kernel, Schedule $schedule)
Web-приложение Laravel использует свой класс ядра, который не загружает список запланированных задач, но мы попросили предоставить нам консольное ядро (IlluminateContractsConsoleKernel) – Laravel'у придется его загрузить для нас. В процессе загрузки произойдет 'побочный' эффект – будут загружены запланированные задачи из App/Console, наконец-то приложение о них узнает. Когда Laravel будет предоставлять следующую зависимость, класс Schedule – у приложения уже будут задачи.
Важная деталь: поменяйте местами Kernel и Schedule в списке параметров и метод перестанет работать. Ядро надо загрузить перед Schedule, потому что нам нужен побочный эффект от его загрузки.
То, что происходит дальше, достаточно просто и понятно, почти повторяет ScheduleRunCommand. Было бы прекрасно использовать существующий класс, но к сожалению его нельзя расширить или переопределить.
Очереди
Одной из целей было свести количество новых классов к минимуму, так что я не стал вводить свои очереди или соединения – удалось обойтись лишь одним job-классом, который будет передан стандартному обработчику очередей.
Метод получился таким:
/**
* @param Request $request
* @param Worker $worker
* @param Container $laravel
* @return array
*/
public function queue(Request $request, Worker $worker, Container $laravel)
{
$this->validateHeaders($request);
$body = $this->validateBody($request, $laravel);
$job = new AwsJob($laravel, $request->header('X-Aws-Sqsd-Queue'), [
'Body' => $body,
'MessageId' => $request->header('X-Aws-Sqsd-Msgid'),
'ReceiptHandle' => false,
'Attributes' => [
'ApproximateReceiveCount' => $request->header('X-Aws-Sqsd-Receive-Count')
]
]);
try {
$worker->process(
$request->header('X-Aws-Sqsd-Queue'), $job, 0, 0
);
} catch (Exception $e) {
return $this->response([
'Couldn't process ' . $job->getJobId()
], 500);
}
return $this->response([
'Processed ' . $job->getJobId()
]);
}
Все, что было нужно сделать – это вытащить метаданные SQS из HTTP заголовков, и вставить их в job-класс. Получился эдакий адаптер с HTTP на SQS. Нам не надо самим удалять работу из очереди или помечать ее как неудачную, все сделает сам AWS. Если мы не вернем HTTP код 200 (к примеру, мы поймали ошибку), то AWS сам сделает все последующее.
Вот и всё! Осталось добавить пару маршрутов (всего два маршрута на любое количество задач) и приложение готово к бою!
Настройка AWS
Не забудьте подписать worker-окружение AWS на соответствующую очередь SQS (или топик SNS).
Чтобы AWS начал "приставать" ежеминутно, в момент предоставления новой версии приложения в корне должен быть файл cron.yaml. Можно добавить его в репозиторий, а можно добавлять на последнем шаге. Содержимое файла:
version: 1
cron:
- name: "schedule"
url: "/worker/schedule"
schedule: "* * * * *"
Выводы
Laravel в очередной раз доказал свои гибкость и расширяемость.
Полный исходный код, рабочий пакет с интеграцией для Laravel и Lumen уже залил на GitHub (и Packagist): https://github.com/dusterio/laravel-aws-worker
Автор: dusterio