Cloud Run上でLaravel×React(Mix使用)のサイトを爆速で動かす - Laravelをデプロイする

こんにちは。松戸市でエンジニアの幸福を目指す企業、Joolenです。
現在、弊社では新規Webサービスを絶賛構築中です！
いきなりですが、新規サービスでは主に下記のフレームワークやクラウドサービスを利用しています。

* Laravel(-mix)
* React
* Cloud Run
* Cloud Storage(CDNとして利用)

この組み合わせ、驚くほどに**良い**です。何が良いかというと
1. 安い → `Cloud Run`のコスパが半端じゃない。(動いた分だけしか課金されない。無料枠も大きい:tada:)
2. 早い → 動的サイトとは思えないほど早い。開発チームメンバー全員が感動するレベル。
3. うまい → LaravelとReactどちらも、拡張性が高い。ライブラリも豊富！特盛!熱盛り！(「うまい」だけこじ付け:sweat_smile:)

ただ、この良さを引き出すためには色々と工夫が必要で、情報探しはかなり苦労しました。
(ありがとう、Github。ありがとう、StackOverflow。その他、有益な情報を公開されている世界中の皆さんに感謝です。)
ということで、Qiita読者の皆さんにもこの良さを伝えるべく、我々が何をしたのか**惜しげなく**公開していきたいと思います！**(施されたら施し返す。恩返しです！)**

ただ、ここではCloud Runとは何か？ということまでは説明しません。それについては[公式サイト](https://cloud.google.com/run?hl=ja)にお任せして、我々はこのサービスを**どうやって**活用しているのかという点に絞って説明をしていきたいと思います。

# -はじめに- どのような設計なのか
システム構成としては下記のようなイメージです。(一部抜粋)
基本的に、クラウドサービスについてはGCP内で完結するようにしています。

![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/d63d29fc-4a5a-433e-c7a1-7de819a00b7d.png)


# この構成を実現するために
次にStep by Stepでこのアーキテクチャをどうやって実現するかこの記事では、最適化したLaravelのデプロイまでに絞って説明していきます。

では早速、具体的な実現方法について説明していきましょう。
# Cloud RunでLaravelを動かす。
最新のソースコードは[Github](https://github.com/joolen/cloudrun-laravel)で公開されています。こちらも、是非、参考にしてください。

## 動作に最低限必要なファイルを作成する

### 1.Laravelのプロジェクトを作成する
任意の場所にディレクトリ を作り、作成したディレクトリの配下で

```sh
composer create-project laravel/laravel app
```

を実行します。これにより、Laravelのプロジェクトが作成されます。

### 2.Apacheの設定ファイルを記述する
作成したディレクトリの配下に、`apache/000-default.conf`という名前でファイルを作りましょう。
ファイルの内容は以下を記載しておきます。

```conf
<VirtualHost *:80>

        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html/public

        <Directory /var/www/html/>
                AllowOverride All
                Require all granted
        </Directory>

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>
```

### 3.php.iniを用意する
同じ階層の配下に`php/php.ini`というファイルを用意します。本来であれば、要件によって細かく記述を変えるところではありますが、デモなので最低限の記述に止めます。

```ini
[php]
max_execution_time = 30
memory_limit = 512M
upload_max_filesize = 1M
post_max_size = 1M
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.revalidate_freq=60
opcache.max_accelerated_files=4000
opcache.validate_timestamps=1

;***** Added by go-pear
include_path=".:/lib/php"
;*****

```

### 4.Dockerfileを記述する
最後に`Dockerfile`を作って下記のようにします。

```dockerfile
FROM php:7.4-apache
WORKDIR /var/www/html

# 必要最低限のPHP拡張とNode.jsのインストール
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get update \
  && apt-get install -y libzip-dev libpq-dev mariadb-client unzip git libfreetype6-dev libjpeg62-turbo-dev libpng-dev \
  && docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
  && docker-php-ext-install zip pdo_mysql mysqli gd exif opcache \
  && a2enmod rewrite \
  && apt-get -y install vim \
  && apt-get install -y nodejs \
  && git clone https://github.com/phpredis/phpredis.git /usr/src/php/ext/redis \
  && docker-php-ext-install redis 

EXPOSE 8080

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin
ENV APACHE_LOG_DIR /var/log/apache2

# composerをインストール
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY ./apache/*.conf /etc/apache2/sites-enabled/
COPY ./php/php.ini /usr/local/etc/php/php.ini
COPY ./app /var/www/html

# composer install を高速化するためのライブラリをグローバルインストール
RUN composer global require hirak/prestissimo
RUN composer install --optimize-autoloader --no-dev
RUN php artisan key:generate  \
  && php artisan config:cache \
  && php artisan view:cache

RUN chmod 777 -R /var/www/html/storage/framework/ \
  && echo "Listen 8080" >> /etc/apache2/ports.conf 

```

### 5.念のためにローカルで動かす
下記のコマンドを実行して、`http://localhost:8080`で画面がみられれば成功です。


```sh
# buildして
docker build ./ -t cloud-run-demo
# 実行
docker run -p 8080:80 -d cloud-run-demo
```

出力される画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/3f37c8da-357e-67f1-7c5f-387ece88bb17.png)

### 6. Cloud Runへデプロイするための設定ファイルを作成する
`GCP`にアカウントがある前提で進めます。

まずは、`.gcloudignore`というファイルをディレクトリ 直下に作成します。ファイル内容は下記の通りです。これは、`composer`で作られる`vendor`や`npm`コマンドで作られる`node_modules`を除外することによりDockerコンテナのイメージサイズを小さくし、デプロイの速度を早めるために使っています。

```sh
*/node_modules/
*/vendor/
```

次に、`cloudbuild.json`というファイルを同じ階層に作ります。`{your-project-id}`の箇所は、GCPで作成したプロジェクトのIDとしてください。
ポイントは`--cache-from`の記述で、これがあることで前回のビルドのキャッシュを取得できるため、次回からのデプロイが高速化されます:rocket:
:point_up:ただし、１ステップ目は**最初のビルドに限り**イメージが見つからない、というエラーが出ます。ご注意ください:point_up:

```json
{
  "steps": [
    {
      "name": "gcr.io/cloud-builders/docker",
      "entrypoint": "bash",
      "args": [
        "-c",
        "docker pull gcr.io/{your-project-id}/cloud-run-demo:latest || exit 0"
      ]
    },
    {
      "name": "gcr.io/cloud-builders/docker",
      "args": [
        "build",
        "-t",
        "gcr.io/{your-project-id}/cloud-run-demo:latest",
        "--cache-from",
        "gcr.io/{your-project-id}/cloud-run-demo:latest",
        "-f",
        "Dockerfile",
        "."
      ]
    }
  ],
  "images": [
    "gcr.io/{your-project-id}/cloud-run-demo:latest"
  ]
}
```

### 7. いよいよCloud Runへデプロイする:tada:

下記のコマンドを実行すると、`GCP`のイメージリポジトリにビルドしたコンテナがpushされます。とても便利ですね。


```sh
gcloud builds submit --config cloudbuild.json --timeout="30m" --ignore-file ".gcloudignore"
```

次に`GCP`のコンソールからサービスを作成します。Webサービスなので、「未認証の呼び出しを許可」しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/3083a1dc-50bf-5a09-4c10-3563791dd960.png)


「次へ」を押した後、既存のコンテナ イメージから 1 つのリビジョンをデプロイする、を選択しましょう。先ほど、pushしたイメージが候補になっていると思います。
![スクリーンショット 2020-09-03 17.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/a482fdb9-5334-7b88-5abc-9a581a0f64c4.png)

次に「詳細設定を表示」して、コンテナポートを`80`にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/4cb180b9-2600-29fe-118b-358c2eb74792.png)

作成ボタンを押して、しばらくしたら完了です:tada:ローカルと同様に、Laravelのデフォルトのトップページが表示されば:ok:です。


## Laravelを高速に動作させるためのコツ
ここで、上記設定を振り返りながらLaravelを高速に動かすためのポイントをいくつかご紹介します。

### OPcacheを有効にする
かなりメジャーな手法であり、ある意味必須とも言える対応ですね。弊社では開発中に無効にしていたのですが、OPcacheを入れたことでレスポンスに１００ms以上改善して驚きました。コードを変更しなくても、かなり効果がありますね。`Dokcerfile`の9行目の記述がポイントです。

```dockerfile
  && docker-php-ext-install zip pdo_mysql mysqli gd exif opcache \
```

`php.ini`にも記述が必要ですので注意しましょう。

```ini
[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.revalidate_freq=60
opcache.max_accelerated_files=4000
opcache.validate_timestamps=1
```

### Laravelのマニュアル(デプロイ)を読み、忠実に従う
こちらにきちんと記述があります。これに従いましょう。

[Deployment(英語)](https://laravel.com/docs/7.x/deployment)
[デプロイ(日本語)](https://readouble.com/laravel/7.x/ja/deployment.html)

`Dockerfile`では31行目以降が、該当します。

```dockerfile
RUN composer install --optimize-autoloader --no-dev
RUN php artisan key:generate  \
  && php artisan config:cache \
  && php artisan view:cache
```

:bangbang::bangbang::bangbang::bangbang:
`cache`の流れでは`route:cache`もパフォーマンス改善に有効です。しかし、デフォルトのままでは`routes/web.php`に、コールバックメソッドがあるため、失敗します。そのため、デモの`Dockerfile`の記述からは除外しています。もちろん、この記述を削除すれば問題なく動きますので、こちらも忘れずに入れると良いでしょう。
:bangbang::bangbang::bangbang::bangbang:

デフォルトのweb.php

```php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

//下記のcallbackが原因です。例えば
//Route::get('/','WelcomeController@index');
//などとすると、問題なく動きます。
Route::get('/', function () {
    return view('welcome');
});
```

上記の対策を全て行った場合、何も対策をしない場合を比べて200ms近くのパフォーマンスアップにつながりました。
人が体感できるレベルで早くなるので是非とも、上記を意識することをお勧めします。

ここまでのソースコードは[Github](https://github.com/joolen/cloudrun-laravel/releases/tag/0.1)にPushされています。こちらも、是非、参考にしてください。