Laravelアプリのメンテナンスモードがコンテナ運用に対して微妙だったので、自作した件

こんにちはみなさん

Laravelはとても書きやすく、「こんなふうな機能がほしいなぁ」って言う要望を割りと簡単に実装させてくれし、テストについてもちゃんと考えているので、社内でも結構普及してきていたりします。

一方で、この機能はどうやって動いているんだろう？っていうのを調べると、ちょ～っと不満を持つ部分があったりします。
まあ、そういうものについては自分で作っちゃえばいい話です。

というわけで、Laravelのメンテナンスモードの実装が個人的に気に入らなかったというか合わなかったので、別途ミドルウェアを作ってコンテナ運用チックなメンテナンスモードを実装してみました。

今回のLaravelは 5.2 のものを使用しています。

# メンテナンスとは
ゲームとか運用している人にはおなじみかと思いますが、メンテナンスとはユーザーがアプリを利用できないようにして、その間に運用側が作業をするための状態です。
用途としては

- 動作コードの更新
- DBの操作
- バグ発生時の緊急停止

とかですかね。
ゲームとかだとリリース直後とか新規イベントリリース直後とかに頻繁にメンテ入りしたりします。

# Laravelでのメンテナンスモード
## 動作
Laravelではコマンドラインでメンテナンスモードに突入できるようです。
https://laravel.com/docs/5.2/configuration#maintenance-mode

```shell-session
$ php artisan down # メンテIN
$ php artisan up   # メンテOUT
```
## 実装確認
では、メンテナンスチェックをLaravelがどのように行っているかを確認してみましょう。
`app/Http/Kernel.php`を見ると、以下のミドルウェアでメンテナンスチェックしているようです。

```app/Http/Kernel.php
protected $middleware = [
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class
];
```
実際にミドルウェアを見ると、更に`Illuminate\Foundation\Application`というクラスの`isDownForMaintenance`というメソッドを参照していました。
そのメソッドを確認してみると。。。

```Application.php
    public function isDownForMaintenance()
    {
        return file_exists($this->storagePath().'/framework/down');
    }
```
`storagePath`にファイルが有るかどうかで判断しているようでした。
`config/filesystems.php`のdefaultをS3にしても、普通に`/var/www/storage`を参照してました。

## 利点
この実装の利点は、メンテナンスモードと通常モードの切替を瞬時に実施できることです。
`php artisan down`を走らせると、メンテナンス状態を示すファイルがtouchされ、以降はこのファイルの存在を参照続けるため、事実上メンテINにかかる時間は、ファイルをtouchする時間( +PHPの動作時間)と同じと考えて良いでしょう。
同様に、`php artisan up`はメンテナンスを示すファイルを削除するだけですので、これも一瞬で完了するわけです。

## 問題点
### ローカルのストレージディレクトリを参照している
フレームワーク内部のストレージディレクトリにメンテナンスファイルを設置しているため、複数のサーバがある場合は、各サーバで`php artisan down`を入力するか、NFSを使ってフレームワークの設置場所を共有化する必要があります。

さらに、私のようにコンテナ運用していると、もっとやりづらくなります。
[PHPカンファレンス関西でも実際のコンテナ運用について喋りましたが](https://speakerdeck.com/niisantokyo/hitasurale-sitephpapuriwokontenayun-yong)、私の場合はコンテナ内部に動作コードを入れていいて、ストレージは全部S3などの外部サービスに頼っています。

### コマンドを叩く必要がある
普通はそれがどうしたっていう感じなのですが、コンテナ運用しているアプリでは、基本的には各コンテナに干渉することはしません。つまり、`docker exec`などを使って、動作中のコンテナに入り込んで作業をするということがないようにしています。

もちろん、動作コードをNFSに入れておき、各コンテナインスタンスの共有ディスクにそのNFSを指定し、更にその共有ディスクを各コンテナの共有ボリュームに指定することで、NFS上でコマンド叩くだけで一気にメンテINできるとは思います。
が、構築が面倒なんで、私は採用しませんでした。

### ホワイトリストなどが使えない
あくまでデフォルトのミドルウェアだと使えないという意味で、[自作すればできます](https://toniperic.com/2015/12/01/maintenance-mode-and-whitelists-in-laravel-5)。ようは、メンテナンス中にチームメンバーで動作確認するために、特定のIPだけは通常動作可能にしたりとか、特定のURL (ヘルスチェックとか) だけメンテナンスモードを解除しておくとかそういったものは、自作する必要があります。

# 環境変数でメンテINする仕組みを作る
前述したように、LaravelのメンテINの仕組みは私の運用環境には合わないので、とっとと自作しちゃうのが良いでしょう。
基本思想は以下のとおりです。

- AWSのECSでの運用を前提にする
- メンテ中かどうかは環境変数で指定
- メンテ中はヘルスチェックにはステータス200で、それ以外は503を返す

## 実装
方針が決まれば後は実装するだけです。
ミドルウェアの作り方とか動作とかは[前に書いたやつを参照してもらえればよいかと](http://qiita.com/niisan-tokyo/items/663300f8df1c6c89f0ae)思います
まず、メンテナンス中かどうかを環境変数から取るのですが、私の場合は一旦configを介して設定することにしました。

```app/config/app.php
'maintenance'   => env('MAINTENANCE_MODE', false),// メンテインするときは、これをtrueにする
```
次にミドルウェアを作ります。

```app/Http/Middleware/MaintenanceCheck.php
<?php

namespace App\Http\Middleware;

use Symfony\Component\HttpKernel\Exception\HttpException;

use Closure;

class MaintenanceCheck
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->path() != 'healthcheck' && config('app.maintenance') === true) {
            throw new HttpException(503, 'メンテナンス中です');
        }
        return $next($request);
    }
}
```
まあ、`php artisan make:middleware`で作ったテンプレートにメンテナンスチェックのコードを入れるだけですね。IPによるホワイトリストは今回は省略して、ヘルスチェックだけ素通りさせるようにしています。
最後に`Kernel`に登録して終了です。

```app/Http/Kernel.php
    protected $middleware = [
       // \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\MaintenanceCheck::class
    ];
```
以上で、**環境変数`MAINTENANCE_MODE`が`true`であれば、`healthcheck`以外の全てのURLがメンテ中となってステータス503を返す**実装が完了しました。

## ECSでの運用
運用はちょっと面倒ですが手動で行います。
ECSがどのように動いているかは[このへん](http://qiita.com/niisan-tokyo/items/4e72c6c11c3f9562fe66)でも見ていただければいいかなと思います。
で、メンテに入るための手順は以下のとおりです。

1. 対象となるタスクの新しいリビジョンを作成する
2. 新しいリビジョン上で、環境変数`MAINTENANCE_MODE`を`true`に設定する
3. 新しいリビジョンを作成したら、そのタスクをサービスに反映する
4. サービスがタスクを再配置したらメンテINとなる

メンテINまでにどれだけ時間がかかるかは、多分アプリの規模によるのかもしれませんが、2つのインスタンスで、一つずつのコンテナ立ち上げを行っていた場合は、反映は1分くらいで完了しました。
メンテ前に予めタスクを作っておき、メンテINの直前にサービスに更新をかけるのが良いかと思います。

メンテOUTに関しては上述した手順で、`MAINTENANCE_MODE`を`false`にするだけでオッケーです

## 利点と欠点
外部から注入される環境変数でメンテの有無を判断しているので、コンテナの内部に変更を入れたり、コマンドを叩いたりする必要がありません。
一方で、結局はコンテナを再配置しているので、どうしてもメンテINに時間がかかるというのが欠点です。

また、タスクをいちいち手動で更新するのは、頻繁にメンテを入れるのなら面倒かもしれません。
そういう場合はchatbotでも使って「メンテIN!!」と叫ぶとメンテ入りするような仕組みを作るのが良いかもしれません (チラッ

# まとめ
というわけで、Laravelが用意しているメンテの仕組みだと、自分で組んだコンテナ運用環境に合わなかったので、メンテの仕組みを自作してみました。

フレームワークに沿うようにしてシステムを組んだほうが良いのか、自分のシステムに合わせて、フレームワークの仕組みを使わずにモノを自作したほうが良いのかは、悩みどころですが、今回みたいに簡単にやれることなら、とっとと作っちゃうのが楽だなと思います。

今回はこんなところです。