設定ファイルがない場合の代替手段と簡易メンテページ

# 設定ファイルがない場合の代替手段と簡易メンテページ

この前[Liteでないアプリの実用的なディレクトリ構造一例](http://qiita.com/azumakuniyuki/items/49106ec3891af32af88d)を書いた[@azumakuniyuki](https://twitter.com/azumakuniyuki)です。

Advent Calendarの例示用にリポジトリを作って二本の記事を書いたのですが、なんとかもう一個ぐらいひねり出せそうな気がしないでもないので、[昨日の記事](http://qiita.com/azumakuniyuki/items/2fcec597335d5fd011e5)の続きを書いてみます。

## 設定ファイルが読込めない?

そもそもWebアプリケーションをデプロイする前にテストをしたり、あるいはステージングサーバで確認をしたり、設定ファイルがないという事態が発生してはいけないのですが、うっかり誰かが消したとか、偶然にもディスクの設定ファイルが書き込まれている部分だけが死んだ(ないやろ)とか、もしかしたらあるかもしれません。

また、ファイルがあっても、編集に失敗して設定ファイルとして有効ではない状態になる事の方が確率としては高いでしょう。[昨日の記事](http://qiita.com/azumakuniyuki/items/2fcec597335d5fd011e5)で登場した `etc/neko.conf` と `etc/auth.conf`です。これらは **Perlのハッシュリファレンス** として記述していますので、Perlとして評価されます。よってSyntax errorになるような記述ミスがあると、下記のようなエラーが出ます。

```
% morbo script/noraneko_web
Couldn't load application from file "script/noraneko_web": Couldn't load configuration from file "Noraneko-Mojo-Sample/etc/neko.conf": Unmatched right curly bracket at (eval 249) line 12, at end of line
syntax error at (eval 249) line 12, near "}
}"
```

## 代替手段を用意しておく

最も簡易で合理的な方法は、アプリケーションを構成するモジュールの中に安全で無難な設定値を書いてしまう事でしょう。「最も」と書きましたが他に良い手段を知っているわけではありません、僕が採っている方法が既定値だけを返すモジュールを用意するという手段であるというだけのことです。

### Noraneko::Config

`Noraneo::Web`はコントローラ用として割り当てた名前空間なので、`Noraneko::Config`という名前で設定ファイルの読込みに失敗した場合の代替設定内容を書いておきます。

```perl:lib/Noraneko/Config.pm
package Noraneko::Config;
use utf8;

sub neko {
    return {
        'site' => {
            'maintenance' => 1,
            'copyright' => 'Copyright (C) 京都市役所ネコ課',
            'ja' => {
                'name' => 'ネコ管理システム',
            },
            'en' => {
                'name' => 'Stray Cats Administration System',
            },
        }
    };
}

sub auth {
    return {
        'database' => {
            'dbtype' => 'PostgreSQL',
            'dbname' => 'nekochan',
            'hostname' => '192.0.2.22',
            'port' => 5432,
            'username' => 'dbadmin',
            'passowrd' => 'nyanko',
        }
    };
}

1;
```

`Noraneko::Config`で定義するサブルーチンは設定ファイル名から`.conf`を取り除いた名前にしてなるべく処理しやすいようにしています。

他に設定ファイルの内容検査とか関連処理を`Noraneko::Config`に入れたい場合は、モジュール名を`Noraneko::Config::Default`なんかにすると良いかもしれません。

## Noraneko::Web

アプリケーションの方では外部設定ファイルの読込みに失敗したら `Noraneko::Config`からファイル名に対応したサブルーチンを呼びだして、その内容を代替設定として読込んでいます。

```perl:lib/Noraneko/Web.pm抜粋
package Noraneko::Web;
use Mojo::Base 'Mojolicious';
use Path::Class;
use Noraneko::Config;

sub startup {
    my $self = shift;
    my $home = new Path::Class::File(__FILE__);
    my $root = $home->dir->resolve->absolute->parent->parent();
    my $conf = {};
    my $fail = 0;

    for my $e ( 'neko', 'auth' ) {
        my $f = $root->stringify.'/etc/'.$e.'.conf';

        eval {
            $conf->{ $e } = $self->plugin( 'Config', { 'file' => $f } );
        };
        $conf->{ $e } ||= Noraneko::Config->$e;
        $fail = 1 if $@;
    }

    for my $e ( values %$conf ) {
        while( my( $k, $v ) = each %$e ) {
            $self->defaults->{'config'}->{ $k } = $v;
        }
    }
    $self->defaults->{'config'}->{'site'}->{'maintenance'} = $fail;

    my $r = $self->routes;
    # ルーティングの処理が続く...
```

設定ファイルを読込むコードを`eval {}`で挟み、外側で読込みに成功していたら存在するであろう値が無ければ、`Noraneko::Config`から代替設定を読込んでいます。

ここでは複数ある設定ファイルのうち一つでも読込みに失敗したら **メンテナンスモード** に移行するようにしています。代替設定でサイトを動かし続けるか、あるいはアラートを投げて対応処置が入るまではメンテナンスモードで稼働させるかはサイトの運営方針次第ですが、本記事では例としてメンテナンスモードに入る事にしています。

メンテナンスモードの判定は、設定ファイル`etc/neko.conf`または`Noraneko::Config`の`maintenance`の値が1であるかどうかで判断する事にしています。

## ところで設定ファイルの既定値があります

前回、12月10日の話で書くべき内容であったのですがうっかりしていまして、`Mojolicious::Plugin::Config`には既定で読込まれる暗黙の設定ファイルに関する動作があります。

1. 環境変数 **MOJO_CONFIG** の値
2. アプリケーション名.conf

Noraneko/Web.pmで以下のようにファイル名を指定しない書き方でConfigプラグインを使うと、上記の1,2の順序で設定ファイルの読込みが試行されます。

```lib/Noraneko/Web.pm抜粋
$self->plugin('Config');
```

先ず、環境変数 **MOJO_CONFIG**の値に入れられている設定ファイルが読込まれます。MOJO_CONFIGの値が空であれば **アプリケーション名.conf** が読込まれます。両方は読込まれません。

 **アプリケーション名.conf** は、この記事で例示しているアプリケーションの場合は **noraneko-web.conf** となります。`script/noraneko_web`と一致する名前(アンダースコアはハイフンに変換されるようです)です。もしも`mojo generate app Noraneko`としていた場合は、`script/noraneko`と`noraneko.conf`となります。

僕はここでの例のように設定ファイルを複数用意している事、`$self->plugin('Config');`だけの記述では読込まれる設定ファイルを捉えにくい事もあり、明示的にコード内に設定ファイル名を特定出来る記述をするか、アプリケーション独自の環境変数(例えば`NORANEKO_CONFIG`とか)から読出す、というようになるべく分かりやすい記述を採っています。

## メンテナンスモード用ルーティングも

突然メンテナンスモードにすると書きましたが、最も簡易なメンテナンスモードの実装は、全てのURLにマッチするルーティングを定義して、「現在メンテナンス中です」的なページを表示する事でしょうか。

```perl:lib/Noraneko/Web.pm抜粋
    my $r = $self->routes;

    my $ctrl = undef;
    my $lang = [ 'ja', 'en' ];

    if( $self->defaults->{'config'}->{'site'}->{'maintenance'} == 1 ) {
        $ctrl = 'admin-maintenance#index';
        $r->route( '/:lang/maintenance', 'lang' => $lang )->to( $ctrl );
        $r->route( '/:lang/:any', 'lang' => $lang, 'any' => qr/.*/ )->to( $ctrl );
    }

    $r->route( '/:lang/user/index', 'lang' => $lang )->to( 'user-root#index' );
    # ...ルーティング設定が続く
```

とりあえずはメンテナンス中の場合、どんなURLが来ても `/{ja,en}/maintenance` へ処理を渡すルーティングを正規表現で言語別にマッチするよう書いています。

ルーティングの設定は書いた順序で有効になるので、

1. メンテナンスページのURL(`/{ja,en}/maintenance`)を定義
2. 全てにマッチするURLの正規表現(`'/:lang/:any',...`)を定義
3. 他のルーティング(これまでの記事で書いたルーティング)を定義

となっていて、3以降に定義されたURLは全て上記2の正規表現に吸収されます。


## メンテナンスページのコントローラ

`/{ja,en}/maintenance` はコントローラ`Noraneko::Web::Admin::Maintenance`が`sub index {...}`で下記のように処理します。と言ってもstashから変数を取り出してメンテナンスページのテンプレートへ渡しているだけの処理です。

```perl:lib/Noraneko/Web/Admin/Maintenance.pm
package Noraneko::Web::Admin::Maintenance;
use Mojo::Base 'Mojolicious::Controller';
use strict;
use warnings;

sub index {
    my $self = shift;
    my $lang = $self->stash('lang');

    $self->stash( 'v' => $self->app->config->{'site'}->{ $lang } );
    return $self->render( 'template' => $lang.'/admin/maintenance/index' );
}
1;
```

## メンテナンスページのテンプレートも作る

```
% mkdir templates/{en,ja}/admin/maintenance/
% vi templates/{en,ja}/admin/maintenance/index.html.ep
```

```html:templates/ja/admin/maintenance/index.html.ep
% layout 'ja/admin/default';
% title 'メンテナンス中 | '.$v->{'name'};
<h1>メンテナンス中です</h1>
<p>
    ただいま緊急のメンテナンス中です。
    ご不便をおかけ致しますが復旧まで今暫くお待ちください。
<p>
```

この記事で例示しているネコ管理アプリケーションは、ネコ課職員(Admin)と市民(User)と動物病院(Clinic)とネコ(Cat)でコントローラを分離しています。ユーザ別にメンテナンスページをそれぞれ表示させる事も出来ますが、それはまた別の記事で書こうと思います。

## github

やはり、ちゃんと動くか心配なのでここで説明している箇所だけ実装したネコの管理アプリを更新してgithubに置いてきました。

[github.com/azumakuniyuki/Noraneko-Mojo-Sample](https://github.com/azumakuniyuki/Noraneko-Mojo-Sample)

## まとめ

Mojolicious::Plugin::Configを使う利点としては、Mojoliciousに標準で含まれているモジュールであるという点でしょうか。もっとも、この例では`Path::Class`を使っていますし、実際にWebアプリケーションを開発する時は他にもPerlのコアモジュールに入っていないものも使いますので、全てMojolicious+コアモジュールで構成するというのは現実的ではありませんし、そうなると前述の利点など何処行ったという感じですが、一つの例として標準的な手段の紹介という立場です。

Mojoliciousで外部設定ファイルを使う良い方法については、今朝[ゆーすけべーさん](https://twitter.com/yusukebe)と話をしていた時に「もっとスマートなやり方がある」って感じの事を聞いたので、ゆーすけべーさんのConfig記事に期待が寄せられますし早く拝読したいところですが、この記事を書いている途中に[未来の日付で公開](http://qiita.com/yusukebe/items/c4d67b49f1deac5a4a80)されました。実際に稼働しているサービス([ボケて](http://bokete.jp/))のコードが例として公開されているので、なかなか勉強になるエントリです。

## 参考

- [Mojolicious::Plugin::Config](https://github.com/yuki-kimoto/mojolicious-guides-japanese/wiki/Mojolicious%3A%3APlugin%3A%3AConfig)
- [250万ダウンロードアプリを支えるBokete.pmを覗く](http://qiita.com/yusukebe/items/c4d67b49f1deac5a4a80)
- 今日も参考ネコ画像はありません