Fusic Advent Calendar 2024 の19日目の記事です。
7月に弊社主催イベント Fusic Tech Live Vol.20 にて「Laravel Octane を AWS Fargate で動かしてみる」という内容で発表しました。
Laravel Octane - FrankenPHP 正式サポートになったので、改めて Fargate にデプロイしてみました。
実運用を想定した際に、行っておくと良いと考えられる設定やポイントを書き留めておきます。
前提
- デプロイのための ECR, タスク定義, Fargate は構築済み
- 環境変数の設定はS3に設置し、タスク起動時にロードする
-
php artisan octane:install
で必要な設定ファイルなどはリポジトリに設定済み
Fargate に設定する .env に必要な項目を追記
...
LOG_STACK=single,stderr # stderrを追加
...
OCTANE_SERVER=frankenphp
OCTANE_HTTPS=true
- LOG_STACK : Laravelのログを標準エラー出力し、CloudWatchLogsに転送するための設定です
- OCTANE_SERVER : FrankenPHPを利用するために追記
- OCTANE_HTTPS : ALBの背後で動かすため、LaravelはHTTPしか利用しないが、Laravelが生成でリンクをHTTPSにするため true を指定
Dockerfile
DBは PostgreSQL を利用する想定です。
FROM node:20 AS build-node
ADD . /work
WORKDIR /work
RUN npm i && npm run build && rm -rf node_modules
FROM dunglas/frankenphp:php8.3
COPY --from=build-node /work /app
RUN apt-get update \
&& apt-get install -y zip unzip git \
&& install-php-extensions \
pcntl \
pdo \
pgsql \
pdo_pgsql \
bcmath \
json \
mbstring \
tokenizer \
xml \
zip \
opcache \
&& rm -rf /var/lib/apt/lists/*
COPY ./my.ini $PHP_INI_DIR/conf.d/
WORKDIR /app
ENV COMPOSER_ALLOW_SUPERUSER=1
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN composer install --no-dev
RUN php artisan octane:install --server frankenphp -n
ENTRYPOINT ["php", "artisan", "octane:start", "--host", "0.0.0.0", "--port", "80", "--admin-port", "2019", "--log-level", "info", "--caddyfile", "deploy/Caddyfile"]
- nodeイメージで必要なアセットをビルドした後、frankenphpが公式で提供するイメージにアセットとLaravelのコードを設置しています
-
my.ini
は各自の環境でPHPにしておきたい各設定を別ファイルで記述する想定です - ENTRYPOINT
- コンテナのプロセスを
octane:start
にし、アプリケーションサーバとして稼働するようにしています -
"--port", "80"
: 後述するタスク定義に記載するポートと合わせておく必要があります -
"--log-level", "info"
: デフォルトではFrankenPHPはCaddyのアクセスログを出力しません
ログレベルを明示的に設定するとJSONでアクセスログが出力されます
https://frankenphp.dev/docs/laravel/#laravel-octane -
"--caddyfile", "deploy/Caddyfile"
: ログやキャッシュの設定をしたいため、自前で用意したCaddyfile(後述)を指定しています
- コンテナのプロセスを
タスク定義
今回はGithubActionsからデプロイを行います。
Fargateで利用するタスク定義のテンプレートは以下のようにしました。
{
"containerDefinitions": [
{
"name": "php",
"image": "xxxxxxx",
"cpu": 1024,
"memory": 2048,
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"essential": true,
"environment": [],
"environmentFiles": [
{
"value": "xxxxxxx",
"type": "s3"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/fargate",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "php"
}
},
"systemControls": []
}
],
"cpu": "1024",
"memory": "2048",
"family": "xxxxxxx",
"taskRoleArn": "xxxxxxx",
"executionRoleArn": "xxxxxxx",
"networkMode": "awsvpc",
"volumes": [],
"placementConstraints": [],
"tags": []
}
- スペック : 1Core, 2GB. ここは状況によって変えていきます。1タスク 1コンテナなのでリソース全振り出来ていいですね
- portMappings : Dockerfile で Octane に指定しているポートをマッピングしておきます
- environmentFiles : デプロイ時にロードする
.env
を置いているS3のパスを指定しておきます - logConfiguration : awslogs を指定して、CloudWatchLogs に出力します
- .env で LOG_STACK に stderr を追記する点と関連があります
Caddyfile
アプリケーションサーバの設定をしたいので、自前でCaddyfileを用意しました。
octane:start
の時に Caddyfile を指定しなければ、vendor/laravel/octane/src/Commands/stubs/Caddyfile
のテンプレートを利用するようになっています。
テンプレートをベースに以下の対応を行いました。
- セキュリティヘッダーの追加
- 静的ファイルのキャッシュ設定
- ログに出力したくないパスを指定し除外
- 静的ファイルへのアクセス
- ヘルスチェックパスへのアクセス
{
{$CADDY_GLOBAL_OPTIONS}
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
frankenphp {
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
}
}
{$CADDY_SERVER_SERVER_NAME} {
log {
level {$CADDY_SERVER_LOG_LEVEL}
# Redact the authorization query parameter that can be set by Mercure...
format filter {
wrap {$CADDY_SERVER_LOGGER}
fields {
uri query {
replace authorization REDACTED
}
}
}
}
@skip path_regexp \.(js|css|png|jpe?g|gif|ico|woff|otf|ttf|eot|svg|txt|pdf|docx?|xlsx?)$
log_skip @skip
log_skip /up
# Security headers
header {
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"
# Prevent iframe embedding
X-Frame-Options "SAMEORIGIN"
# Send the full URL for same-origin requests, only the origin for HTTPS cross-origin requests, and nothing for HTTP cross-origin requests.
Referrer-Policy "no-referrer-when-downgrade"
}
@immutable_assets {
path /build/* /vendor/*
}
header @immutable_assets Cache-Control "public, max-age=31536000, immutable"
route {
root * "{$APP_PUBLIC_PATH}"
encode zstd br gzip
# Mercure configuration is injected here...
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
index frankenphp-worker.php
# Required for the public/storage/ directory...
resolve_root_symlink
}
}
}
- 参考にさせていただいたドキュメント・Issue
Github Actions でデプロイ
aws-actions が提供するアクションを利用してデプロイを行います。
AWSへの認証にはIAMロールを別途設定しています。
Use IAM roles to connect GitHub Actions to actions in AWS
前述のDockerfile, タスク定義のパスを設定し、適宜環境に合わせて書き換えます。
指定のブランチにマージして無事デプロイが終われば、Laravel Octane がFaragate上で動いているはずです!
name: Deploy Fargate
on:
push:
branches:
- deploy
jobs:
deploy:
name: Deploy
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: xxxxxxx
aws-region: ap-northeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ github.sha }}
restore-keys: |
${{ runner.os }}-docker-
- name: Build & TAG PHP
id: build-php
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker buildx create --use
docker buildx build \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache \
--file Dockerfile \
-t $ECR_REGISTRY/xxxxxxx/php \
--push .
echo "image=$ECR_REGISTRY/xxxxxxx/php" >> $GITHUB_OUTPUT
- name: Fill in the new PHP image ID in task definition
id: render-app-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ./task_definition.json
container-name: php
image: ${{ steps.build-php.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-app-container.outputs.task-definition }}
service: xxxxxxx
cluster: xxxxxxx
wait-for-service-stability: true
CloudWatchLogsでみると
FrankenPHP(Caddy)が出力したJSONのアクセスログが確認できます。
JSON形式なので Insights での検索とも相性が良く便利です。
その他の調整
アプリケーションログもJSON化したい
CaddyのログがJSONなので、アプリケーションログもJSONの方が後々使いやすいです
Laravel で、エラーログを JSON で出力しつつ Stack trace を含めたい
こちらの記事を参考にさせていただき stderr の出力をすべてJSONにしました
(Inertiaの場合)ブラウザタブのアプリ名が Laravel
のまま
特殊なことをしていなければ、Inertiaのブラウザタブは resources/js/app.tsx
の以下の場所で設定されます。
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
今回の僕の作りの問題ですが、Dockerfileでビルドするタイミングでは .env がない状態のため、VITE_APP_NAMEが存在せず Laravel が出力されてしまいます。
本番稼働のアプリであれば、ここのデフォルト値にアプリ名を入れておけばいいかなと思います。
終わり
今回の環境でシステムテスト、負荷検証などを行い本運用に移行予定です。
追加で必要になった調整や知見があれば、追記/別記事などで共有していけれたらと思います!