9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LaravelAdvent Calendar 2020

Day 23

Laravel on GAE Standard 環境構築の手引き

Last updated at Posted at 2020-12-22

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にした。

config/session.php
-    '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に指定した。

config/view.php
-    'compiled' => env(
-        'VIEW_COMPILED_PATH',
-        realpath(storage_path('framework/views'))
-    ),
+    'compiled' => env('VIEW_COMPILED_PATH', '/tmp'),

Cache

キャッシュファイルの指定先を/tmp/cacheに変更

config/cache.php
         'file' => [
             'driver' => 'file',
-            'path' => storage_path('framework/cache/data'),
+            'path' => '/tmp/cache',
         ],

Logging

ロギングはCloud Loggingクライアントライブラリを使用することもできるが、ここでは構造化ログを標準エラー出力に書き込む方式をとる。
ログはjson形式でstderrに出力し、リクエストログに関連付けるためにリクエストのトレースIDを付与する。
emergencyはロガーのfallbackとなっているため、これもstderrに向ける。

config/logging.php
+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を適用するためのクラス

app/Logging/Appliers/WebProcessorApplier.php
<?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を付与するためのクラス

app/Logging/Appliers/CloudTraceProcessorApplier.php
<?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の設定

config/cloud.php
<?php

return [

    'project_id' => env('GCP_PROJECT_ID'),

];

.envの管理

今回は環境ごとに異なる設定を以下のような.envで管理する。

  • .env.local
  • .env.production

また、.envに入れられない秘匿情報は環境変数によってビルド時に挿入する。

.env.local
APP_KEY=base64:LY6qNPet6j3g1MtKVKPX7aAJ3RdDgWNdNxylOzqIsMQ=
APP_DEBUG=true
APP_URL=http://localhost

SESSION_SECURE_COOKIE=false

LOG_STDERR_LEVEL=debug

GCP_PROJECT_ID=null
.env.production
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のシークレットアクセサー権限を与える必要がある。

composer.json
    "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がエラーになってしまうため予め削除している。

bin/build.sh
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のような書き方でビルド時に不要なファイルを無視できる。

.gcloudignore
.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を起動するところまであるのですが、すこしおかしなところを見つけたので解決し次第追記します

9
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?