Capistrano でホスト毎に設定(デプロイ手順)を変化させる

Rails のアプリを書いていて、ホストによって routes.rb を切り替えたくなることがあります。
別にそんなのいらないんじゃないの? と思うのですが、お客様の要件なので嫌でも切り替えたくなってきます。自然の摂理ですね。

そんな時、Capistrano でどう書くべきかちょっと調べてみました。

ロールを使う

Capistrano で標準的かつ一番最初に思いつく方法です。
ロール定義とタスク定義に条件を付けることで、ホストごとに実行するタスクを切り替えることができます。

namespace :app do
  desc "Switch routes.rb (for admin)"
  task :switch_admin_routes_rb, :roles => :admin_web do
    run get_routes_rb_switcher(:admin)
  end
  after "deploy:finalize_update", "app:switch_admin_routes_rb"

  desc "Switch routes.rb (for portal)"
  task :switch_portal_routes_rb, :roles => :portal_web do
    run get_routes_rb_switcher(:portal)
  end
  after "deploy:finalize_update", "app:switch_portal_routes_rb"

  def get_routes_rb_switcher(route_type)
    <<-CMD
      test -e #{shared_path}/config/routes.rb.#{route_type} &&
        cp -f #{shared_path}/config/routes.rb.#{route_type} #{release_path}/config/routes.rb
      || true
    CMD
  end
end

このコードを書いておくと、あとはロールを設定する際に

role :admin_web, "web01"
role :portal_web, "web02"

のようにロールを追加しておくだけで ok です。
一つのホストに対して複数のロールをもたせることができるので、
:web ロールと付加的なロールをもたせて置くとよいと思います。

なお、この方法は :roles に一致するホストが一台もない状態だとエラーになります。

`app:switch_admin_routes_rb' is only run for servers matching {:roles=>:admin_web}, but no servers matched

ロールの属性情報を利用する

これでやりたいことが実現できたわけですが、
ちょっとした分岐に対して新しいロールを付加するというのはちょっと大げさな気がします。

というわけで、もうひとつ、ロールの属性を使って条件設定する方法をご紹介します。
ロールの属性情報というのは

role :db  , "localhost" , :primary => true

の :primary => true の部分です。
あまり使っている例を見ないのですが、ここには任意の属性を設定することができます。

この属性を利用した例はこちらです。

namespace :app do
  task :switch_admin_routes_rb, :roles => :web, :only => {:route_type => :admin} do
    run get_routes_rb_switcher(:admin)
  end
  after "deploy:finalize_update", "app:switch_admin_routes_rb"

  task :switch_portal_routes_rb, :roles => :web, :only => {:route_type => :admin} do
    run get_routes_rb_switcher(:portal)
  end
  after "deploy:finalize_update", "app:switch_portal_routes_rb"

  def get_routes_rb_switcher(route_type)
    <<-CMD
      test -e #{shared_path}/config/routes.rb.#{route_type} &&
        cp -f #{shared_path}/config/routes.rb.#{route_type} #{release_path}/config/routes.rb
      || true
    CMD
  end
end

ロールの属性情報はタスクを定義する際に :only や :except 句を使ってフィルタすることができます。

これを利用するためには、次のようにロールを定義します。

role :web, "web01", :route_type => :admin
role :web, "web02", :route_type => :portal

なお、この方法でも :roles に一致するホストが一台もない状態だとエラーになります。

`app:switch_admin_routes_rb' is only run for servers matching {:roles=>:web, :only=>{:route_type=>:admin}}, but no servers matched

parallel を利用する

大抵の場合は先ほど紹介したいずれかの方法で問題ないのですが、
環境が複雑になると「一致するホストが一台もない状態だとエラーになる」という条件が窮屈に感じてきます。
例えば開発環境やステージング環境ではリソースの都合で一台にまとめたい、といったケースが出てきた場合の対処です。

また、似たようなタスクを大量に作っていくのもちょっと見通しが悪くてイマイチと感じたりしてきます。
get_routes_rb_switcher() を作って共通部分をなるべく排除していても、あまり見栄えはよろしくありません。

そこで出てくるのが parallel です。
parallel は run コマンドと同じ位置づけで、リモートホスト上でコマンドを実行するためのコマンドです。
run コマンドの違いとして、run コマンドはすべてのホストで同じコマンドを実行しますが、
parallel コマンドは条件にもとづいてホストごとに実行するコマンドを差し替えます。

条件は様々なものが書けますが、よくサンプルに出てくるものは "in?(:role)" という、ロールによる分岐です。

実際に parallel を使った例がこちらです。

namespace :app do
  desc "Switch routes.rb"
  task :switch_routes_rb do
    parallel do |session|
      session.when "options[:route_type] == :admin",  get_routes_rb_switcher(:admin)
      session.when "options[:route_type] == :portal", get_routes_rb_switcher(:portal)
    end
  end
  after "deploy:finalize_update", "app:switch_routes_rb"

  def get_routes_rb_switcher(route_type)
    <<-CMD
      test -e #{shared_path}/config/routes.rb.#{route_type} &&
        cp -f #{shared_path}/config/routes.rb.#{route_type} #{release_path}/config/routes.rb
      || true
    CMD
  end
end

ここでは "options[:route_type] == :admin" のようにロールの属性情報を利用して分岐を仕掛けています。
このように書くとタスクを複数書かずに分岐を行うことができます。

この parallel を使うと条件に一致するホストがない場合でもエラーになりません。
(ちなみに parallel では session.else というどの条件にも一致しない場合に実行するコマンドも書けます)


というわけで、ちょっとした分岐が必要になった時の対処を調べてみました。

追記: on_no_matching_servers というオプションがあった

ここまで書いて、ソースを読んでいたら :on_no_matching_servers というオプションがあることに気づきました。
タスクを定義する際に次のように書くと、条件に一致するホストがない場合でもエラーになりません。

  desc "Switch routes.rb (for admin)"
  task :switch_admin_routes_rb, :roles => :admin_web, :on_no_matching_servers => :continue do
    run get_routes_rb_switcher(:admin)
  end
  after "deploy:finalize_update", "app:switch_admin_routes_rb"

実行してみたらこんなメッセージが出て次に進んでいました。

 ** skipping `app:switch_admin_routes_rb' because no servers matched

undocumented な上に長くて覚えられないのですが、こんな方法もあるよ、という参考まで。
(ぐぐっても 720件しか出てこない…)