Capistrano で rsync

Capistrano によるファイル転送手段で標準で用意されてる方法は

  • uploadput でファイルをアップロードする(scp / sftp 経由)
  • Git や Subversion などの SCM からファイルをチェックアウトする

であり,rsync はない(と思う)。

なので,Capistranorsync しようと思った(あ,でも 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 で定義された)プロパティのように値をひきだすことができる。