イフブロ

イフブロ

インフラエンジニアのブログ

CapistranoのHost/Role Filterのやり方

こんにちは。
今日も作業の中でハマった事を書きたいと思います。
誰かのお役に立てればいいなと。

CapistranoのFilter機能ですが、どうやら最新を取るとDocumentの通りにはいかなくなっている様なので、その内容を本日は書きます。
前回の記事「Capistranoでon rolesだけなく on host 又は on serverみたいに1台指定固定をやりたい時の方法」では、
コード上で個別のサーバーでしか動かない処理を実装しました。

今回は、共通のコードなのでon roles(:all)で書きたいけど、実行対象毎に、:webロールだけに実行したい。 さらに、これらはコマンドラインベースではなくて、deploy.rbの定義でもやれるか。という所を検証しています。

ネットの記事も2012~2014年頃が中心にありますが、Capistrano v3 に入った辺から仕様が大きく変わったりしている様ですね。 あまり詳しく追っていませんが、ほんとよくハマってます・・・ HostFilterをconfigからやりたかったのに、何故か全然効かなかったので調査した話。 また、改めてコマンドラインベースでのフィルタも希望通りに動いていなかったので調査した話になります。

Capistranoのバージョン

私の環境は以下のバージョンで動いています。

> gem list | grep capistrano
capistrano (3.4.0)
capistrano_colors (0.5.5)

結論

Filterは以下の様にしましょう。

# コマンドラインベース
> ROLES=web cap prod cleanup:repository
> HOSTS=appserver01 cap prod cleanup:repository

※ 他記事にある様なon rolesを上書きして実行される事はありませんでした。

# コンフィグファイルベース
> vi config/deploy.rb
set :filter, :role=> "web"
  OR
set :filter, :host => "appserver01"


※ deploy.rbの中でロジックを記載して、:hostに設定してFilterする事が可能です。

Filterの機能

FilterはRoleとHostのFilter機能が提供されています。 色んなサイトを見るとHOSTFILTER と HOSTROLEFILTERを使いましょうとか、 HOSTSやROLESを使いましょうとか書いてますが、現時点でのコードでは仕様が変わっています。

以下がそのコードです。

> capistrano-3.4.0/lib/capistrano/configuration.rb
 98     def setup_filters
 99       @filters = cmdline_filters.clone
100       @filters << Filter.new(:role, ENV['ROLES']) if ENV['ROLES']
101       @filters << Filter.new(:host, ENV['HOSTS']) if ENV['HOSTS']
102       fh = fetch_for(:filter,{})
103       @filters << Filter.new(:host, fh[:host]) if fh[:host]
104       @filters << Filter.new(:role, fh[:role]) if fh[:role]

環境変数にROLES / HOSTSが定義されていた場合のみ。Filterに定義を足しています。 また、:filterの変数の中に、ハッシュでhostとroleが指定されていた場合にもFilterに定義が足されます。

検証

config/deploy/staging.rb は以下の様にサーバーを定義しています。 サーバー1台での検証です。

server 'localhost', user: 'umisora', roles: %w{app1 ope}
server '127.0.0.1', user: 'umisora', roles: %w{app2 ope}
server 'localhost4', user: 'umisora', roles: %w{web}

実行するタスクは以下の様にしました。

namespace :test do
  task :remote_test do
    on roles(:all) do
      output = capture "echo hostname=`hostname`"
      info output
    end
  end
# end list namespace
end

この状態で、config/deploy.rbにFilterを定義せずに実行してみると、

> cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging 
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora
INFO hostname=test-umisora
INFO hostname=test-umisora

期待通り、3回(3サーバー分)実行されています。

ROLES/HOSTS 指定

実行時指定で検証してみます。

> ROLES=app2 cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

対象ロールの分(1台分)だけ実行されています。
※ ちなみにappと指定した場合は1行も表示されません。部分一致ではありません。
但し、以下の様な正規表現であればマッチします。 ROLES=app.* cap staging test:remote_test --trace

では、HOSTSを指定した場合は

HOSTS=localhost cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora
INFO hostname=test-umisora

お、2行出てきましたね。HOSTSは部分一致です!前方一致でも後方一致でもなく部分一致でした。

で、次に他の記事でも書かれている様に、コードで指定されたrolesが上書きされるのか?という点も検証してみます。
先ほどのテストコードを一部修正します。

namespace :test do
  task :remote_test do
    on roles(:app1) do         <=== ROLEを固定する。
      output = capture "echo hostname=`hostname`"
      info output
    end
  end
# end list namespace
end

この状態で実行すると、

> cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

期待通り、指定されたROLEに属するサーバーだけが実行されました。
この状態で、先述したオプションを実行すると、、、

ROLES=app2 cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test


HOSTS=127.0.0.1 cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test

どちらにおいても、期待されるINFO hostname=test-umisoraのメッセージが出てきませんでした。
つまり、上書きではなく複合条件として設定されている様に見受けられます。
複合条件でtrueになるケースも実行しましょう。

ROLES=ope cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

HOSTS=localhost cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

こちらは、複合条件に合致した件数分出力されています。
これらから、以前あったHOSTFILTER HOSTROLEFILTERHOSTS ROLESに置き換わった事が確認取れました。

set :filterで定義してみる。

同じ検証を何回もしても意味がないので、動く事だけ確認します。

> vi config/deploy.rb
set :filter, :host => 'hostname'
#set :filter, :role => 'app2'

> cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

> vi config/deploy.rb
# set :filter, :host => 'hostname'
set :filter, :role => 'app2'

> cap staging test:remote_test --trace
** Invoke staging (first_time)
** Execute staging
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke test:remote_test (first_time)
** Execute test:remote_test
INFO hostname=test-umisora

期待通りの結果が返ってきています。 以上です。

ネットの検索は便利ですが、大きく仕様が変わると多数の情報の方が古い事もあるので難しいですね。
出来る限りソースを追いかけて解決する様に引き続き頑張ろうと思います。

2016/02/03 補足 HOSTとROLEの複数指定

複数指定の場合を確認。 コード上は

module Capistrano
  class Configuration
    class Filter
      def initialize type, values = nil
        raise "Invalid filter type #{type}" unless [:host, :role].include? type
        av = Array(values)
        @strategy = case
                    when av.size == 0 then EmptyFilter.new
                    when av.include?(:all), av.include?('all') then NullFilter.new
                    when type == :host then HostFilter.new(values)
                    when type == :role then RoleFilter.new(values)
                    else NullFilter.new
                    end
      end

      def filter servers
        @strategy.filter servers
      end
    end
  end
end

となっている。:hostが指定されていたら、HostFilter.newされるので追いかけると、

class HostFilter
      include RegexFilter

      def initialize values
        av = Array(values).dup
        av.map! { |v| (v.is_a?(String) && v =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/) ? v.split(',') : v }
        av.flatten!
        @rex = regex_matcher(av)
      end

      def filter servers
        Array(servers).select { |s| @rex.match s.to_s }
      end
    end

引数のvaluesをArrayで処理している。 RubyのArrayの書き方は ["value1","value2"] なので、カンマ区切りの指定が必要そうですね。Array