Официальная документация 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 использует свой класс ядра, который не загружает список запланированных задач, но мы попросили предоставить нам консольное ядро (Illuminate\Contracts\Console\Kernel) – 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