MixIn に強い Webistrano Recipe の書き方


Webistrano はプロジェクト間でレシピを共有できる。レシピの再利用を促進する設計になっているので、「単機能のレシピをたくさんつくって組み合わせる」という使い方をしたい。

しかしロールが絡むと話がややこしくなる。ややこしくなる具体的な例があったほうが後述するテクニックの利点を説明しやすいので、まず「単機能のレシピを再利用しづらくなる例」を示す。

仕様

電子書籍の執筆・販売プラットフォームを提供するウェブサイトがあり、 :app, :web, :db の他に :pdf, :drm というロールが存在する、としよう (注: 実在のウェブサイトとは関係ありません、あくまで例です)

:pdf ロールのサーバはダウンロード販売のマスターデータとなる PDF ファイルを非同期で作成する。 :drm ロールのサーバは購入者のメールアドレスを埋め込んだ PDF ファイルを非同期で作成する。いずれのロールも Resque のワーカーが常時起動している。

レシピの例 (改善前)

この場合、普通は以下のような二つのレシピを用意し、 Webistrano で新規プロジェクトを作成後、両方のレシピを登録するだろう (注: あくまで例なので、このままコピペしても動きません。利用の際は環境にあわせて適時修正してください)

# Recipe: Restart Unicorn
namespace :deploy do
  task :restart, :roles => [:app]
    run "kill -s QUIT `cat /var/run/unicorn.pid`"
    run "unicorn -c /etc/unicorn.rb -E production"
  end
end
# Recipe: Restart Resque
namespace :resque do
  task :restart, :roles => [:pdf, :drm] do
    run "kill -s QUIT `cat /var/run/resque.pid`"
    run "PIDFILE=/var/run/resque.pid QUEUE=pdf,drm RAILS_ENV=production rake environment resque:work"
  end
end

after 'deploy:restart', 'resque:restart'

問題点

この二つのレシピの組み合わせは、 [:app, :pdf, :drm] という三つのロールが全て定義されているプロジェクトならば問題なく動く。典型的には、全てのロール、つまり全てのサーバに対してデプロイしたい場合だろう。小規模でシンプルなシステム構成のウェブサイトであれば、全台にデプロイするプロジェクトが一つあれば十分だ。

サーバの台数が増えてくると、 :pdf ロールだけにデプロイして Resque ワーカーを再起動したくなるかもしれない。 Webistrano を使う場合、デプロイ実行時に対象サーバのチェックを外すよりもプロジェクトを分けるほうがわかりやすいので、新しいプロジェクトを作って :pdf ロールを指定し、レシピを二つ登録しよう。

しかしこれはうまくいかない。 resque:restart というタスクには [:pdf, :drm] というロールが指定されているため、 :drm というロールがないとデプロイ実行時にエラーがでてしまうのだ。

タスクの定義時にロールをインラインで直接指定しているのが原因だが、実行時に環境変数でロールを渡せる Capistrano とは違い、 Webistrano のインターフェースではデプロイ時にロールを特定するのは手間がかかる。サーバの台数が増えるとチェックを外すのも一苦労だし誤操作のおそれもある。

レシピを再利用できないからといってロール指定だけが異なるレシピを複数作るのも良くない。タスク定義部分のコードが複数レシピ間で重複してしまうので修正漏れのおそれがあるし、似たようなレシピがたくさんあると管理もしづらい。タスク定義の重複はなくし、ロール指定は柔軟に行えるようにしたい。

レシピの例 (改善後)

そこで、 http://tech.feedforce.jp/capistrano.html で紹介されている Tips : タスクをメソッド内で定義 というテクニックを使う。

# Recipe: Restart Resque
def prepare_resque_tasks(target_roles)
  namespace :resque do
    task :restart, :roles => target_roles do
      run "kill -s QUIT `cat /var/run/resque.pid`"
      run "PIDFILE=/var/run/resque.pid QUEUE=pdf,drm RAILS_ENV=production rake environment resque:work"
    end
  end
end

end

resque_roles = [:pdf, :drm]

after 'deploy:restart' do
  defined_roles = roles.keys
  prepare_resque_tasks(resque_roles & defined_roles)
  resque.restart
end

resque.restart というタスクを実行する 前に prepare_resque_tasks というメソッドを呼び出して、動的にタスクを定義している。このとき、プロジェクトに指定されているロールを roles.keys で取得し、あらかじめ列挙しておいた resque_roles との集合の積をとり、それを prepare_resque_roles に渡すことで、「resque:restart を実行可能 かつ このプロジェクトで指定されているロール」に対して resque:restart タスクが実行されるように、柔軟にタスクを定義できる。

このテクニックを利用すると、「プロジェクトに指定されているロールとレシピが期待するロールの組み合わせが矛盾して実行時エラー」というトラブルを避けて、安全にレシピを再利用できる。