開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. AWS >
  4. AWS App RunnerのPHPマネージドランタイムをApache + PHP-FPMの構成で動作させる

AWS App RunnerのPHPマネージドランタイムをApache + PHP-FPMの構成で動作させる

こんにちは、ナカエです。本日はAWS App Runnerについての記事です。

App Runnerのデプロイ方法とマネージドランタイムのサポート追加

AWS App Runnerでは

  • 自前でコンテナを用意するコンテナベースのサービス
  • GitHubリポジトリにコードを用意するコードベースのサービス(マネージドランタイムを利用)

の2パターンを選択できます。

先日、マネージドランタイムに複数の言語が追加され話題になりました。

参考:AWS App Runner がサポートするマネージドランタイムに PHP、Go、.Net、Ruby を追加

PHPのマネージドランタイムの問題点

新しくサポートされた言語には我らがPHPも含まれていましたが、AWS公式ドキュメントを見たPHPer達は口々にこれでは本番環境で使えないと嘆きました。

AWS公式ドキュメントの Using the PHP platform - AWS App Runner には、マネージドランタイムのコンテナでPHPのビルトインウェブサーバを動かすサンプルが掲載されていたからです。

PHPer達の間では、ビルトインウェブサーバを公開ネットワーク上で使ってはいけないことはよく知られています。

参考: PHP: ビルトインウェブサーバー - Manual

警告 このウェブサーバーは、アプリケーション開発の支援用として設計されたものです。 テスト用に使ったり、制約のある環境でアプリケーションをデモするために使ったりすることもできるでしょう。 あらゆる機能を兼ね備えたウェブサーバーを目指したものではないので、 公開ネットワーク上で使ってはいけません。

App RunnerのロードマップのGitHubリポジトリに、PHPのビルトインウェブサーバを使った理由を問う Issue も立つほどでした。

私もせっかくのPHPサポートが勿体ないと感じたため、ビルトインウェブサーバ以外の構成でPHPマネージドランタイムを利用できるのかどうかを調査しました。

結論から述べるとApache HTTP Server(以下Apache) + PHP-FPMの構成で動作させることは可能でした。

今回の記事ではその設定方法を紹介します。

環境(ローカル)

  • OS: macOS Monterey 12.6.1
  • CPU: Intel Core i5
  • zsh 5.8.1
  • PHP: 8.1.11

PHPアプリケーションの作成

まずはPHPアプリケーションのディレクトリを作成します。

mkdir app-runner-managed-php
cd app-runner-managed-php

下記内容のcomposer.jsonを作成します。

composer.json

{
    "name": "nextat/app-runner-managed-php",
    "type": "project",
    "description": "Test App Runner's PHP Managed Runtime.",
    "require": {
        "php": "^8.1.10",
        "monolog/monolog": "^3.2",
        "psr/log": "^3.0"
    },
    "config": {
        "optimize-autoloader": true,
        "sort-packages": true
    },
    "license": "MIT",
    "prefer-stable": true
}

App Runnerのマネージドランタイムに合わせてPHPのバージョンを指定し、ログ出力用のmonolog/monologとpsr/logをインストールする設定です。

Composerを利用して依存をインストールします。

composer install

続いて、public/index.php を作成します。

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

$logger = createLogger();
$env = getenv('APP_ENV') ?? 'unknown';
$logger->info("Logging from PHP application. (env={$env})");

echo "Hello, App Runner PHP Managed Runtime!";

function createLogger(): Psr\Log\LoggerInterface
{
    $logger = new Monolog\Logger('MyApp');
    $logger->pushHandler(new Monolog\Handler\StreamHandler('php://stdout', Monolog\Level::Debug));
    return $logger;
}

標準出力にログを書き込み、Hello, App Runner PHP Managed Runtime! というテキストのレスポンスを返すだけの簡単なアプリケーションです。

php -S localhost:8080 -t ./public

としてビルトインウェブサーバを起動し、https://localhost:8080 にアクセスすると動作を確認できます。

app-runner-managed-php-local-server.png

App Runnerの設定ファイルと各種スクリプト・ファイルの作成

続いてApp Runnerの設定ファイルおよび各種スクリプト・ファイルを作成していきます。

apprunner.yaml

こちらがApp Runnerのマネージドランタイムを使う際の設定ファイルとなります。 実は設定ファイルなしでも簡易的に利用できますが、全体像が掴みやすいため今回は設定ファイルを使うこととします。

参考: https://docs.aws.amazon.com/apprunner/latest/dg/config-file-ref.html

version: 1.0
runtime: php81
build:
  commands:
    pre-build:
      - scripts/pre-build.sh
    build:
      - scripts/build.sh
    post-build:
      - scripts/post-build.sh
run:
  runtime-version: 8.1.10
  command: scripts/entrypoint.sh
  network:
    port: 8080
    env: APP_PORT
  env:
    - name: APP_ENV
      value: "production"

ビルド時にはpre-build、build、post-buildの3つのコマンドを設定できます。今回はそれぞれBashのスクリプトを作成して実行し、挙動を確認することにします。

またrunのcommandでコンテナ起動時に実行するコマンドも指定可能です。こちらも scripts/endtrypoint.sh というシェルスクリプトを実行するよう指定します。

アプリケーションのポートはApp Runnerデフォルトの8080を指定し、APP_ENVという環境変数を追加しています。

なお、記事執筆時点ではPHPのバージョンは8.1.10がサポートされていました。サポートされているPHPのバージョンは下記のページで確認できます。

PHP runtime release information - AWS App Runner

scripts/post-build.sh

コンテナイメージのビルド前に実行するコマンドとして指定したスクリプトです。 今回は特に使いませんが動作だけ確認します。

#!/usr/bin/env bash

echo "pre build"

scripts/build.sh

コンテナイメージのビルド時の実行するコマンドとして指定したスクリプトです。

#!/usr/bin/env bash

set -xe

# Apache httpd config
cp -f /app/conf/httpd/app.conf /etc/httpd/conf.d/app.conf
ln -s /dev/stderr /var/log/httpd/error.log
ln -s /dev/stdout /var/log/httpd/access.log
sed -i -e 's/LoadModule mpm_prefork_module/#LoadModule mpm_prefork_module/g' /etc/httpd/conf.modules.d/00-mpm.conf
sed -i -e 's/#LoadModule mpm_event_module/LoadModule mpm_event_module/g' /etc/httpd/conf.modules.d/00-mpm.conf

# PHP-FPM config
cp -f /app/conf/php-fpm/container.conf /etc/php-fpm.d/container.conf
ln -s /dev/stderr /var/log/php-fpm/error.log
ln -s /dev/stdout /var/log/php-fpm/www-access.log
ln -s /dev/stderr /var/log/php-fpm/www-error.log

# Install Composer
EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]
then
    >&2 echo 'ERROR: Invalid installer checksum'
    rm composer-setup.php
    exit 1
fi

php composer-setup.php
rm composer-setup.php

# Install dependencies
php composer.phar install

Apache用に後述の設定ファイルを追加し、シンボリックリンクを用いてログを標準出力・標準エラー出力に出すよう設定しています。 また、MPM(Multi-Processing Module)をPrefork MPMからEvent MPMに変更しています。

PHP-FPMのログも標準出力・標準エラー出力に向けるため、シンボリックリンクを設定しておきます。

マネージドと謳うからにはcomposer installコマンドくらいは自動で走っているかと思いきや特に走ってはいなかったので、自前実行です。

参考: How do I install Composer programmatically? - Composer

scripts/post-build.sh

コンテナイメージのビルド後に実行するコマンドとして指定するスクリプトです。 こちらもpre-buildと同じく特に使いませんが動作だけ確認します。

#!/usr/bin/env bash

echo "post build"

scripts/entrypoint.sh

コンテナ起動時のコマンドとして指定するスクリプトです。

#!/usr/bin/env bash

set -xe

php-fpm -D

httpd -DFOREGROUND

バックグラウンドでPHP-FPMを、フォアグラウンドでApacheを起動するのみとしていますが、本番環境で利用するためには双方のプロセスを監視するためのプロセスマネージャーを使うかプロセス監視用のスクリプトを書いたほうが良いでしょう。

参考:Run multiple services in a container | Docker Documentation

conf/httpd/app.conf

Apacheの設定ファイルを用意します。

build.shで設定した標準出力・標準エラー出力へのシンボリックリンクへとログの出力先を指定し、8080番ポートで今回作成したPHP製Webアプリケーションを動作させる設定になります。

Listen 8080
ErrorLog /var/log/httpd/error.log
CustomLog /var/log/httpd/access.log combined
<VirtualHost "*:8080">
    ServerName any
    DocumentRoot /app/public
    DirectoryIndex index.php
    <Directory "/app/public">
        AllowOverride All
        Options All
        Require all granted
    </Directory>
</VirtualHost>

conf/php-fpm/container.conf

こちらはPHP-FPMの設定ファイルです。 ログ出力先の設定が主です。

[global]
error_log = /var/log/php-fpm/error.log
log_limit = 8192

[www]
access.log = /var/log/php-fpm/www-access.log
php_admin_value[error_log] =  /var/log/php-fpm/www-error.log
clear_env = no
catch_workers_output = yes
decorate_workers_output = no

実行権限を与える

各ファイルを作成し終わったら、スクリプトには実行権限を与えておきましょう

chmod 744 scripts/*

Gitリポジトリの初期化とコミット

Gitリポジトリの初期化前に.gitignoreファイルを作成しておきましょう。

.gitignore

vendor

今回は最小限のため、vendorディレクトリの除外指定のみです。

Gitリポジトリを初期化し、コミットします。

git init
git add .
git commit -m "initial commit"

GitHubリポジトリの作成とコードのプッシュ

app-runner-managed-php という名前でプライベートリポジトリを作成し、ここまでの内容をpushします。

※ 以下、{GITHUB_ACCOUNT_NAME} は自身のアカウント名で置き換えてください

git remote add origin https://github.com/{GITHUB_ACCOUNT_NAME}/app-runner-managed-php.git
git push -u origin main

App Runnerサービスの作成とデプロイ

コードがGitHubにpushできたら、次はApp Runnerのサービスを作成していきます。

AWSのマネジメントコンソールからApp Runnerの画面を開き、App Runnerサービスを作成をクリックします。

app-runner-managed-php-create-start.png

ステップ1 ソースおよびデプロイ

app-runner-managed-php-create-step1.png

リポジトリタイプはソースコードリポジトリを選択します。 GitHubに接続の項目では、AWS Connector for GitHubというアプリをインストールしてGitHubのリポジトリに接続する必要があります。 接続が未作成であれば、今回作成したGitHubリポジトリに接続できるように 新規追加 を選んで指示に従ってアプリをインストールし、リポジトリへのアクセス権限を追加してください。 接続が作成済みで今回作成したリポジトリのアクセス権がない場合は、GitHub の Settings -> ApplicationsからAWS Connector for GitHubがリポジトリにアクセスできるように設定を変更してください。

接続が作成できたら、リポジトリとブランチを選択して 次へ をクリックします。 デプロイトリガーは今回は手動のままです。

ステップ2 構築を設定

app-runner-managed-php-create-step2.png

設定ファイルを使用 を選択して 次へ をクリックします。

ステップ3 サービスを設定の画面

app-runner-managed-php-create-step3.png

サービス名を適宜入力し(今回はapp-runner-managed-phpとしている)、Auto Scalingはカスタム設定を新規追加してインスタンスの最小サイズと最大サイズを下限の1に絞っておきます。他はデフォルトのまま 次へ をクリックします。

ステップ4 確認および作成

設定内容を確認して 作成とデプロイ をクリックします。 ボタンをクリックすると先ほど作成した app-runner-managed-php のサービスの詳細画面に遷移し、デプロイが開始されます。

App Runnerのサービス詳細画面とログ

「{サービス名} へのデプロイ中です」というメッセージが表示されているはずなので、デプロイが終了するまでは画面の項目やログを確認するなどして待ちましょう。

サービスの概要にはサービスのデフォルトのURLやGitHubのリポジトリのURLが表示されてます。

イベントログ、デプロイログ、アプリケーションログもこの画面で確認可能です。これらの実体はCloudWatch Logsのログストリームとなっています。

  • イベントログ: /aws/apprunner/{サービス名}/{サービスID}/service ロググループ の events ログストリーム
  • デプロイログ: /aws/apprunner/{サービス名}/{サービスID}/service ロググループ の deployment/{オペレーションID} ログストリーム
  • アプリケーションログ: /aws/apprunner/{サービス名}/{サービスID}/application ロググループ の instance/{インスタンスID} ログストリーム

serviceのロググループにはコンテナのビルドなどApp Runnerサービス自体に関するログが記録されおり、applicationのロググループは実際にデプロイされたコンテナの標準出力を記録しています。 ビルド〜デプロイ時に何が起こっているのかを調べるためにはserviceを参照し、コンテナの起動後のログを見るにはapplicationの方を参照する必要があるということですね。

参考: CloudWatch Logs にストリーミングされたアプリケーションランナーのログの表示 - AWS App Runner

実際にデプロイログを見ると、下記のようにDockerfileを利用しているらしきコンテナイメージのビルドログが確認できます。

※ 読みづらいので日時は省略

[Build] Sending build context to Docker daemon 24.06kB
[Build] Step 1/5 : FROM 335838599203.dkr.ecr.ap-northeast-1.amazonaws.com/awsfusionruntime-php81:8.1.10
[Build] 8.1.10: Pulling from awsfusionruntime-php81
(略)
[Build] Step 2/5 : COPY . /app
(略)
[Build] Step 3/5 : WORKDIR /app
(略)
[Build] Step 4/5 : RUN scripts/build.sh
(略)
[Build] Step 5/5 : EXPOSE 8080

ここでビルドされるコンテナイメージがマネージドランタイムの実体と言えます。
335838599203.dkr.ecr.ap-northeast-1.amazonaws.com/awsfusionruntime-php81 のイメージは Amazon Linux 2ベースのようです。

※ scripts/build.shに cat /etc/system-release を仕込むと確認できます。

[Build] [91m+ cat /etc/system-release
[Build] [0mAmazon Linux release 2 (Karoo)

このイメージをローカルにDLすることができればコンテナの中身の確認が非常に捗ったのですが、URLからしてどうやらAmazon ECRのプライベートレジストリです。ビルドスクリプトに調査用のコマンドを仕込むのを繰り返す必要がありました。

ただし、ApacheやPHP-FPMの設定ファイルの配置については Amazon Linux 2のイメージ を参考にすることができました。

このコンテナイメージビルドの終盤のステップで

[Build] Step 4/5 : RUN scripts/build.sh

というログが見られるため、先ほど作成したbuild用のスクリプトでコンテナイメージのビルドに介入できていることがわかります。

pre-build、post-buildのスクリプトは特に使い道がありませんでしたが、コンテナイメージのビルドの前後に出力が確認できます。

pre-build

[AppRunner] Starting to build your application source code.
[PreBuild] pre build
[Build] Sending build context to Docker daemon 24.06kB
[Build] Step 1/5 : FROM 335838599203.dkr.ecr.ap-northeast-1.amazonaws.com/awsfusionruntime-php81:8.1.10

post-build

[Build] Successfully built 2fbd723bf803
[Build] Successfully tagged application-image:latest
[PostBuild] post build

表示を確認

デプロイが完了したら、デフォルトドメインとして表示されているURLにアクセスします。

app-runner-managed-php-worked.png

ローカルと同様に Hello, App Runner PHP Managed Runtime! というテキストが表示されることを確認できました。

CloudWatch Logsのアプリケーションログを見ると、PHPアプリから出力したログが確認できます。

[2022-11-12T12:40:00.921680+00:00] MyApp.INFO: Logging from PHP application. (env=production) [] []

後片付け

App Runnerは現時点では最小のインスタンス数を0にすることができず、リクエストがない場合でもメモリと起動時間に応じた従量課金が発生します。 確認やテストが終わったら後片付けをお忘れなきよう。

サービス詳細画面のアクション -> 削除 でサービス自体を削除するか、一時停止でサービスを停止させておきましょう。

必要に応じてApp Runnerのサービス作成時に作られたロググループも削除してください。

PHPの実行方式の選定について

今回、Apache + PHP-FPMを選んだ理由は下記の三点です。

  • マネージドランタイムのコンテナイメージにはApacheとPHP-FPMがインストール済みだった
  • PHPがApache対応でビルドされていないようで、mod_phpをそのままでは有効化できなかった
  • マネージドランタイムに乗っかるためにミドルウェアの追加はできればなしで済ませたい


コンテナイメージのビルド時には yum install などのコマンドも使えるため、例えば皆さん大好きnginxもインストールできます。 ただしそこまでするならコンテナベースのデプロイを選択してコンテナイメージのビルドを自分の管理下に置いた方がはるかに楽です。 独自の変更を加えすぎてしまうと、マネージドランタイムのメリットが活かせず本末転倒と言えます。

また他の選択肢として、ビルトインウェブサーバーは論外としても、ReactPHPなどのライブラリを利用すれば特にミドルウェアを追加することなくPHPだけでHTTPサーバを立てることは可能なはずです。ただし一般的な実行方式とは言いづらいのでこちらも万人に薦められるものではありませんでした。

以上の理由を考慮し、消去法で元々インストールされているApacheとPHP-FPMの組み合わせを選択しました。

1コンテナ1プロセスの教えとHTTPサーバ

AWS App Runnerの中の人たちがPHPのビルトインウェブサーバを選んでしまった理由は割と明らかに思えます。 App RunnerやCloud RunのようなCaaSを利用する際は、単一のコンテナをHTTPサーバとして扱う構成が一般的だからです。 PHPでの実現方法として一番手軽なのはやはりビルトインウェブサーバであり、本番で利用すべきではないことに気づかずもしくは気づきつつも諸事情により仕方なくこうなったというのであれば理解はできます。他のプログラミング言語をメインで使っている方からすれば言語自体とそのライブラリだけでProduction ReadyなHTTPサーバを立てづらいPHPがむしろ異端であるとさえ感じるかもしれません。

もう一つの一般的な構成として、Apache(Prefork MPM) + mod_php を選択することで1プロセスでHTTPサーバとPHPを動かすことができるので、PHP界隈の事情にあまり詳しくなかったとしても一考の余地があったのではと思わなくもないですが。

また、一つのコンテナの中で複数のプロセスを動かすのは一般的に良くないとされていますが、PHPとCaaSないしPaaSを利用する場合は事情が異なります。

有名なPaaSであるHerokuも基本的にはApache + PHP-FPM ないし nginx + PHP-FPM の構成を採用していますし、1コンテナ内でWebサーバとPHP-FPMを動かす構成にも十分実績はあります。前述のApache + mod_php は API専任サーバとして動かす場合は良好なパフォーマンスを発揮しますが、HTTP/2に対応していなかったり、高負荷環境の静的アセットの配信では利用メモリの効率に問題があったりと、使い分けが必要な場面もあります。

現時点ではコンテナベースのサービスのほうが

ログで確認した通り、PHPマネージドランタイムに使われているコンテナイメージはプライベートであり、手元で中身を確認することは難しいです。調査のためのビルドスクリプトに調査用コマンドを仕込んだデプロイを繰り返すことになり、非常に難儀しました。やはり手元で再現可能な環境の方が取り回ししやすいです。

また、今回は簡単のためにエントリポイントのスクリプトでPHP-FPMとApacheを動かすだけの構成としましたが、本来プロセスマネージャーを使ったり、より複雑な監視用のスクリプトを書いたりする必要があります。そこまで行くと正直なところ、マネージドランタイムとは一体という気持ちが抑えきれません。

DockerfileでApache + mod_phpの構成のコンテナイメージを作成するのはこれに比べれば難しくありません。また、Webサーバ + PHP-FPM の構成のために監視用のプロセスマネージャーの設定やスクリプトを自前で書くとなると手間ですが、Cloud Native Buildpacksを用いればHerokuで動いているWebサーバ+PHP-FPM構成を拝借してコンテナイメージを容易に作成することができます。

CodeBuildなどを用いてコンテナのビルドパイプラインさえ構築してしまえばコンテナベースのApp Runnerへのデプロイを実現できるので、この手間とPHPマネージドランタイムをこねくり回す手間を天秤にかけることになります。

個人的には前者の方が圧倒的に楽だったため、コードベースのデプロイとPHPマネージドランタイムの組み合わせを選択する理由は現状では特にないという結論に至りました。

参考: AWS CodeBuild + Cloud Native BuildpacksでLaravelアプリのイメージをビルドし、App Runnerにデプロイする

まとめ

現時点では使う理由なしとバッサリ切ってしまいましたが、App RunnerにPHPのマネージドランタイムが追加されたことはPHP界隈のApp Runner利用およびコンテナ利用を促進しうる大きな一歩に違いありません。

今後のさらなるサービス改善に期待します。

  • PHPのマネージドランタイムでもApache + PHP-FPMの構成でアプリケーションを動かすことは可能
  • ただしビルドコマンドや起動時のコマンドが一筋縄ではいかないため、マネージドランタイムの恩恵をフルに享受できているとは言い難い
  • 現状ではコンテナベースのデプロイのほうがオススメ
  • AWS様、Apache + mod_php構成か nginx + PHP-FPM構成をPHPマネージドランタイムのデフォルトにしてログの設定も標準出力にしてください
  • もしくはCloud Native Buildpacksを使ったソースコードからのデプロイ方式に対応してください
TOPに戻る