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
HOSTROLEFILTER
はHOSTS
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