Laravel6 on Google App Engineでキューワーカを動かす


TL;DR

LaravelのキューワーカをApp Engineで動かすにはフレキシブル環境を使いましょう。

App EngineでのSupervisor

Laravelで重い処理を実行するときにキューを使うことはよくあると思います。
そしてキューワーカの永続化にはSupervisorを使うのが一般的です。
App EngineでSupervisorを使うにはどうしたらいいのでしょうか。

App Engineの環境

App EngineのPHP環境にはスタンダード環境とフレキシブル環境の2種類があります。
スタンダード環境には無料枠があること。フレキシブル環境のほうが多少設定の融通が効くことなどの違いがあります。

https://cloud.google.com/appengine/docs/php/?hl=ja
https://cloud.google.com/appengine/docs/flexible/php/runtime?hl=ja#configuring_supervisord_in_the_php_runtime

ドキュメントを読むと、フレキシブル環境にしかキューワーカの永続化に必要なSupervisorの記述がありませんでした。
ということはスタンダード環境ではSupervisorが動かせないのでしょうか。

Laravel6 on Google App Engineでのキュー実装

以前の記事、

こちらで作ったLaravel6アプリにキューを使った処理を実装して試してみます。

$ php artisan queue:table
$ php artisan queue:failed-table
$ php artisan make:migration create_table_queue_logs --create=queue_logs
database/migrations/create_table_queue_logs.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTableQueueLogs extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('queue_logs', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('message');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('queue_logs');
    }
}
$ php artisan migrate
$ php artisan make:model QueueLog
$ php artisan make:job SampleJob
app/Jobs/SampleJob.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\QueueLog;

class SampleJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $log = new QueueLog();
        $log->message = 'Dequeue';
        $log->save();
    }
}
routes/web.php
<?php

use App\QueueLog;
use App\Jobs\SampleJob;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('/enqueue', function () {
    $log = new QueueLog();
    $log->message = 'Enqueue';
    $log->save();

    SampleJob::dispatch();

    return 'Enqueue.';
});

QUEUE_CONNECTIONの設定をapp.yamlに追加します。

app.yaml
runtime: php72

env_variables:
  ## Put production environment variables here.
  APP_KEY: YOUR_APP_KEY
  APP_STORAGE: /tmp
  VIEW_COMPILED_PATH: /tmp
  CACHE_DRIVER: database
  SESSION_DRIVER: database
  QUEUE_CONNECTION: database
  ## Set these environment variables according to your CloudSQL configuration.
  DB_DATABASE: YOUR_DB_DATABASE
  DB_USERNAME: YOUR_DB_USERNAME
  DB_PASSWORD: YOUR_DB_PASSWORD
  DB_SOCKET: "/cloudsql/YOUR_CONNECTION_NAME"

Supervisor用の設定としてadditional-supervisord.confをプロジェクトのルートディレクトリに追加します。

additional-supervisord.conf
[program:queue-worker]
command = php %(ENV_APP_DIR)s/artisan queue:work
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes=0
user = www-data
autostart = true
autorestart = true
priority = 5
stopwaitsecs = 20

スタンダード環境にデプロイ

$ gcloud app deploy

routes/web.phpで設定した/enqueueにアクセスするとjobsテーブルとqueue_logsテーブルにデータは格納されますが、それがデキューされてジョブが実行されていません。
スタンダード環境ではSupervisorは動きませんでした。

フレキシブル環境にデプロイ

次にフレキシブル環境にデプロイします。
フレキシブル環境にデプロイするにはapp.yamlenv: flexdocument_root: publicを追加します。
また、Cloud SQL用にUNIXドメインソケットを有効にするためのcloud_sql_instancesも追加します。
IAM管理において、App Engineフレキシブル環境のサービスエージェントにCloud SQL ClientのIAM役割を与えることも必要です(これを忘れてキューワーカの起動に失敗して散々悩みました…)

app.yaml
runtime: php
env: flex

runtime_config:
  document_root: public

beta_settings:
  cloud_sql_instances: YOUR_CONNECTION_NAME

env_variables:
  ## Put production environment variables here.
  APP_KEY: YOUR_APP_KEY
  APP_STORAGE: /tmp
  VIEW_COMPILED_PATH: /tmp
  CACHE_DRIVER: database
  SESSION_DRIVER: database
  QUEUE_CONNECTION: database
  ## Set these environment variables according to your CloudSQL configuration.
  DB_DATABASE: YOUR_DB_DATABASE
  DB_USERNAME: YOUR_DB_USERNAME
  DB_PASSWORD: YOUR_DB_PASSWORD
  DB_SOCKET: "/cloudsql/YOUR_CONNECTION_NAME"
$ gcloud app deploy

デプロイはスタンダード環境よりも時間がかかります。
数分かかるのでコーヒーも淹れられます

/enqueueにアクセスして、データベースを確認したところ、ジョブが実行されたことが確認出来ました

戸惑ったところ

フレキシブル環境でデプロイした後にスタンダード環境に切り替えてデプロイしたら、古いフレキシブル環境のインスタンスが残ったままで、そこのキューワーカが動いて、スタンダード環境なのにキューワーカが正常実行されていると錯覚してしまうことがありました。

まとめ

Laravel on Google App Engineでキューを使いたい場合はフレキシブル環境を使いましょう
でもApp Engineの魅力はスタンダード環境のシンプルで速いデプロイにあると思うので、
デプロイに数分かかってしまうフレキシブル環境はどうもなーと思ってしまいます。

スタンダード環境にこだわりたい場合は、キューの処理をCloud Functionsに任せる手もありますが、Cloud FunctionsではPHPがサポートされていないので言語を揃えたいときは難しいところ。
そもそもGCPでPHPは歓迎されてないのでは、という気もしますが。。。

参考URL

https://cloud.google.com/appengine/docs/php/?hl=ja
https://cloud.google.com/appengine/docs/flexible/php/runtime?hl=ja#configuring_supervisord_in_the_php_runtime
https://cloud.google.com/sql/docs/mysql/connect-app-engine?hl=ja#configuring