この記事は最終更新から1年以上経過しています。 気をつけてね。
Ruby製のデプロイツールCapistranoは何かと便利でよくつかう。Railsにかぎらずコードベースから何かを持ってくる用途に向いている。
capコマンドから実行するのもいいが、適当なSinatraアプリに組み込んだりできると良いかもしれないと思い、感触をつかむためirbなんかでインタラクティブに叩いてみた。
単純にCapistranoのWEB-UIが必要ならrailsで作られたWebistranoというのがある。元は古いけど幾つかのフォークされたものは新しめの環境でも動作するようだ。
CapistranoソースのチェックにはメタプログラミングRubyをかなり参考にした。
まずは Capistrano::Configuration.new
今回はRubyの実行にPryを使う。
どうやらCapistranoのすべてはCapistrano::Configuration
クラスのオブジェクトに集まる模様、インスタンスを作成する。
> require 'capistrano'
=> true
> config = Capistrano::Configuration.new
=> #<Capistrano::Configuration:0x00000001f26678
@callbacks={},
@debug=false,
@dry_run=false,
@load_paths=
[".",
"/usr/local/rvm/gems/ruby-1.9.3-p286@cap/gems/capistrano-2.13.5/lib/capistrano/recipes"],
@loaded_features=[],
@logger=
#<Capistrano::Logger:0x00000001f264c0
@device=#<IO:<STDERR>>,
@disable_formatters=nil,
@level=0,
@options={}>,
@name=nil,
@namespaces={},
@original_procs={},
@parent=nil,
@preserve_roles=false,
@roles={},
@tasks={},
@variable_locks=
{:ssh_options=>%<8d5c2f16Mutex:0x00000001f26150>,
:logger=>#<Mutex:0x00000001f25fc0>,
:default_environment=>#<Mutex:0x00000001f25368>,
:default_run_options=>#<Mutex:0x00000001f251b0>},
@variables=
{:ssh_options=>{},
:logger=>
#<Capistrano::Logger:0x00000001f264c0
@device=#<IO:<STDERR>>,
@disable_formatters=nil,
@level=0,
@options={}>,
:default_environment=>{},
:default_run_options=>{}}>
CapistranoのCapfile, deploy.rb
などに出てくる単語がインスタンス変数としてたくさん出てくる。
継承チェック
各モジュールやクラスから、CLIツールでデプロイする時のCapitranoの各挙動が垣間見れる。
> config.class.ancestors
=> [Capistrano::Configuration,
Capistrano::Configuration::Callbacks,
Capistrano::Configuration::Actions::FileTransfer,
Capistrano::Configuration::Actions::Inspect,
Capistrano::Configuration::Actions::Invocation,
Capistrano::Configuration::AliasTask,
Capistrano::Configuration::Connections,
Capistrano::Configuration::Execution,
Capistrano::Configuration::Loading,
Capistrano::Configuration::LogFormatters,
Capistrano::Configuration::Namespaces,
Capistrano::Configuration::Roles,
Capistrano::Configuration::Servers,
Capistrano::Configuration::Variables,
Object,
PP::ObjectMixin,
Kernel,
BasicObject]
出力を詳細に変更
ここで@logger
をTRACEレベルにして、Capistranoが実行する内容を逐一表示するように設定。
> Capistrano::Logger.constants
=> [:IMPORTANT, :INFO, :DEBUG, :TRACE, :MAX_LEVEL, :COLORS, :STYLES]
> config.logger.level = Capistrano::Logger::TRACE
=> 3
Namespaceとタスクを定義
この段階ではタスクは何もない。
confingについた#namespace
はタスクを動的に定義する、これはどういうことか。
> config.namespace :hello do
* task :world do
* run "echo Hello World ?"
* end
* end
=> #<Proc:0x000000022dadf0@/usr/local/rvm/gems/ruby-1.9.3-p286@cap/gems/capistrano-2.13.5/lib/capistrano/configuration/namespaces.rb:83 (lambda)>
Procだった、どこかでcallする必要がある。
タスクProcの格納先
configオブジェクトをたどっていく。
> config.namespaces.keys
=> [:hello]
> config.namespaces[:hello].tasks.keys
=> [:world]
> config.namespaces[:hello].tasks[:world].body
=> #<Proc:0x000000022db110@(pry):9>
あった、#call
を叩いてみよう。中身のrun
が呼ばれる。
> config.namespaces[:hello].tasks[:world].body.call
* executing "echo Hello World ?"
Capistrano::NoMatchingServersError: no servers found to match {:eof=>true}
from /usr/local/rvm/gems/ruby-1.9.3-p286@cap/gems/capistrano-2.13.5/lib/capistrano/configuration/connections.rb:174:in `execute_on_servers'
run
は対象のサーバを必要とするのでサーバのリストを取れるようにする。
Role(server)の追加
namespaceの方法から推測できるが、これもレシピ通りに定義出来る。config.
を除けばCapistrano使いにはおなじみの内容。
こういう内部DSLの実装はメタプログラミングRubyをみたら何となくわかる。
> config.server "localhost", :web
=> [:web]
空だった@roles
にサーバリストが代入された。
> config.roles
=> {:web=>
#<Capistrano::Role:0x00000002427870
@dynamic_servers=[],
@static_servers=[localhost]>}
タスクの実行を2通りの書式で
まず先ほどコケた#call
。
> config.namespaces[:hello].tasks[:world].body.call
* executing "echo Hello World ?"
servers: ["localhost"]
[localhost] executing command
** [out :: localhost] Hello World ?
command finished in 171ms
=> nil
これは次の書式でも一緒になる、要はCapistranoレシピと一緒だ。
> config.hello.world
* 2012-11-07 09:12:24 executing `hello:world'
* executing "echo Hello World ?"
servers: ["localhost"]
[localhost] executing command
** [out :: localhost] Hello World ?
command finished in 7ms
=> nil
namespaceによってメソッドとして定義されてるのでconfigから呼べる。
> config.methods(false)
=> [:hello,
:_cset,
:scm_default,
:depend,
:with_env,
:run_locally,
:try_sudo,
:try_runner]
これでひとまずRoleへのタスク実行が出来た。
いつもの cap deploy をloadしてみる
@load_paths
には組込みレシピへのパスがある。
> config.load_paths
=> [".", "/usr/local/rvm/gems/ruby-1.9.3-p286@cap/gems/capistrano-2.13.5/lib/capistrano/recipes"]
#load
を使うと組み込める、これでいつものdeploy
系タスクが定義される。
> config.load "deploy"
=> ["deploy"]
> config.namespaces.keys
=> [:hello, :deploy]
CLI実行を補足するとcapifyで作成したCapfileにあるload "deploy"
でこれと同じことを行なっている事を確認できる。
組み込みのDeployを実行してみる
ロードしたdeploy
レシピのタスク一覧を確認すると、おなじみの面々が並んでいることがわかる。
> config.namespaces[:deploy].tasks.keys
=> [:default,
:setup,
:update,
:update_code,
:finalize_update,
:symlink,
:create_symlink,
:upload,
:restart,
:migrate,
:migrations,
:cleanup,
:check,
:cold,
:start,
:stop]
軽そうなdeploy:setupから
もちろん#set
が使えるので、setupに必須のパラメータ:deploy_to
を定義したらsetup
実行。
> config.set :deploy_to, "/opt/deploy/hello"
=> "/opt/deploy/hello"
> config.deploy.setup
* 2012-11-07 09:34:17 executing `deploy:setup'
* executing "sudo -p 'sudo password: ' mkdir -p /opt/deploy/hello /opt/deploy/hello/releases /opt/deploy/hello/shared /opt/deploy/hello/shared/system /opt/deploy/hello/shared/log /opt/deploy/hello/shared/pids"
servers: ["localhost"]
[localhost] executing command
command finished in 14ms
* executing "sudo -p 'sudo password: ' chmod g+w /opt/deploy/hello /opt/deploy/hello/releases /opt/deploy/hello/shared /opt/deploy/hello/shared/system /opt/deploy/hello/shared/log /opt/deploy/hello/shared/pids"
servers: ["localhost"]
[localhost] executing command
command finished in 11ms
=> nil
> Dir.glob("/opt/deploy/hello/**/")
=> ["/opt/deploy/hello/",
"/opt/deploy/hello/shared/",
"/opt/deploy/hello/shared/system/",
"/opt/deploy/hello/shared/log/",
"/opt/deploy/hello/shared/pids/",
"/opt/deploy/hello/releases/"]
Capistranoぽいディレクトリツリーが出来た。
コードベースからデプロイする(update_code, create_symlink)
deploy
レシピを組み込んだことにより作成されたAttributeをチェック。
> config.namespaces[:deploy].variables.keys
=> [:ssh_options,
:logger,
:default_environment,
:default_run_options,
:application,
:repository,
:scm,
:deploy_via,
:deploy_to,
:revision,
:rails_env,
:rake,
:source,
:real_revision,
:strategy,
:release_name,
:version_dir,
:shared_dir,
:shared_children,
:current_dir,
:releases_path,
:shared_path,
:current_path,
:release_path,
:releases,
:current_release,
:previous_release,
:current_revision,
:latest_revision,
:previous_revision,
:run_method,
:latest_release]
要るものだけ上書き&定義する、コードベースの情報があればOK。
> config.set :scm, 'git'
=> "git"
> config.set :repository, 'https://github.com/higanworks/Files_and_Tasks.git'
=> "https://github.com/higanworks/Files_and_Tasks.git"
> config.deploy.check
* 2012-11-07 10:27:26 executing `deploy:check'
* executing "test -d /opt/deploy/hello/releases"
servers: ["localhost"]
[localhost] executing command
command finished in 7ms
* executing "test -w /opt/deploy/hello"
servers: ["localhost"]
[localhost] executing command
command finished in 5ms
* executing "test -w /opt/deploy/hello/releases"
servers: ["localhost"]
[localhost] executing command
command finished in 5ms
* executing "which git"
servers: ["localhost"]
[localhost] executing command
command finished in 7ms
You appear to have all necessary dependencies installed
=> nil
update_code
Github
からコードを設置。
> config.deploy.update_code
* 2012-11-07 10:11:19 executing `deploy:update_code'
* executing "git clone -q https://github.com/higanworks/Files_and_Tasks.git /opt/deploy/hello/releases/20121107100343 && cd /opt/deploy/hello/releases/20121107100343 && git checkout -q -b deploy ea5f5e0f9e782305dcd3c27a091fe6c8387760c6 && (echo ea5f5e0f9e782305dcd3c27a091fe6c8387760c6 > /opt/deploy/hello/releases/20121107100343/REVISION)"
servers: ["localhost"]
[localhost] executing command
command finished in 6293ms
* 2012-11-07 10:11:26 executing `deploy:finalize_update'
* executing "chmod -R -- g+w /opt/deploy/hello/releases/20121107100343 && rm -rf -- /opt/deploy/hello/releases/20121107100343/public/system && mkdir -p -- /opt/deploy/hello/releases/20121107100343/public/ && ln -s -- /opt/deploy/hello/shared/system /opt/deploy/hello/releases/20121107100343/public/system && rm -rf -- /opt/deploy/hello/releases/20121107100343/log && ln -s -- /opt/deploy/hello/shared/log /opt/deploy/hello/releases/20121107100343/log && rm -rf -- /opt/deploy/hello/releases/20121107100343/tmp/pids && mkdir -p -- /opt/deploy/hello/releases/20121107100343/tmp/ && ln -s -- /opt/deploy/hello/shared/pids /opt/deploy/hello/releases/20121107100343/tmp/pids"
servers: ["localhost"]
[localhost] executing command
command finished in 22ms
* executing "find /opt/deploy/hello/releases/20121107100343/public/images\\ /opt/deploy/hello/releases/20121107100343/public/stylesheets\\ /opt/deploy/hello/releases/20121107100343/public/javascripts -exec touch -t -- 201211071011.26 {} ';'; true"
servers: ["localhost"]
[localhost] executing command
*** [err :: localhost] find: `/opt/deploy/hello/releases/20121107100343/public/images /opt/deploy/hello/releases/20121107100343/public/stylesheets /opt/deploy/hello/releases/20121107100343/public/javascripts': No such file or directory
command finished in 17ms
=> nil
create_symlink
load "deploy"
で自動的に定義されたattributeは、各タスクで使い回されている。
> config.release_name
=> "20121107100343"
> config.release_path
=> "/opt/deploy/hello/releases/20121107100343"
> config.deploy.create_symlink
* 2012-11-07 10:22:00 executing `deploy:create_symlink'
* executing "rm -f /opt/deploy/hello/current && ln -s /opt/deploy/hello/releases/20121107100343 /opt/deploy/hello/current"
servers: ["localhost"]
[localhost] executing command
command finished in 11ms
=> nil
まとめ
Capistrano::Configuration
をうまいこと操作できれば、Dev向けにシンプルな共有デプロイツールを作れそうな気がする。