App RunnerなどのHTTPリクエストをベースにしたコンテナランナーでLaravelのようなフルスタックのWebアプリケーションフレームワークを動作させる構成の例は意外に見つからない。
そもそもLaravelの公式には本番運用のノウハウはあまり提供されていない(Forge / Vapor が収益源だからか?)。
クラウドプロバイダの公式に提示されるのはあくまでHTTPリクエストを受け付ける部分だけなことが多く、実際にWebサービスを提供するための情報としては片手落ち感が強い。
サービス提供にあたってはHTTPリクエスト以外の要素も存在するからだ。
他の要素には他の実行環境を用意する? それでは平易にコンテナを動かせる利点が半減してしまう。ここしばらくApp RunnerでLaraveを動かす試行をしていたのでまとめておく。
現状は何となく動かす分には不足なさそうだった。
- 試した結果のリポジトリ
- Laravel の基本構成要素
- Laravel をコンテナで動かす際の考え方
- 今回の構成
- App Runner の振る舞い
- App Runner でHTTPのリクエスト以外を動かしても大丈夫か?
- 追記: 不意の終了対応
試した結果のリポジトリ
主に Dockerfile とそれにコピーしている設定ファイルを見てもらえばよいかと思う。
- https://github.com/7474/laravel-app-runner/blob/app-runner-v1/laravel/Dockerfile
- https://github.com/7474/laravel-app-runner/tree/app-runner-v1/laravel/docker/cloud
Laravel の基本構成要素
Laravelというよりは、Webサービスの典型的な構成要素と言ってもいいかもしれない。概ね以下の3要素になると考えている。
- HTTPのリクエスト処理
- LaravelのWebアプリケーションの部分
- 非同期なジョブ処理
- QueueとWorker
artisan queue:work
で処理する部分- artisan コマンドが常駐してメッセージをPull
- 定期的なジョブ処理
- スケジューラ
artisan schedule:run
で処理する部分- 定期的に artisan コマンドを実行
このうちHTTPのリクエスト処理は世の中に例が溢れているし、今回焦点を当てているApp Runnerでも自然に処理できる。
そのため、主に後者2つについて考えることになる。
Laravel をコンテナで動かす際の考え方
これはあまり世の中に出回っていないが、1つの回答として以下のようなものがあるようだった。
- NginxなどのHTTP受付とphp-fpmなどのPHP実行のコンテナを分けるのか?
- 1つのコンテナで処理してしまう
- supervisord などで複数のプロセスを動作させる
- スケジュールされた処理などをどう動かすのか?
- コンテナ内で処理してしまう
- supervisord でキューをポーリングしたりスケジュールのプロセスを動作させる
要するに非コンテナ環境で動かす際と同様に、1つのコンテナで全部処理してしまう形だ。
負荷の大きなアプリケーションになってくると破綻しそうだが、そうなったらまた別の構成を考えればよい、と取り合えず全部入りコンテナで動かそう、という割り切りは有効に思える。
ググってもあまり例は出てこなかったが、もう一歩進んでECSである程度責務分担した例は以下の記事が分かりやすかった。
今回の構成
supervisord で以下のプロセスを動作させる構成にした。
https://github.com/7474/laravel-app-runner/blob/app-runner-v1/laravel/docker/cloud/supervisord.conf
- Nginx
- Nginx
command=/usr/sbin/nginx -g 'daemon off;'
- php-fpm
- php-fpm
command=/usr/local/sbin/php-fpm -R
- laravel-schedule
artisan schedule:run
- 毎分実行するようにループ
- laravel-queue
artisan queue:work
スケジュールの重複実行
実行環境がスケールアウトすると、スケジュールが複数インスタンスで実行される点には注意が必要になる。
これに関しては非コンテナ環境でスケールアウトする際と同様なので、Laravelには対応手法がある。並列動作すると不都合のあるジョブは onOneServer
しておけばよい。
https://laravel.com/docs/9.x/scheduling#running-tasks-on-one-server
DB Migration のタイミング
https://github.com/7474/laravel-app-runner/blob/app-runner-v1/laravel/docker/cloud/start-container
コンテナ起動時に php artisan migrate --force
した。
一般的にはバッドプラクティスだが、実行環境は確実にDBと接続できる状態であるため、マイグレーションのCIを構成する手間をかけるほどではない場合は便利だろうと考えている。
App Runner の振る舞い
App Runnerにはプロビジョンされたインスタンスと、アクティブなインスタンスという概念がある。
前者はメモリだけに課金される状態、後者はメモリとCPUに課金される状態で、いくつHTTPリクエストを並列処理したらアクティブなインスタンスをスケールアウトするかを設定できる。
HTTPリクエスト以外を処理している場合にこれらがどう推移するのかを観察してみた。
想定している使い方なのかは不明なため、あくまでこの試行を行ったときにはそう振舞っていた、である点に留意されたい。 (Clour RunなどはHTTP以外の処理を想定した造りになっており案内もあるが、App Runnerにはそうしたものはないはず)
アクティブなインスタンスは0まで減る
これはCloudWatchにメトリクスがあるのでそれで確認できる
プロビジョンされたインスタンスは設定した最小サイズまで減る / アクティブでなくなった後もプロビジョンされたインスタンスはしばらく残る
こちらはメトリクスはないのだが、定期的な処理のログがアクティブなインスタンスが減った後も継続して出力されるインスタンスがある一方、5分程度で途切れるインスタンスもあった。
最小サイズ分のプロビジョンされたインスタンスではCPUが完全に止まるわけではなく、バックグラウンドの処理は継続して行われていそうだった(そうでないのミリ秒のレイテンシでHTTPリクエストを受け付けられないだろう)。
正直CPUに課金されない状態で処理が動いていていいのか分かっていないが、現状はとりあえずHTTPリクエスト以外の処理も動かせそうだ。
観察時の設定
resource "aws_apprunner_auto_scaling_configuration_version" "this" { auto_scaling_configuration_name = var.name # 動作確認のために少なく設定している max_concurrency = 5 max_size = 2 min_size = 1 }
App Runner でHTTPのリクエスト以外を動かしても大丈夫か?
今のところ大丈夫そう。
今後どうなるかは分からない。
先にも書いた通り、ミッションクリティカルな用途ではApp Runnerを使わないほうがいいと考えている。
他方Oracle Cloudを使うよりはApp Runnerを使うほうがいいんじゃないか。
1つのコンテナに全てを押し込めればWebサービスの構成要素を何となく動かせそうだった。
色々文句も言っているが、CodeXxxを使ってCDを構成しなくてもデリバリできるのはなんだかんだで楽だと思う。
追記: 不意の終了対応
コンテナのスケールインがいつ起こるかは分からないので、不意にプロセスが終了してもよいように構成する必要がある。
ECSなどなら「終了前にこうなる」と定義されているが、App Runnerでは「HTTPリクエストの処理さえしていなければいつでも終了されてよい」が建前になる。
であるからには不意にPHPのプロセスが終了してもなるべく影響が出ない構成をとっておくのが良い。
JobのリトライとRDBのトランザクションを当てにしておけばよいかと思う。実際問題としてはQueleのドライバにRedisはやめておく(メッセージがDequeueしたら消えるため)、位で十分ではなかろうか。