capistrano + nginx + Rails 環境で簡単にメンテナンス画面を表示する方法


## はじめに

サーバーのメンテナンス中には、503レスポンスと一緒にメンテナンス画面をレスポンスすることが多いと思います。
nginxのレイヤーで503レスポンスとメンテナンス画面を表示する仕組みと、それをcapistranoで操作する仕組みを作りました。

今まで、nginx + unicorn な環境でメンテナンスモードにするときには、いちいちnginxの`nginx.conf`をいじって出していました。
綺麗ではないですし、オートスケール環境の時は全てのサーバーの`nginx.conf`を修正するのも現実的ではないです。

ということで、capistranoで、

```bash
bundle exec cap production maintenance:on
```

を実行すると、メンテナンス画面が表示されて、

```bash
bundle exec cap production maintenance:off
```

を実行すると、元に戻る機能を作ってみました。

[turnout](https://github.com/biola/turnout)というgemを使うとメンテナンス画面を簡単に出すことはできますが、[turnout](https://github.com/biola/turnout)の場合は、メンテナンスモードであってもRailsへアクセスしてメンテナンス画面を表示します。
例えば、`database.yml`の設定を変えているタイミングでRailsからデータベースにアクセスできないような状態でアクセスするとメンテナンス画面ではなく500エラーがレスポンスされてしまい、正しくメンテナンス画面を表示できません。
また、できればメンテナンス中はRailsが稼働するunicornサーバーへのアクセスの受け入れは避けたいです。

そのため、nginx のレイヤーで503レスポンスとメンテナンス画面を表示する仕組みを作りました。

## 前提

すでにcapistrano + nginx + unicornを利用したデプロイの仕組みができているものとして説明します。
「capistrano nginx unicorn」と検索するといい感じの記事がいくつかヒットするので参考にしてみてください。

- [Capistrano で Rails アプリケーションの自動デプロイ](http://qiita.com/Salinger/items/4ee4f3c5ebd5227196c0)
- [Capistrano3でUnicorn+Nginxな環境にRailsをデプロイする:初心者向け](http://qiita.com/zaru/items/9e02706b27094ce36902)
- [rails + nginx + unicorn連携](http://qiita.com/shinyashikis@github/items/ace49154f0c71c65b2c9)

## 設定

### maintenance.html の作成

まず、`maintenance.html`を作成して、`/public/maintenance.html`に配置してください。
`maintenance.html`は、CSSなどをstyleタグで埋め込んだ１枚だけの簡単なファイルにしておくといいと思います。

### deploy.rb の設定

メンテナンス中であることは`/public/tmp/maintenance.html`が存在するかどうかで判断します。
デフォルトのままだと、リリースのたびに`/public/tmp`ディレクトリは、リセットされてしまいます。
`/public/tmp`をshared_pathに退避させるために、`:linked_dirs`に追加します。

```ruby:config/deploy.rb
set :linked_dirs, fetch(:linked_dirs, []).push('public/tmp')
# 'public/tmp'以外にも :linked_dirs に追加するものがある場合は追加してください。
```

次に、メンテナンスモードのオン・オフを操作する、`maintenance:on`タスクと`maintenance:off`タスクを作成します。
`/lib/capistrano/tasks`にrakeファイルを追加してもいいのですが、今回は簡単のため、`deploy.rb`に追加します。
以下を、`config/deploy.rb`の末尾に追加して下さい。

```ruby:config/deploy.rb
namespace :maintenance do
  desc 'start maintenance'
  task :on do
    on roles(:web) do
      target_dir = "#{shared_path}/public/tmp"
      target_path = "#{target_dir}/maintenance.html"
      source_path = "#{release_path}/public/maintenance.html"
      execute :mkdir, '-p', target_dir
      execute :cp, '-f', source_path, target_path
    end
  end

  desc 'stop maintenance'
  task :off do
    on roles(:web) do
      target = "#{shared_path}/public/tmp/maintenance.html"
      execute :rm, target if test "[ -f #{target} ]"
    end
  end
end
```

### .gitignore の設定

気になる人は、.gitignoreに`public/tmp`を追加してもいいと思います。

```.gitignore
public/tmp/*
```

### nginx.conf の設定

最後にnginx.confの設定をします。

```nginx.conf
	upstream unicorn {
      server unix:/var/www/rails/shared/tmp/sockets/unicorn.sock;
    }

    server {
		listen       80;
		server_name  example.com;
		root         /var/www/rails/current/public;
		
		proxy_connect_timeout 60;
		proxy_read_timeout    60;
		proxy_send_timeout    60;

        set $maintenance false;
        
        if ( -f $document_root/tmp/maintenance.html ) {
            set $maintenance true;
        }

        if ( $args ~ "mode=hogehoge" ) {  
            set $maintenance false;
        }

        location @maintenance {
            try_files /tmp/maintenance.html /maintenance.html;
        }
        
        location /healthcheck {               # healthcheckをしている場合
            if ( $maintenance = true ) {
                return 200;
            }
        }
		
        location / {
		    index index.html;

            if ( $maintenance = true ) {
                error_page 503 @maintenance;          # メンテナンスページの設定
                return 503;
            }

		    try_files $uri @app;
		}
		
        location @app {
		    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		    proxy_set_header Host $http_host;
		    proxy_redirect off;

		    proxy_pass http://unicorn;
		}
    }
```

これを設定して、

```bash
sudo service nginx configtest  # successful と表示されることを確認した上で
sudo service nginx reload
```

とすると`nginx.conf`が反映されます。

`/var/www/rails/current/public/tmp/maintenance.html`が存在するときに`$maintenance`をtrueにして、メンテナンス画面をレスポンスします。
また、実際のサイトを確認したい場合は、雑ですが、URLに`?type=hogehoge`を追加すると実際のサイトを確認できます。

AWSのELBなどでヘルスチェックをしている場合は、メンテナンスモード中は、200レスポンスを返すように設定してください。

## 最後に

設定は以上です。あとは、設定を含んだソースコードをデプロイした後に、

```bash
bundle exec cap production maintenance:on
```

を実行すると、メンテナンス画面が表示されて、

```bash
bundle exec cap production maintenance:off
```

を実行すると、元に戻ります。
ぜひ参考にしてみてください。

## 参考URL

- [Nginxでメンテナンスページ用意する技](http://blog.kenjiskywalker.org/blog/2015/03/12/nginx-maintenance/)