Capistrano で rsync
Capistrano によるファイル転送手段で標準で用意されてる方法は
upload
やput
でファイルをアップロードする(scp / sftp 経由)- Git や Subversion などの SCM からファイルをチェックアウトする
であり,rsync はない(と思う)。
なので,Capistrano で rsync しようと思った(あ,でも rails とか用の標準 deploy レシピで使う方法とかじゃないです; 単に task に rsync を書くだけ)。
Capfile は実際 ruby のコードなのでなんとでもやりようはある。
task :rsync_task do source_path = "/source/path/" target_path = "target_host:/target/path/" `rsync -avz #{source_path} #{target_path}` end
`〜`
の部分は,load "deploy"
で標準 deploy レシピを読み込んでいる場合は run_locally
を使うとより『っぽい』かもしんない((ちなみに git 上の最新版では run_locally
が Global に定義されているので,deploy レシピを読み込まなくても使える。))。
でも,これだと target_host
が決め打ちになってしまう。せっかく task には :roles
や :hosts
を指定できるのに活用できてない。
rdoc やソースを読み込んでたら find_servers()
/ find_servers_for_task()
という API を使えばいい模様。ただしこれらの関数の戻り値はサーバ名そのものではなくて,サーバ名を含む Object (Struct) になってる。実サーバ名は .host
でとれる。
task :upload_via_rsync, :roles => [ :app ] do source_path = "/source/path/" target_path = "/target/path/" username = user || ENV['USER'] find_servers_for_task(current_task).each do |server| `rsync -avz #{source_path} #{username}@#{server.host}:#{target_path}` end end
こんな感じ。これで動いた。
さて。
このままだと rsync を使う task ごとに上記のコードをいちいち書かなきゃいけなくなる。避けたい。
task から別の task を呼び出すことは可能である。なので,そのサブタスクに上記コードを再利用可能なように書けば(いちおう)いける。その場合,上記の current_task
の部分を task_call_frames[-2].task
にすればよい。
でも転送元パスと転送先パスを options で指定してかなきゃいけなくなる(それでも構わないけど)。
なので,Configuration の Action として関数を定義して,それを呼び出すようにする。
module ::Capistrano class Configuration module Actions module Rsync def rsync_upload(from, to, args=[], options={}) options = current_task.options.merge(options) servers = find_servers_for_task(current_task, options) raise Capistrano::NoMatchingServersError if servers.empty? servers = [servers.first] if options[:once] if logger logger.trace "servers: #{servers.map { |s| s.host }.inspect}" end servers.each do |server| begin u = server.user || options[:user] || exists?(:user) ? fetch(:user) : nil dest = "#{server.host}:#{to}" if not u.nil? dest = "#{u}@#{dest}" end args.push from args.push dest args.unshift '-n' if dry_run args.flatten! if logger logger.trace "executing rsync: #{args.join(' ')}" end system 'rsync', *args or raise Capistrano::CommandError rescue => error raise unless current_task && current_task.continue_on_error? end end end end end end end class ::Capistrano::Configuration include Actions::Rsync end task :rsync_task, :roles => [ :app ] do rsync_upload '/source/path/', '/destination/path/', %w( -v -az --delete ) end
実際は別モジュールに切り出して load()
したほうがいいと思う。あと,Ruby に書きなれてないからか,インデントが深くなって横桁があふれそうになってしまった。関数をこまかく分離するべきなの?
ちなみに上記のようにわざわざ長ったらしい namespace の Module を定義せずに直接 Capistrano::Configuration
class を再 open して書いてもいいんだけど,ちょっとカッコつけて書いてみた。
ほんとは IO.popen()
あたりでコマンドを呼び出して出力をキャプチャしたいところ。
ともかく,run_tree()
や execute_on_server()
あたりのコードをコピペまくったおかげで,cap コマンドに -n
オプションを指定した場合にきちんと dry-run されるようになった。
指定した秘密鍵を使って ssh アクセスする
:ssh_options
という variable に ssh 時のオプションを設定して実行すればよい。
role :app, 'appserver' set :user, 'other_user' set :ssh_options, { :auth_methods => %w( publickey ), :keys => %w( ~/.ssh/other_user.id_dsa ), } desc "Hello World" task :hello, :roles => [ :app ] do run "echo Hello World! $HOSTNAME by $USER" end
どんな値が設定可能かは Capistrano::SSH::connection_strategy()
あたりを参照。そのコードを参照すればわかるけど,上記ではグローバルな :user
にユーザ名を設定しているが,:ssh_options
の :user
に設定してもいい(あんまり意味ないけど)。
上記では set()
関数を使って設定しているけど,
ssh_options[:auth_methods] = %w( publickey ) ssh_options[:keys] = %w( ~/.ssh/other_user.id_dsa )
と書くこともできる。でもこの書き方はあまりおすすめしない((:ssh_options
という variables がデフォルト(Capistrano::Configuration(::Variables)#initialize
)で設定済だからできるだけ。))。ハッシュ全体の上書きが気に入らないのなら fetch(:ssh_options)
した値に merge!()
すればいいのかな。あんまりそういう局面はないと思うけど。
task 単位で秘密鍵を変えたい場合はどうすればいいんだろう。
Capistrano のコードやや追い
まずもって Capistrano::Configuration
は,各サブモジュールを include
した Mix-in class になってる。
んで,task の定義で使われる #task()
メソッドは概略は以下のようになってる。
# class Capistrano::Configration (module ::Namespaces) def task(name, options={}, &block) # 同一名称のメソッド(not タスク)が定義されていればエラー tasks[name] = TaskDefinition.new(name, self, {...}, &block) # 同一名称のタスクの再定義でなければ,Capistrano::Configuration にメソッドとして登録 metaclass.send(:define_method, name) { execute_task(tasks[name]) } end
#task()
メソッドは Capistrano::Configuration
のインスタンスメソッドである。Capfile を書くときにあたかもグローバルメソッドであるかのように呼び出せるしくみは後述。
最後のとこで define_method()
してるので,task の中から別の task をメソッド名を通じて呼び出すことができる。
中ほどで出てきた class Capistrano::TaskDefinition
の定義は下記のような感じ。
# class Capistrano::TaskDefinition def initialize(name, namespace, options={}, &block) @name, @namespace, @options = name, namespace, options # ... @body = block or raise ArgumentError end
ただのコンテナクラス。
で,#task()
の最後のあたりで define_method()
したメソッドの内部で #execute_task()
を呼んでいたけど,それの実装は下記のとおり。
# class Capistrano::Configration (module ::Execution) def execute_task(task) logger.debug ... push_task_call_frame(task) invoke_task_directory(task) ensure pop_task_call_frame end
push_task_call_frame()
と pop_task_call_frame()
でタスクの呼び出しの入れ子状態を管理しているんだけど,まぁそこはおいとく。
実際に task を呼び出している private メソッドの invoke_task_directory()
の実装は,
def invoke_task_directory(task) task.namespace.instance_eval(&task.body) end
のように task の namespace(というのはここでは,上記をみるとわかるけど Capistrano::Configration
のインスタンスのことだ)のコンテキストで instance_eval()
してる。
んで,Capfile のなかでグローバルメソッドのように task
メソッド等を呼び出せるのは,Capistrano::CLI(::Execute)#execute!()
で Capistrano::Configuration インスタンスを生成して load()
してるんだけど,Capistrano::Configuration(::Loading)#load()
で instance_eval()
してるから(評価コンテキストが Capistrano::Configuration
インスタンスになる)。
あと,set
/ fetch
まわりについては,Capistrano::Configuration(::Variables)
あたりで実装されてる。実体は Capistrano::Configuration
クラスの @variables[]
インスタンス変数。このモジュールの included()
メソッドで,クラスの respond_to?()
や method_missing()
メソッドを再定義してる。簡単にいうと,そこで fetch()
を呼び出しているので,あたかも(attr_reader
で定義された)プロパティのように値をひきだすことができる。