AWS Lambda へのデプロイ
AWS Lambda は、サーバーを管理せずに必要なときだけコードを 実行するサービスです。Bref を使うと、CakePHP 2 アプリケーションは従来の サーバーと同じように PHP-FPM で Lambda 上で動きます。すべての HTTP リクエストは webroot/index.php に渡され、bin/cake のコマンドは別の関数で実行します。
このページでは、pieceofcake2/app スケルトンから作成した アプリケーションを Bref 3 と Docker コンテナイメージでデプロイする方法、ほかの AWS サービス (S3、DynamoDB、SES、RDS、SSM パラメータストア) との連携、GitHub Actions からデプロイする方法を 説明します。AWS の基本と Bref のドキュメント の内容を知っていることを 前提とします。デプロイ のページの一般的な注意も当てはまります。
Lambda が適している場合
Lambda は、アクセスが不規則なアプリケーションや急に増えるアプリケーション、社内ツール、API に 向いています。料金はリクエストごとで、スケーリングは自動で行われ、パッチを当てるサーバーも ありません。次の制約に注意してください。
- ファイルシステムは
/tmpを除いて読み取り専用です。/tmpは Lambda のインスタンスごとに 別々で、インスタンスの起動時には空です。セッション、アップロードされたファイル、共有する キャッシュはほかのサービスに置く必要があります。 - HTTP のリクエストとレスポンスは 6 MB までで、API Gateway は 30 秒で応答を待つのをやめます。
- 新しいインスタンスの起動には少し時間がかかります (コールドスタート)。Bref のドキュメントに よると、Bref のランタイムで約 250 ms、それにアプリケーションの読み込み時間が加わります。
- Lambda はインスタンスを増やしてスケールし、インスタンスはそれぞれデータベースに接続します。
ローカルのファイルシステムに書き込むアプリケーション、時間のかかるリクエストや長時間の接続 (WebSocket、ストリーミング) を使うアプリケーションは、変更が必要です。ECS のコンテナや従来の サーバーのほうが適している場合もあります。
必要なもの
pieceofcake2/appスケルトン (モダンな構成) をもとにしたアプリケーション、または同じ設定を した既存のアプリケーション。従来のapp/構成でパスが異なるところは、その都度説明します。- PHP 8.2 以降。Bref 3 はそれより古いバージョンに対応していません。例では PHP 8.4 を使います。
- イメージをビルドするための Docker。
- Node.js と Serverless CLI。Bref はデプロイツールとして、Bref のメンテナーがメンテナンスしている Serverless Framework v3 のオープンソースのフォーク OSS Serverless (
osls) を使います。有料サービスの Bref Cloud でも同じserverless.ymlをデプロイできます。 - AWS アカウントと、CloudFormation のスタックをデプロイできる認証情報。
例ではサービス名に my-cake-app、リージョンに ap-northeast-1 を使います。自分の値に 置き換えてください。
仕組み
デプロイされるものは次のとおりです。
- Bref の
bref/php-84:3イメージをもとにした Docker イメージ。PHP と Bref のランタイムを含み、 アプリケーションは/var/taskにコピーします。 - イメージを
fpmランタイムで実行するweb関数。API Gateway (HTTP API) がすべての HTTP リクエストを関数に送り、Bref がそれを PHP-FPM に渡してwebroot/index.phpを実行します。 - 同じイメージを
consoleランタイムで実行するconsole関数。呼び出しのたびに、スケジュールやserverless bref:cliコマンドから渡された引数でphp bin/cakeを実行します。 - セッション用の DynamoDB テーブルやアップロードされたファイル用の S3 バケットなど、同じ
serverless.ymlで定義するほかの AWS リソース。
Serverless CLI がイメージをビルドして Amazon ECR にプッシュし、すべてを CloudFormation の スタックとしてデプロイします。webroot/ の静的なファイルは PHP-FPM では配信されません。 静的なアセット を参照してください。
アプリケーションの準備
Bref のインストール
bref/bref パッケージをインストールします。イメージで実行するランタイムのコードと、 Serverless CLI のプラグインが含まれています。
composer require bref/bref:^3pieceofcake2/cakephp が依存している php81_bc/strftime は intl 拡張を必要とします。 Bref は intl を含んでいますが有効にしていないので、php/conf.d/php.ini で有効にします。 Bref の PHP は /var/task/php/conf.d/、つまりプロジェクトの php/conf.d/ ディレクトリにある すべての .ini ファイルを読み込みます。
; php/conf.d/php.ini
extension=intl
; Enable the extensions your application uses, for example for Redis:
;extension=redis
; The code does not change on Lambda; do not check files for changes.
opcache.validate_timestamps=0intl がないと、Composer のプラットフォームチェックが "Composer detected issues in your platform" というエラーでアプリケーションを止めます。Bref はデフォルトで OPcache と pdo_mysql、pdo_pgsql、pdo_sqlite 拡張を有効にしています。Bref に含まれている拡張や、 gd、imagick などの追加の拡張については PHP extensions を参照してください。
書き込めるディレクトリとリクエストの変数
Lambda ではアプリケーションのディレクトリは読み取り専用です。CakePHP はキャッシュを TMP ディレクトリに、ファイルのログを LOGS ディレクトリに書き込みます。コアはどちらの定数も、 まだ定義されていないときだけ定義します。webroot/index.php と bin/cake が CakePHP より先に 読み込む config/define.php でこれらを定義します。Lambda は環境変数 LAMBDA_TASK_ROOT を セットするので、次のコードは Lambda 上でだけ実行されます。
// config/define.php
// AWS Lambda: only /tmp is writable.
if (getenv('LAMBDA_TASK_ROOT') !== false && !defined('TMP')) {
define('TMP', '/tmp/cake' . DS);
define('LOGS', TMP . 'logs' . DS);
foreach (['cache/models', 'cache/persistent', 'cache/views', 'logs', 'sessions'] as $dir) {
if (!is_dir(TMP . $dir)) {
mkdir(TMP . $dir, 0777, true);
}
}
if (PHP_SAPI !== 'cli') {
// API Gateway and Lambda function URLs only accept HTTPS.
$_SERVER['HTTPS'] = 'on';
// Bref sends every request to webroot/index.php without SCRIPT_NAME.
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['PHP_SELF'] = '/index.php';
}
}ディレクトリを作成しているのは、FileEngine と FileLog がデバッグモードのときしか存在しない ディレクトリを作成しないためです。インスタンスの起動時には /tmp は空なので、インスタンスは それぞれコアとモデルのキャッシュを作り直し、新しいデプロイは必ず空のキャッシュから始まります。
最後のブロックは、CakePHP から見たリクエストを、URL リライトを使う Apache の背後にあるときと 同じにします。
SCRIPT_NAMEがないと、CakePHP はリクエストのパスからアプリケーションのベースパスを計算し、/posts/view/1のような URL で誤ったベースパスになります。両方の変数を/index.phpにすると、 ベースパスは''になります。HTTPSにより、CakeRequest::is('ssl')が true を返し、セッションの Cookie にsecureフラグが付き、完全な URL がhttps://で始まります。
従来の app/ 構成では、同じコードを app/webroot/index.php と app/Console/cake.php の先頭、 DS を定義した後に書きます。
完全なベース URL
CakePHP は、リダイレクト先を含む完全な URL を Host ヘッダーから組み立てます。CloudFront や カスタムドメインの背後では、このヘッダーに公開しているドメインではなく API Gateway のドメインが 入ることがあります。config/core.php で環境変数から App.fullBaseUrl をセットします。
// config/core.php
if (env('APP_FULL_BASE_URL')) {
Configure::write('App.fullBaseUrl', env('APP_FULL_BASE_URL'));
}CloudWatch へのログ出力
Lambda は、PHP が stderr に書き込んだものをすべて CloudWatch Logs に送ります。/tmp の ログファイルはインスタンスが停止すると失われるので、config/bootstrap.php で Lambda 上では ログを stderr に書き込むように設定します。コアの ConsoleLog エンジンなら、追加のパッケージ なしでできます。
// config/bootstrap.php
if (getenv('LAMBDA_TASK_ROOT') !== false) {
CakeLog::config('default', [
'engine' => 'Console',
'stream' => 'php://stderr',
]);
} else {
CakeLog::config('debug', [
'engine' => 'File',
'types' => ['notice', 'info', 'debug'],
'file' => 'debug',
]);
CakeLog::config('error', [
'engine' => 'File',
'types' => ['warning', 'error', 'critical', 'alert', 'emergency'],
'file' => 'error',
]);
}ConsoleLog はメッセージを 1 行ずつ書き込むので、CloudWatch ではスタックトレースの各行が別々の ログイベントになります。メッセージとスタックトレースを 1 つのイベントにまとめ、CloudWatch Logs Insights でログレベルによる絞り込みができるようにするには、Console エンジンの代わりに Monolog プラグイン と Bref の CloudWatchFormatter を使います。
composer require pieceofcake2/monolog bref/monolog-bridge// config/bootstrap.php
if (getenv('LAMBDA_TASK_ROOT') !== false) {
CakePlugin::load('Monolog');
CakeLog::config('default', [
'engine' => 'Monolog.Monolog',
'channel' => 'app',
'handlers' => [
'Stream' => [
'php://stderr',
'formatters' => ['Bref\Monolog\CloudWatchFormatter'],
],
],
]);
}各行は Lambda のリクエスト ID で始まり、ログレベル、メッセージ、JSON 形式のメッセージが続きます。 PHP 自身が報告するエラーも CloudWatch に書き込まれます。エラーを Sentry にも送るには Sentry プラグイン を参照してください。
設定ファイル
スケルトンの .gitignore は config/database.php、config/email.php、composer.lock を 除外しています。CI でビルドするイメージにはリポジトリにあるファイルしか入らないので、次のように します。
config/database.php.defaultから作成したconfig/database.phpをコミットします。接続の設定は 環境変数DB_*から読み込むので、秘密の情報は含まれません。- メール で説明する SES のトランスポートを設定した
config/email.phpをコミットします。 IAM ロールを使うので、こちらにも認証情報は含まれません。 - テストした依存パッケージがそのままデプロイされるように、
composer.lockをコミットします。
.env ファイルはコミットせず、イメージにもコピーしないでください。Lambda では設定を環境変数から 読み込みます。設定と秘密の情報 を参照してください。
イメージのビルド
Bref 3 は、bref/php-84:3 のように PHP のバージョンごとに 1 つのイメージを公開しており、 すべてのランタイムで同じイメージを使います。ランタイムは環境変数 BREF_RUNTIME で選ぶので、 web 関数と console 関数で同じイメージを使えます。イメージをビルドする前に Composer の依存 パッケージをインストールし (後述のデプロイのワークフローで行います)、アプリケーションを /var/task にコピーします。
# Dockerfile
FROM bref/php-84:3
# Extra extensions come from Bref's extra extension images, for example GD:
# COPY --from=bref/extra-gd-php-84:3 /opt /opt
COPY . /var/task開発用のファイル、ローカルの設定、ログは .dockerignore ファイルでイメージから除外します。
# .dockerignore
.git
.github
.serverless
.env
config/.env
logs
node_modules
tests
tmpビルドする前に、本番用に依存パッケージをインストールします。
composer install --no-dev --prefer-dist --optimize-autoloaderDockerfile では /tmp に書き込まないでください。関数の起動時に Lambda が空の /tmp をマウントします。新しいメジャーバージョンでイメージが意図せず変わらないように、Bref の イメージは常にメジャーバージョンのタグ (:3) で指定してください。
AWS プラグインが使う aws/aws-sdk-php パッケージにはすべての AWS サービスのクライアントが 含まれており、vendor/ が 100 MB 以上大きくなります。コンテナイメージは 10 GB まで使えるので 上限の問題にはなりませんが、イメージが小さいほど起動は速くなります。SDK は使わないクライアントを 削除できます。Bref のドキュメントの Reducing package size を参照してください。
serverless.yml
プロジェクトに Serverless CLI をインストールします。Bref がウェブサイト用に使っている Serverless のプラグイン Lift が、静的なファイルのための CloudFront と S3 をセットアップします。
npm install --save-dev osls serverless-lift次の serverless.yml は、web 関数と console 関数、セッション用の DynamoDB テーブル、 アップロードされたファイル用の S3 バケットをデプロイします。各部分は以降の節で説明します。
# serverless.yml
service: my-cake-app
provider:
name: aws
region: ap-northeast-1
stage: ${opt:stage, 'dev'}
ecr:
images:
app:
path: ./
file: Dockerfile
platform: linux/amd64
provenance: false
environment:
DEBUG: '0'
APP_FULL_BASE_URL: https://www.example.com
SECURITY_SALT: bref-ssm:/my-cake-app/${sls:stage}/security-salt
SECURITY_CIPHER_SEED: bref-ssm:/my-cake-app/${sls:stage}/security-cipher-seed
DB_HOST: ${ssm:/my-cake-app/${sls:stage}/db-host}
DB_USERNAME: my_cake_app
DB_PASSWORD: bref-ssm:/my-cake-app/${sls:stage}/db-password
DB_DATABASE: my_cake_app
SESSION_TABLE: !Ref SessionsTable
UPLOADS_BUCKET: !Ref UploadsBucket
MAIL_FROM: noreply@example.com
iam:
role:
statements:
- Effect: Allow
Action: ssm:GetParameters
Resource: arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/my-cake-app/${sls:stage}/*
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: !GetAtt SessionsTable.Arn
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Join ['', [!GetAtt UploadsBucket.Arn, '/*']]
- Effect: Allow
Action: ses:SendEmail
Resource: '*'
plugins:
- ./vendor/bref/bref
- serverless-lift
functions:
web:
image:
name: app
command:
- webroot/index.php
environment:
BREF_RUNTIME: fpm
memorySize: 1024
timeout: 28
events:
- httpApi: '*'
console:
image:
name: app
command:
- bin/cake
environment:
BREF_RUNTIME: console
timeout: 900
constructs:
website:
type: server-side-website
assets:
'/css/*': webroot/css
'/js/*': webroot/js
'/img/*': webroot/img
'/favicon.ico': webroot/favicon.ico
resources:
Resources:
SessionsTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
TimeToLiveSpecification:
AttributeName: expires
Enabled: true
UploadsBucket:
Type: AWS::S3::Bucket注意点:
provider.ecr.imagesにより、serverless deployがイメージをビルドし、ECR のリポジトリを 作成して、イメージをプッシュします。platform: linux/amd64は、Apple シリコンを含むどの マシンでも x86 のイメージをビルドします。Bref の追加の拡張は ARM では使えません。provenance: falseは Docker BuildKit の provenance attestation を無効にします。これがあると マルチプラットフォームのマニフェストが作られ、Lambda が受け付けないことがあります。- 各関数の
commandはハンドラーです。fpmランタイムでは PHP-FPM が実行する PHP ファイル、consoleランタイムではphpで実行するスクリプトです。従来の構成ではapp/webroot/index.phpとapp/Console/cake.php(またはcakeスクリプトのパス) を使います。 pluginsの./vendor/bref/brefは Bref のプラグインで、serverless bref:cliコマンドを 提供します。vendor/から読み込むので、Serverless CLI の前にcomposer installを実行して ください。timeout: 28は、関数のタイムアウトを API Gateway の 30 秒の上限より短くします。遅い リクエストは関数の中で失敗し、ログに残ります。httpApi: '*'は、すべてのパスを関数に送る API Gateway の HTTP API を作成します。 Lambda 関数 URL (関数にurl: true) を使うと、API Gateway なしで、30 秒の上限もなく HTTP リクエストを 受け付けられます。
設定と秘密の情報
スケルトンは設定を環境変数 DEBUG、SECURITY_SALT、SECURITY_CIPHER_SEED、DB_* から 読み込みます (環境変数 を参照してください)。 Lambda では、これらを serverless.yml の environment で、すべての関数 (provider.environment) または関数ごとに定義します。スケルトンは、セットされていない変数だけを .env ファイルから読み込み、実際の環境変数が常に優先されます。.env ファイルはデプロイしないで ください。
ソルトやデータベースのパスワードなどの秘密の情報は、 AWS Systems Manager パラメータストア に、たとえば SecureString のパラメータとして保存します。
aws ssm put-parameter --region ap-northeast-1 \
--name /my-cake-app/prod/db-password --type SecureString --value 'secret'serverless.yml からパラメータを読み込む方法は 2 つあります。
${ssm:/my-cake-app/prod/db-host}は、デプロイ時に Serverless CLI が解決します。値は関数の 設定に平文で保存され、デプロイする人がパラメータを読める必要があります。秘密でない値に 使ってください。bref-ssm:/my-cake-app/prod/db-passwordは、Lambda のインスタンスの起動時に Bref が、すべての 変数をまとめて 1 回の API 呼び出しで解決します。値は関数の設定には保存されません。bref/secrets-loaderパッケージと、上のiamの節にあるssm:GetParametersの権限が必要です。bashcomposer require bref/secrets-loaderパラメータをデフォルトの
aws/ssmキーではなくカスタマー管理の KMS キーで暗号化している場合は、 ロールにそのキーに対するkms:Decryptの権限も必要です。
Bref はアプリケーションの開始前に bref-ssm: の値を解決するので、env('DB_PASSWORD') や getenv() は復号された値を返します。Lambda は AWS_REGION などの ランタイムの環境変数 をすべての関数にセットします。
AWS のサービス
AWS プラグイン (pieceofcake2/aws) は、DynamoDB の セッションハンドラー、SES のメールトランスポート、AWS SDK のクライアントを作成するコンポーネントを 提供します。インストールして読み込みます。
composer require pieceofcake2/aws// config/bootstrap.php
CakePlugin::load('Aws');プラグインは Aws の設定からクライアントを作成します。認証情報は設定しないでください。Lambda では AWS SDK が関数の IAM ロールの認証情報を、手元のマシンでは AWS プロファイルの認証情報を 使います。
// config/core.php
Configure::write('Aws', [
'region' => env('AWS_REGION') ?: 'ap-northeast-1',
'version' => 'latest',
]);関数の IAM ロールには、serverless.yml の例の iam の節のように、アプリケーションが使う サービスごとに権限が必要です。
セッション
デフォルトの php セッションハンドラーはセッションを /tmp のファイルに保存しますが、これは Lambda のインスタンス間で共有されません。次のリクエストが別のインスタンスに送られると、ユーザーの セッションが失われます。プラグインの DynamoDbSession ハンドラーで、セッションを DynamoDB に 保存してください。テーブルには id という名前の文字列のパーティションキーが必要です。 プラグインは各セッションの有効期限を expires 属性に書き込むので、この属性で Time to Live を 有効にします (serverless.yml の例は両方を設定しています)。
// config/core.php
if (env('SESSION_TABLE')) {
Configure::write('Session', [
'defaults' => 'php',
'cookie' => 'MYAPP',
'timeout' => 120,
'handler' => [
'engine' => 'Aws.DynamoDbSession',
'table_name' => env('SESSION_TABLE'),
],
'ini' => [
// Written to the "expires" attribute of each item
'session.gc_maxlifetime' => 7200,
],
]);
} else {
Configure::write('Session', ['defaults' => 'php']);
}プラグインは config/core.php の後の config/bootstrap.php で読み込みますが、セッションは その後、セッションに最初にアクセスしたときに開始するので問題ありません。DynamoDB のテーブルは VPC なしで使え、料金はリクエストごとです。セッションのロックなど、ほかのオプションについては DynamoDB Session Handler を参照してください。
キャッシュ
上の TMP ディレクトリを使えば FileEngine は Lambda でも動きますが、キャッシュはインスタンス ごとに別々で、インスタンスが停止すると失われます。コアのキャッシュ (_cake_core_ と _cake_model_) は作り直しが速く、どのみちデプロイのたびに作り直す必要があるので、これで 問題ありません。
レート制限やクエリ結果のキャッシュなど、リクエストやインスタンスの間で共有するデータには、共有の キャッシュサービスを使います。CakePHP の RedisEngine は Amazon ElastiCache (Redis OSS または Valkey) で使え、Bref に含まれている redis 拡張が必要です (php/conf.d/php.ini に extension=redis を追加します)。
// config/bootstrap.php
if (env('REDIS_HOST')) {
Cache::config('default', [
'engine' => 'Redis',
'server' => env('REDIS_HOST'),
'port' => (int)(env('REDIS_PORT') ?: 6379),
'prefix' => 'myapp_',
'duration' => '+1 hours',
]);
} else {
Cache::config('default', ['engine' => 'File']);
}ElastiCache は VPC の中で動くので、関数も同じ VPC で実行する必要があり (データベース を参照してください)、クラスターには動いている間ずっと料金が かかります。RedisEngine には TLS のオプションがないので、プライベートな VPC の中で転送中の 暗号化を使わないクラスターを使うか、PHP の Redis クライアントが server の tls:// プレフィックスを受け付けることを確認してから使ってください。VPC なしでリクエストごとの料金に したい場合は DynamoDB にキャッシュするデータを保存できますが、CakePHP 2 には DynamoDB の キャッシュエンジンはありません。キャッシュエンジンについては キャッシュ を参照してください。
メール
メールは、プラグインの AmazonSesApiTransport で Amazon SES から送信します。このトランスポートは SMTP ではなく SES の API を使います。
// config/email.php
class EmailConfig
{
public $default = [
'transport' => 'Aws.AmazonSesApi',
'from' => 'noreply@example.com',
'charset' => 'utf-8',
'headerCharset' => 'utf-8',
];
}送信元のアドレスまたはそのドメインは SES で検証済みの ID である必要があります。また、新しい SES アカウントは、本番環境へのアクセスを申請するまで検証済みのアドレスにしか送信できません。関数には ses:SendEmail の権限が必要です。 Amazon SES Email Transport と メール を参照してください。
ファイルのアップロードと S3
/tmp は共有されず、インスタンスが停止すると空になるので、アップロードされたファイルは S3 に 保存する必要があります。アップロードには Lambda の 2 つの制限も当てはまります。リクエストは 6 MB までで、Bref は upload_max_filesize と post_max_size を 6 MB にしています。
小さいファイルは、コントローラーでいつものように受け取り、一時ファイルを S3 クライアントで S3 に コピーできます。$this->Aws->S3 は Aws の設定から作成した S3Client を返します。
// src/Controller/DocumentsController.php
class DocumentsController extends AppController
{
public $components = ['Aws.Aws'];
public function upload()
{
$file = $this->request->data['Document']['file'];
$key = 'documents/' . CakeText::uuid() . '.pdf';
$this->Aws->S3->putObject([
'Bucket' => env('UPLOADS_BUCKET'),
'Key' => $key,
'SourceFile' => $file['tmp_name'],
'ContentType' => 'application/pdf',
]);
// Save $key in the database ...
}
}大きいファイルは、署名付き URL を使ってブラウザーから直接 S3 にアップロードします。 アプリケーションが短時間だけ有効な URL を作成し、ブラウザーがその URL にファイルを PUT リクエストで送り、アプリケーションがキーを保存します。バケットには、自分のドメインからの PUT を許可する CORS のルールが必要です。
// src/Controller/DocumentsController.php
public function uploadUrl()
{
$s3 = $this->Aws->S3;
$key = 'documents/' . CakeText::uuid() . '.pdf';
$command = $s3->getCommand('PutObject', [
'Bucket' => env('UPLOADS_BUCKET'),
'Key' => $key,
'ContentType' => 'application/pdf',
]);
$request = $s3->createPresignedRequest($command, '+10 minutes');
return $this->response->body(json_encode([
'key' => $key,
'url' => (string)$request->getUri(),
]));
}バケットは非公開のままにし、ファイルは同じように署名付きの GetObject の URL で配信します。 これでダウンロードも 6 MB の制限を受けません。
// src/Controller/DocumentsController.php
public function download($id)
{
$document = $this->Document->findById($id);
if (!$document) {
throw new NotFoundException();
}
$s3 = $this->Aws->S3;
$command = $s3->getCommand('GetObject', [
'Bucket' => env('UPLOADS_BUCKET'),
'Key' => $document['Document']['key'],
]);
$request = $s3->createPresignedRequest($command, '+10 minutes');
return $this->redirect((string)$request->getUri());
}Upload プラグイン はローカルのファイルシステム上で ファイルを移動します。Lambda で使うには、handleUploadedFileCallback オプションでファイルを 別の場所に保存するモデルのメソッドを指定できます。プラグインが作成するパスが /tmp の下に なることを確認してください。AWS プラグインは S3 ストリームラッパー を登録することも でき、PHP のファイル関数で s3://bucket/key のパスを使えるようになります。
静的なアセット
Bref が PHP-FPM で実行するのはハンドラーのファイル webroot/index.php だけです。 /css/cake.generic.css へのリクエストは CakePHP に届き、webroot/ のファイルを配信するものが ないので 404 エラーになります。 Bref のウェブサイトについてのドキュメント が推奨しているように、これらのファイルは CloudFront を通して S3 から配信します。
serverless.yml の例の Lift の server-side-website は、S3 バケットと CloudFront ディストリビューションを作成します。 CloudFront は assets に書いたパスをバケットに、それ以外のリクエストを API Gateway に送ります。 Lift はデプロイのたびに、書いたファイルをアップロードします。アプリケーションが参照する webroot/ のファイルやディレクトリはすべて追加してください。
プラグインやテーマのアセットは、CakePHP の AssetDispatcher フィルターが Lambda 関数から 配信します。これでも動きますが、ファイルごとに関数の呼び出しが発生します。S3 から配信するには、 デプロイの前にアセットを webroot/ にコピーし (たとえば DebugKit というプラグインなら webroot/debug_kit/)、assets に追加します。
カスタムドメインは API Gateway ではなく CloudFront に (us-east-1 リージョンの ACM の証明書で) 設定し、APP_FULL_BASE_URL をそのドメインにします。Bref のドキュメントの Custom domain name を参照してください。
データベース
Amazon RDS と Aurora (MySQL または PostgreSQL) は、config/database.php の DB_* の設定で 使えます。Bref は pdo_mysql と pdo_pgsql を有効にしています。次の点を考えておいてください。
- ネットワーク。 プライベートな VPC にあるデータベースを使うには、関数をその VPC で実行する 必要があります (
serverless.ymlのprovider.vpcにsecurityGroupIdsとsubnetIds)。 VPC の中の関数はインターネットにアクセスできないので、VPC に NAT ゲートウェイかこれらの サービスの VPC エンドポイントがないと、SSM、DynamoDB、SES、S3 に接続できません。bref-ssm:の変数はインスタンスの起動時に解決されるので、経路がないとすべてのコールドスタートが失敗します。 データベースをパブリックにアクセスできるようにすれば VPC は不要ですが、データベースが インターネットに公開されます。Bref のドキュメントの Using a database を参照してください。 - 接続数。 Lambda の各インスタンスは一度に 1 つのリクエストを処理し、それぞれが接続を開くので、 接続数は関数の同時実行数とともに増えます。 Amazon RDS Proxy で接続をプールし、関数に
reservedConcurrencyを設定してインスタンス数の上限を決めてください。persistent接続は有効にしないでください。 - スキーマの変更。 デプロイでは新しいコードに切り替わる間、データベースは少しの間古い スキーマのままです。マイグレーションはデプロイの前か後にコンソールのコマンドで実行し (コンソールのコマンド を参照してください)、スキーマの変更は新旧どちらの コードとも互換性があるようにしてください。
コンソールのコマンド
console 関数は、イベントで渡された引数で php bin/cake を実行します。手元のマシンや CI から コマンドを実行するには、Serverless CLI の Bref プラグインを使います。
npx serverless bref:cli --stage=prod --args="command_list"
# For example, to update the database from config/Schema/schema.php:
npx serverless bref:cli --stage=prod --args="schema update --yes"コマンドの出力と終了コードが返され、出力は CloudWatch Logs にも書き込まれます。0 以外の終了コードで 終わったコマンドは、呼び出しの失敗になります。関数は最大 15 分 (timeout: 900) 実行できます。 関数には端末がないので、確認を求めるコマンドには --yes などのオプションを渡してください。
スケジュールでコマンドを実行するには、console 関数に schedule イベントを追加します。 input は引数の文字列を JSON の文字列にしたものなので、二重に引用符で囲みます。
# serverless.yml
functions:
console:
# ...
events:
- schedule:
rate: cron(0 18 * * ? *)
input: '"report send --verbose"'
- schedule:
rate: rate(15 minutes)
input: '"queue process"'スケジュールは EventBridge のルールで、時刻は UTC です。コマンドは実行のたびに別々の呼び出しで 動きます。実行が間隔より長くかかる可能性がある場合は、同じコマンドが重なって実行されても問題ない ようにしてください。cron ジョブ と コンソールとシェル を参照してください。
デプロイ
手元のマシンに AWS の認証情報を設定したら、本番用に依存パッケージをインストールしてデプロイします。
composer install --no-dev --prefer-dist --optimize-autoloader
npx serverless deploy --stage=prod最初のデプロイでは CloudFormation のスタック、ECR のリポジトリ、Lift を使う場合は CloudFront ディストリビューションが作成されるので、数分かかります。最後に CLI が API とウェブサイトの URL を 表示します。CloudFormation は、新しいバージョンのデプロイが終わってからトラフィックを切り替え、 デプロイが失敗した場合はロールバックします。
関数のログを見るには次のようにします。
npx serverless logs --stage=prod --function=web --tailイメージをローカルでテストする
Bref のイメージには Lambda Runtime Interface Emulator が含まれているので、読み取り専用の ファイルシステムで、Lambda と同じようにイメージを実行できます。
docker build --platform linux/amd64 -t my-cake-app .
docker run --rm --platform linux/amd64 --read-only --tmpfs /tmp:exec,mode=1777 \
-p 9000:8080 -e BREF_RUNTIME=fpm -e DEBUG=0 my-cake-app webroot/index.php別の端末から、エミュレーターに API Gateway のイベントを送ります。
curl -X POST http://localhost:9000/2015-03-31/functions/function/invocations -d '{
"version": "2.0", "rawPath": "/posts", "rawQueryString": "",
"headers": {"host": "localhost"},
"requestContext": {"http": {"method": "GET", "path": "/posts",
"protocol": "HTTP/1.1", "sourceIp": "127.0.0.1"}},
"isBase64Encoded": false
}'関数が API Gateway に返す JSON がレスポンスとして返り、コンテナがログを出力します。ローカルの HTTP サーバーで開発する方法については、Bref のドキュメントの Local development を参照してください。
GitHub Actions からのデプロイ
GitHub Actions を使うと、AWS のアクセスキーを保存せずに、プッシュのたびにデプロイできます。 ワークフローは GitHub に OpenID Connect (OIDC) のトークンを要求し、 aws-actions/configure-aws-credentials がそれを IAM ロールの一時的な認証情報に交換します。
IAM で、オーディエンス
sts.amazonaws.comの ID プロバイダーtoken.actions.githubusercontent.comを追加します (AWS アカウントごとに 1 回)。デプロイ用のロールを作成します。信頼ポリシーで、自分のリポジトリと環境だけがロールを引き受け られるようにします。
json{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:my-org/my-cake-app:environment:production" } } } ] }ロールにデプロイに必要な権限を与えます。CloudFormation、Lambda、関数の IAM ロール、ECR、 デプロイ用の S3 バケット、API Gateway、CloudWatch Logs と、スタックのリソース (DynamoDB、S3、 CloudFront) です。出発点として、Bref が 最小限のポリシー を公開しています。
serverless.ymlで${ssm:...}を使う場合は、それらのパラメータに対するssm:GetParameterの権限も必要です。GitHub のリポジトリで
production環境を作成し、ロールの ARN を変数AWS_DEPLOY_ROLE_ARNに設定します。
ワークフローは本番用に依存パッケージをインストールし、serverless deploy を実行します。 serverless deploy がイメージをビルドし、ECR にプッシュし、スタックを更新します。
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
id-token: write
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: intl
tools: composer:v2
coverage: none
- name: Install Composer dependencies
run: composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install the Serverless CLI
run: npm ci
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Deploy
run: npx serverless deploy --stage=prod注意点:
- ランナーの PHP のバージョン (
8.4) をイメージ (bref/php-84:3) と合わせ、Composer が イメージで使うのと同じ依存パッケージを解決するようにします。 - テストは別のジョブまたはワークフローで実行し、
needsでデプロイのジョブがそれに依存するように してください。 concurrencyにより、デプロイは 1 つずつ順に実行されます。- データベースのマイグレーションなど、デプロイの後にコマンドを実行するには、
npx serverless bref:cli --stage=prod --args="..."を実行するステップを追加します。 developブランチからstagingをデプロイするなど、複数のステージをデプロイするには、 ステージごとに GitHub の環境と IAM ロールを使います。
トラブルシューティングとヒント
- コールドスタート。 イメージを小さく保ち (
tests/と開発用の依存パッケージを除外し、使わない AWS SDK のクライアントを削除します)、OPcache を有効のままにします。Bref はwarmerイベントを 認識するので、アクセスの少ないアプリケーションでインスタンスを起動したままにしておけます。 Cold starts を参照してください。 プロビジョニングされた同時実行を使うと、固定の料金でコールドスタートをなくせます。 - メモリと CPU。 Lambda はメモリに比例して CPU を割り当てます。web 関数は 1024 MB から 始めるのが妥当です。変更したら、CloudWatch Logs の
REPORT行で実行時間を比べてください。 - タイムアウト。 API Gateway は、関数のタイムアウトに関係なく 30 秒でエラーを返します。 時間のかかる処理は console 関数かキューに移してください (Bref は
functionランタイムで SQS に対応しています)。 - 6 MB の制限。 アップロードや生成したファイルを含め、リクエストとレスポンスは 6 MB までです。 大きなファイルには S3 の署名付き URL を使います。
- バイナリのレスポンス。 PHP で生成した PDF や画像などのレスポンスを返すには、環境変数
BREF_BINARY_RESPONSESを1にして、API Gateway が必要とする base64 に Bref がエンコード するようにします。base64 でサイズが約 3 分の 1 増えるので、こうしたレスポンスは約 4.5 MB までです。 Binary requests and responses を参照してください。 - ディスク容量。
/tmpはデフォルトで 512 MB です。アプリケーションが大きな一時ファイルを 書き込む場合は、関数のephemeralStorageSizeで増やします (10240 MB まで)。 - 起動時のエラー。 "Composer detected issues in your platform" は、拡張 (たいていは
intl) がphp/conf.d/php.iniで有効になっていないことを示します。"is not writable" の警告は、TMPやLOGSがまだ/var/taskの中を指していることを示します。config/define.phpを確認して ください。 - URL やリダイレクトの誤り。 API Gateway のドメインにリダイレクトされる場合は
APP_FULL_BASE_URLがセットされていません。パスが重複した URL になる場合は、 書き込めるディレクトリとリクエストの変数 の ようにSCRIPT_NAMEがセットされていません。 - PHP の拡張。 読み込まれている拡張を確認するには、それを出力するコンソールのコマンドを実行 するか、イメージで
php -mを実行します:docker run --rm --entrypoint php my-cake-app -m。
