Laravel Advent Calendar 2020の23日目の記事です。
はじめに
Google App Engine上でLaravelを動作させるには基本的に以下のチュートリアルを参考にすればできる。
Run Laravel on Google App Engine standard environment
しかしこのチュートリアルでは
- ビルド時にconfigなどのキャッシュを生成していない
- stderrの構造化ログに対応できない
- nginxやphp-fpmの設定ができない
など個人的に物足りない部分もあったため、より詳細な設定をメモとして残すことにした。
そのため、上記のことに不満がないならひとまずチュートリアルに従って構築することをおすすめする。
Laravelの設定
GAEでは/tmp以外には書き込むことができないため、実行時に生成されるキャッシュなどは/tmpに書き込むように設定する必要がある。
Session
デフォルトの設定のfile
だと、GAEのインスタンス間でセッションを共有できなくなるため、ここでは一番対応が楽なcookie
にした。
- 'driver' => env('SESSION_DRIVER', 'file'),
+ 'driver' => env('SESSION_DRIVER', 'cookie'),
View
viewもビルド時にキャッシュを生成するようにしたかったが、以下のような問題があり手間がかかるため断念した。
- ライブラリ内の相対パスで指定されたviewが
view:cache
コマンドで正しくキャッシュされず、実行時に再生成されるバグがある - viewは最終更新日時を比較してキャッシュを再生成するか判断するが、Buildpacksで配置されたファイルはタイムスタンプが1980年になるため、正しく比較できない
詳しくは以下を参照
[7.x] Allow disabling checks for expired views #31206
[7.x] FileViewFinder: Resolve hinted paths #31804
1つ目がキャッシュの再生成をさせないオプションを追加するプルリク
2つ目が相対パスのバグを修正するプルリク
どちらもマージされていないため、対応したいなら自分でプルリクのとおりに修正する必要がある。
結局、viewのキャッシュについてはビルド時に生成するのは諦め、生成先を/tmpに指定した。
- 'compiled' => env(
- 'VIEW_COMPILED_PATH',
- realpath(storage_path('framework/views'))
- ),
+ 'compiled' => env('VIEW_COMPILED_PATH', '/tmp'),
Cache
キャッシュファイルの指定先を/tmp/cacheに変更
'file' => [
'driver' => 'file',
- 'path' => storage_path('framework/cache/data'),
+ 'path' => '/tmp/cache',
],
Logging
ロギングはCloud Loggingクライアントライブラリを使用することもできるが、ここでは構造化ログを標準エラー出力に書き込む方式をとる。
ログはjson形式でstderrに出力し、リクエストログに関連付けるためにリクエストのトレースIDを付与する。
emergency
はロガーのfallbackとなっているため、これもstderrに向ける。
+use Monolog\Formatter\JsonFormatter;
+use App\Logging\Appliers\CloudTraceProcessorApplier;
+use App\Logging\Appliers\WebProcessorApplier;
...
'stack' => [
'driver' => 'stack',
- 'channels' => ['single'],
+ 'channels' => ['stderr'],
- 'ignore_exceptions' => false,
+ 'ignore_exceptions' => true,
+ 'tap' => [
+ CloudTraceProcessorApplier::class,
+ WebProcessorApplier::class,
+ ],
],
...
'stderr' => [
'driver' => 'monolog',
+ 'level' => env('LOG_STDERR_LEVEL'),
'handler' => StreamHandler::class,
- 'formatter' => env('LOG_STDERR_FORMATTER'),
+ 'formatter' => env('LOG_STDERR_FORMATTER', JsonFormatter::class),
'with' => [
'stream' => 'php://stderr',
],
],
...
'emergency' => [
- 'path' => storage_path('logs/laravel.log'),
+ 'path' => 'php://stderr',
],
WebProcessorを適用するためのクラス
<?php
namespace App\Logging\Appliers;
use Illuminate\Log\Logger;
use Monolog\Processor\WebProcessor;
class WebProcessorApplier
{
public function __invoke(Logger $logger)
{
$logger->pushProcessor(new WebProcessor());
}
}
リクエストのトレースIDを付与するためのクラス
<?php
namespace App\Logging\Appliers;
use Illuminate\Http\Request;
use Illuminate\Contracts\Config\Repository as Config;
use Illuminate\Log\Logger;
class CloudTraceProcessorApplier
{
protected $request;
protected $project;
public function __construct(Request $request, Config $config)
{
$this->request = $request;
$this->project = $config->get('cloud.project_id');
}
public function __invoke(Logger $logger)
{
$logger->pushProcessor(function (array $record) {
$trace = explode('/', $this->request->header('X-Cloud-Trace-Context'))[0];
if ($this->project !== null && $trace !== '') {
$record['logging.googleapis.com/trace'] = "projects/$this->project/traces/$trace";
}
return $record;
});
}
}
GCPのプロジェクトIDの設定
<?php
return [
'project_id' => env('GCP_PROJECT_ID'),
];
.envの管理
今回は環境ごとに異なる設定を以下のような.envで管理する。
- .env.local
- .env.production
また、.envに入れられない秘匿情報は環境変数によってビルド時に挿入する。
APP_KEY=base64:LY6qNPet6j3g1MtKVKPX7aAJ3RdDgWNdNxylOzqIsMQ=
APP_DEBUG=true
APP_URL=http://localhost
SESSION_SECURE_COOKIE=false
LOG_STDERR_LEVEL=debug
GCP_PROJECT_ID=null
APP_KEY=
APP_DEBUG=false
APP_URL=https://example.com
SESSION_SECURE_COOKIE=true
LOG_STDERR_LEVEL=info
GCP_PROJECT_ID=hoge
ここでは、APP_KEY
が.envにはいれられないため、ビルド時に環境変数として与える。
ビルド設定
GAE Standardでは、ビルド時にcomposer gcp-build
が呼ばれる。そのため、キャッシュ生成や秘匿情報の挿入はこの中で行えばいい。
ただし現在、composer.jsonのscriptsにgcp-build
を配列として持たせると実行されないバグ(仕様?)があるため注意。gcp-build
は文字列である必要がある。
GAE StandardのBuildpacksのソースはこちら
GoogleCloudPlatform/buildpacks
秘匿情報の管理はSecret Managerを使う。ビルド環境からシークレットを取得するためには、Cloud Buildのサービスアカウント(<数字>@cloudbuild.gserviceaccount.com)にSecret Managerのシークレットアクセサー権限を与える必要がある。
"scripts": {
...
"gcp-build": "bash bin/build.sh",
"cache": [
"rm -f bootstrap/cache/*.php",
"@php artisan package:discover",
"@php artisan config:cache",
"@php artisan route:cache",
"@php artisan event:cache"
],
}
bootstrap/cache/*.php
は開発環境のものがある状態でビルドすると、require-devのライブラリが原因でpackage:discover
がエラーになってしまうため予め削除している。
set -euo pipefail
if [ "${APP_ENV:-production}" = production ]; then
secret='gcloud secrets versions access latest --secret'
export APP_ENV=production
export APP_KEY=`$secret APP_KEY`
fi
composer cache
ビルドに不要なファイルのignore
.gcloudignoreを用意することで、.gitignoreのような書き方でビルド時に不要なファイルを無視できる。
.DS_Store
/vendor
/node_modules
/bootstrap/cache/*
!/bootstrap/cache/.gitignore
/storage/framework/cache/*
!/storage/framework/cache/.gitignore
/storage/framework/sessions/*
!/storage/framework/sessions/.gitignore
/storage/framework/testing/*
!/storage/framework/testing/.gitignore
/storage/framework/views/*
!/storage/framework/views/.gitignore
/storage/debugbar/*
!/storage/debugbar/.gitignore
/storage/logs/*
!/storage/logs/.gitignore
/storage/*.key
/public/storage
/_ide_helper.php
/_ide_helper_models.php
/.phpstorm.meta.php
/.phpunit.result.cache
続き
本当はsupervisorでphp-fpmとnginxを起動するところまであるのですが、すこしおかしなところを見つけたので解決し次第追記します