koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

Laravel を AWS App Runner で動かしてみる

App RunnerなどのHTTPリクエストをベースにしたコンテナランナーでLaravelのようなフルスタックのWebアプリケーションフレームワークを動作させる構成の例は意外に見つからない。

そもそもLaravelの公式には本番運用のノウハウはあまり提供されていない(Forge / Vapor が収益源だからか?)。

クラウドプロバイダの公式に提示されるのはあくまでHTTPリクエストを受け付ける部分だけなことが多く、実際にWebサービスを提供するための情報としては片手落ち感が強い。

サービス提供にあたってはHTTPリクエスト以外の要素も存在するからだ。

他の要素には他の実行環境を用意する? それでは平易にコンテナを動かせる利点が半減してしまう。ここしばらくApp RunnerでLaraveを動かす試行をしていたのでまとめておく。

現状は何となく動かす分には不足なさそうだった。

試した結果のリポジトリ

github.com

主に Dockerfile とそれにコピーしている設定ファイルを見てもらえばよいかと思う。

Laravel の基本構成要素

Laravelというよりは、Webサービスの典型的な構成要素と言ってもいいかもしれない。概ね以下の3要素になると考えている。

  1. HTTPのリクエスト処理
    • LaravelのWebアプリケーションの部分
  2. 非同期なジョブ処理
    • QueueとWorker
    • artisan queue:work で処理する部分
    • artisan コマンドが常駐してメッセージをPull
  3. 定期的なジョブ処理
    • スケジューラ
    • artisan schedule:run で処理する部分
    • 定期的に artisan コマンドを実行

このうちHTTPのリクエスト処理は世の中に例が溢れているし、今回焦点を当てているApp Runnerでも自然に処理できる。

そのため、主に後者2つについて考えることになる。

Laravel をコンテナで動かす際の考え方

これはあまり世の中に出回っていないが、1つの回答として以下のようなものがあるようだった。

  1. NginxなどのHTTP受付とphp-fpmなどのPHP実行のコンテナを分けるのか?
    • 1つのコンテナで処理してしまう
    • supervisord などで複数のプロセスを動作させる
  2. スケジュールされた処理などをどう動かすのか?
    • コンテナ内で処理してしまう
    • supervisord でキューをポーリングしたりスケジュールのプロセスを動作させる

要するに非コンテナ環境で動かす際と同様に、1つのコンテナで全部処理してしまう形だ。

負荷の大きなアプリケーションになってくると破綻しそうだが、そうなったらまた別の構成を考えればよい、と取り合えず全部入りコンテナで動かそう、という割り切りは有効に思える。

ググってもあまり例は出てこなかったが、もう一歩進んでECSである程度責務分担した例は以下の記事が分かりやすかった。

kotamat.com

今回の構成

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リクエストを並列処理したらアクティブなインスタンスをスケールアウトするかを設定できる。

aws.amazon.com

HTTPリクエスト以外を処理している場合にこれらがどう推移するのかを観察してみた。

想定している使い方なのかは不明なため、あくまでこの試行を行ったときにはそう振舞っていた、である点に留意されたい。 (Clour RunなどはHTTP以外の処理を想定した造りになっており案内もあるが、App Runnerにはそうしたものはないはず)

  1. アクティブなインスタンスは0まで減る
  2. プロビジョンされたインスタンスは設定した最小サイズまで減る
  3. アクティブでなくなった後もプロビジョンされたインスタンスはしばらく残る

アクティブなインスタンスは0まで減る

これはCloudWatchにメトリクスがあるのでそれで確認できる

HTTPのリクエストがなくなるとすぐにアクティブなインスタンスが減る

プロビジョンされたインスタンスは設定した最小サイズまで減る / アクティブでなくなった後もプロビジョンされたインスタンスはしばらく残る

こちらはメトリクスはないのだが、定期的な処理のログがアクティブなインスタンスが減った後も継続して出力されるインスタンスがある一方、5分程度で途切れるインスタンスもあった。

最小サイズ分のプロビジョンされたインスタンスではCPUが完全に止まるわけではなく、バックグラウンドの処理は継続して行われていそうだった(そうでないのミリ秒のレイテンシでHTTPリクエストを受け付けられないだろう)。

正直CPUに課金されない状態で処理が動いていていいのか分かっていないが、現状はとりあえずHTTPリクエスト以外の処理も動かせそうだ。

1つのインスタンスはスケジュールのログが継続して出力されていた

別のインスタンスは5分程度で途切れた

観察時の設定

https://github.com/7474/laravel-app-runner/blob/39277389243fbf3d11a1bc28efef3a8f198ba50b/terraform/app-runner.tf#L30-L37

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を使わないほうがいいと考えている。

koudenpa.hatenablog.com

他方Oracle Cloudを使うよりはApp Runnerを使うほうがいいんじゃないか。

1つのコンテナに全てを押し込めればWebサービスの構成要素を何となく動かせそうだった。

色々文句も言っているが、CodeXxxを使ってCDを構成しなくてもデリバリできるのはなんだかんだで楽だと思う。

追記: 不意の終了対応

コンテナのスケールインがいつ起こるかは分からないので、不意にプロセスが終了してもよいように構成する必要がある。

ECSなどなら「終了前にこうなる」と定義されているが、App Runnerでは「HTTPリクエストの処理さえしていなければいつでも終了されてよい」が建前になる。

であるからには不意にPHPのプロセスが終了してもなるべく影響が出ない構成をとっておくのが良い。

JobのリトライとRDBトランザクションを当てにしておけばよいかと思う。実際問題としてはQueleのドライバにRedisはやめておく(メッセージがDequeueしたら消えるため)、位で十分ではなかろうか。