Официальная документация Laravel достаточно подробно описывает установку веб-приложения и сопутствующих процессов-работников, но что если я хочу развернуть продукт в среде AWS Elastic Beanstalk?


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


Эта статья не только покажет как можно легко и просто запустить планировщик и обработчик очередей в AWS, но также в очередной раз докажет, что Laravel очень легко расширяется.


Что такое Elastic Beanstalk?


Для тех, кто не знаком с сервисом EB от AWS, попробую объяснить в двух предложениях. Elastic Beanstalk – это готовая связка сервисов (виртуальные сервера, балансировщики нагрузки, мониторинг) для автоматического масштабирования приложений. Благодаря EB, в команде не обязательно иметь DevOps, и приложение будет само адаптироваться под любую нагрузку.


Elastic Beanstalk: особенности


Amazon предлагает отдельный, специальный вид окружения для приложений-работников – окружение 'worker'. И несмотря на то, что AWS позволяет запускать и запланированные задачи, и задачи из очередей, процесс отличается от стандартного:


Обработка очередей в Laravel

В стандартном процессе, Laravel вставляет задачи в очередь, а другая копия этого же приложения опрашивает очередь периодически, надеясь получить задачу. Запланированные задачи обрабатываются внутренним планировщиком Laravel, который в свою очередь запускается каждую минуту через стандартный UNIX cron tab.


А вот в среде AWS EB, мы уже не сможем устанавливать свои cron файлы или работать с очередью напрямую:


Обработка очередей в AWS EB

Вместо этого, внутренний процесс 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