rails アプリを複数台にデプロイする時に考えなければならない静的ファイルの配布について整理。社内用のつもりだったが、公開して困るものでもないのでここに書く。

前提知識

capistrano で rails アプリをデプロイした時の動き、rails の静的ファイルの扱いについて説明する。

アセットプリコンパイル

rails には静的ファイルを扱う仕組みとしてアセットプリコンパイルというものがある。この前処理によって、javascript, css ファイルを結合、minify したり、coffee script を javascript に変換したりする。

$ rake assets:precompile

生成されるファイルには以下のようにファイルの内容を元にしたダイジェスト値が付加される。 これはファイルの内容が変わった場合に、キャッシュされるのを防ぐためである。

public/application-908e25f4bf641868d8683022a5b62f54.css

capistrano-rails でのデプロイ

一般的に capistrano-rails プラグインを使って rails アプリをデプロイすると以下のようなディレクトリ構成ができる。

.
|-- current -> /home/sonots/sample/releases/20141203182648
|-- releases
|   |-- 20140806104707
|   |   |-- log -> /home/sonots/sample/shared/log
|   |   |-- pids -> /home/sonots/sample/shared/pids
|   |   `-- public -> /home/sonots/sample/shared/public
|   |-- 20140806104750
|   |-- 20141203181125
|   |-- 20141203182305
|   `-- 20141203182648
└── shared
    ├── log
    ├── pids
    └── public

rake assets:precompile によって生成した静的ファイルは current ディレクトリで実行され、shared/public ディレクトリに配置される。デプロイのたびに上書きで新しいファイルが積み上げられていく。

ポイントは内容が変わればダイジェスト値が変わるため古いファイルが上書きされないという点である。


なお、古いファイルは 
rake assets:clean を実行することで、最新と直近2つ(デフォルト)の assets を残して掃除することができる。 capistrano-rails 的には  cap deploy:cleanup_assets であるし、 cap deploy でも自動的に実行される。

capistrano-bundle_rsync の場合

さて、自作プラグインの話であるが、capistrano-bundle_rsyncでは、 デプロイサーバ上で git clonebundle install を実行し、rake assets:precompile して生成された静的ファイルを以下のようにしてそれぞれのホストの shared/public ディレクトリに配信する。

rsync -az -e ssh .local_repo/releases/YYYYYMMDDXXXXXXX/public/ #{host}:/home/sonots/sample/shared/public

ポイントは --delete オプションを付けずに上書き同期している点である。古いファイルを消さない。

タスクの定義の仕方は README.md に書いているのでそちらを参照してもらいたい。

補足: ここでは releases/*/public -> shared/public に symlink を貼らないようにしています。また、nginx が参照するパスを shared/public  にして、全ての静的ファイルを shared/public に集めているということです。

アプリのデプロイ

では、複数サーバにアプリをデプロイすることについて考える。また、古い静的ファイルをお掃除することについても同時に考えたい。

Akamai のような CDN は使わずにアプリサーバ上の nginxで静的ファイルも配信するようなシチュエーションを想定している。アプリサーバが A, B 2台あり、その2台には LB (別のnginx インスタンスかもしれないし、Big IP のような箱物かもしれない)を通してラウンドロビンで負荷分散しているものとする。

だめなパターン

  1. A にアプリをデプロイ、再起動
  2. B にアプリをデプロイ、再起動

これはダメである。

なぜならば、1. の時点で A のアプリが再起動されると、A の画面は更新され、新しい静的ファイルを要求するようになるが、 その静的ファイルを取得するためのリクエストは A, B どのサーバに振り分けられるのかわからない。ここで、B に振り分けられてしまった場合、静的ファイルが B にはまだ存在せず問題となる。

* 再起動 = ホットリスタート、または「サービスアウト => 再起動 => サービスイン」とする。

大丈夫なパターン

  1. A, B の順にアプリをデプロイ
  2. A, B の順にアプリを再起動

このような手順にすると、1. の段階で A, B 全てに最新静的ファイル、および古い静的ファイルが配置される。 そのため、2. の段階で A のアプリのみが再起動されたとしても、全サーバに新しい静的ファイルが配置されているのだから、 静的ファイルのリクエストはどのサーバに向けられても問題がない。古い静的ファイルのリクエストが来た場合でも同じである。

古い静的ファイルのお掃除

最後に、古い静的ファイルのお掃除について考えたい。shared/public ディレクトリには静的ファイルを積み上げる方式となっているため、古いファイルがどんどん溜まっていってしまう。 いつ、どのように消すのかについて考える。

これは次のような手順にすればよいだろう。2. で全てのアプリが再起動されていれば、もう古い静的ファイルにはリクエストが来ないため、消してしまって良いということだ。

  1. A, B の順にアプリをデプロイ
  2. A, B の順にアプリを再起動
  3. A, B の順に古い静的ファイルを消す

古い静的ファイルを消すには、以下のようにすると簡単である。各ホストで以下を実行する。

rsync -az --delete /home/sonots/sample/releases/*/public/ /home/sonots/sample/shared/public

capistrano-bundle_rsync で README 通りにデプロイした場合、releases ディレクトリの下にそのバージョンの静的ファイルが残っている。それを shared/public に --delete オプションつきで同期をとることで、古いファイルを削除している。

releases ディレクトリに残っている静的ファイルは全て残すように * と書いている。さきほど「もう古い静的ファイルにはリクエストが来ない」と言ったが、クライアントでのキャッシュを考えると可能性もないとは言い切れない。そこで、念のため1世代前、ついでに releases に残っている全世代(デフォルトで5世代)の静的ファイルを shared に残すようにした。クライアントがアプリ画面はキャッシュしていて、静的ファイルはキャッシュしていないなんて状況は通常ないと思うので心配しすぎかもしれない。