capistrano3のコードを読んで仕組みを理解する
メモです。
はじめに
capistrano3の概要を知るには以下の記事がとても参考になる。
業務での使用も経て何となく分かったつもりだったが、デフォルトで色々やってくれるのが逆に少し気持ち悪かった。
内部を理解しないことには気持ち悪さは解消されそうにないのでコードを読んだ。
だいぶ理解したものの、すぐに忘れそうなのでメモしておく。
対象
- capistrano3について何となく知っている。使ったことがある
- capistranoの内部でどういう風に処理が走っているかは知らない
- capistranoのバージョンは3.4.0
長くなったので、各項目のまとめだけ読むと手っ取り早いと思う。
1. cap install
入り口としてcap install
が何故実行出来るのか、から。
bin/cap
の中を覗く。
require 'capistrano/all' Capistrano::Application.new.run
capistrano/all
を読み込みCapistrano::Application
を実行しているのみ。
1.1. capistrano/all
require 'capistrano/version' require 'capistrano/version_validator' require 'capistrano/i18n' require 'capistrano/dsl' require 'capistrano/application' require 'capistrano/configuration'
ここは特筆することなさそう。
1.2. capistrano/application
Rake::Application
を継承。bin/cap
にあったCapistrano::Application.new.run
のクラス。
クラスがインスタンス化されるときの処理(initialize)が以下のようになっている。
自前のCapfile
に加えて、lib/Capfile
がRakefileとして読み込まれる。
def initialize super @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile end # allows the `cap install` task to load without a capfile def capfile File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile')) end
なおrun
はRakeに処理を委譲しているだけ。
def run Rake.application = self super end
1.3. lib/Capfile
cap install
した時に作られるCapfile
ではなくライブラリ側のCapfile
。
include Capistrano::DSL require 'capistrano/install'
lib/capistrano/install.rb
の定義は1行だけ。
load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake'))
このあと見て行くsetup
やdeploy
でもほとんど同じで、最終的に.rake
ファイルを読み込むようになっている。
`lib/capistrano/tasks/install.rake` desc 'Install Capistrano, cap install STAGES=staging,production' task :install do # ... end
:install
にcap install
時の処理が書かれている。
つまり色々とファイルを自動生成してくれる。
1.4. cap installのまとめ
bin/cap
がcapistrano/all
を読み込み、Cap用のRakeを実行するcapistrano/all
により基本的なDSLが定義される- 中で読まれる
capistrano/application
によりライブラリ側のCapfile
が読まれる - ライブラリの
Capfile
にはinstall
だけが定義されている- なので
cap install
が実行出来る
- なので
- 中で読まれる
cap install
すると生成されるCapfile
は不要なコメントアウト行を除くと以下の通り。
# Load DSL and set up stages require 'capistrano/setup' # Include default deployment tasks require 'capistrano/deploy' # Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
setup
とdeploy
について読み進めていく。
(lib/capistrano/tasks/*.rake
についてはコメントの通りカスタム定義なので読むところはなし)
2. capistrano/setup
include Capistrano::DSL namespace :load do task :defaults do load 'capistrano/defaults.rb' end end stages.each do |stage| Rake::Task.define_task(stage) do set(:stage, stage.to_sym) invoke 'load:defaults' load deploy_config_path load stage_config_path.join("#{stage}.rb") load "capistrano/#{fetch(:scm)}.rb" I18n.locale = fetch(:locale, :en) configure_backend end end require 'capistrano/dotfile'
処理の流れは何となく読み取れる。
- stage(たぶんproductionとかstagingとか)ごとの設定を読み込んでいる
- defaultsっていうのを読み込んでいる。きっとデフォルト設定だろう
- scmの定義を読み込んでいる
- その他なんか設定している
もう少しコードを読んでみる。
2.1. stage(たぶんproductionとかstagingとか)ごとの設定を読み込んでいる
いくつかプロパティが出てくるが、これらはどこで定義されているか。
stages
前述のcapistrano/dsl
にて読み込まれるlib/capistrano/dsl/stages.rb
に定義されている。
def stages Dir[stage_definitions].map { |f| File.basename(f, '.rb') } end def stage_definitions stage_config_path.join('*.rb') end
stage_config_path
はlib/capistrano/dsl/paths.rb
に定義されている。
def stage_config_path Pathname.new fetch(:stage_config_path, 'config/deploy') end
:stage_config_path
が設定されていればそのパス、設定されていなければconfig/deploy
のパスを取得している。
cap install
するとconfig/deploy/{production,staging}.rb
を作成してくれるので、デフォルトではこれらが取得される。
File.basename
なので結果としては['production', 'staging']
という値がstages
の戻り値になっているはず。
deploy_config_path
上記のstage_config_path
と同じくlib/capistrano/dsl/paths.rb
に定義されている。
def deploy_config_path Pathname.new fetch(:deploy_config_path, 'config/deploy.rb') end
こちらもcap install
で作られるconfig/deploy.rb
がデフォルトである。
なおsetup
のコードを再び見ると、loadの順番は次のようになっている。
load deploy_config_path load stage_config_path.join("#{stage}.rb")
config/deploy.rb
を読み込んでからconfig/deploy/*.rb
を読み込む。
なので両者で同じ定義をしているとconfig/deploy/*.rb
が勝ちそうな気がする。試していない。
2.2. defaultsっていうのを読み込んでいる
その名の通り変数のデフォルト値を定義している。
set_if_empty :scm, :git set_if_empty :branch, :master set_if_empty :deploy_to, -> { "/var/www/#{fetch(:application)}" } set_if_empty :tmp_dir, "/tmp" set_if_empty :default_env, {} set_if_empty :keep_releases, 5 set_if_empty :format, :pretty set_if_empty :log_level, :debug set_if_empty :pty, false set_if_empty :local_user, -> { Etc.getlogin }
多くの変数については、自動生成されるdeploy.rb
にコメントアウトされた内容として書かれている。
# Default value for :scm is :git # set :scm, :git ...
2.3. scmの定義を読み込んでいる
前述の通り:scm
はdeploy.rb
などで定義するがデフォルトはgitである。
lib/capistrano/git.rb
が読み込まれ、このあとのdeploy
で必要なコマンドが定義される。
あとでまた詳しく見るとする。
2.4. その他なんか設定している
configure_backendについて。
def configure_backend backend.configure do |sshkit| sshkit.format = fetch(:format) sshkit.output_verbosity = fetch(:log_level) sshkit.default_env = fetch(:default_env) sshkit.backend = fetch(:sshkit_backend, SSHKit::Backend::Netssh) sshkit.backend.configure do |backend| backend.pty = fetch(:pty) backend.connection_timeout = fetch(:connection_timeout) backend.ssh_options = (backend.ssh_options || {}).merge(fetch(:ssh_options,{})) end end end
sshkitの設定ぽい。処理の全体像とは関係が低そうなのでこれ以上は追わない。
2.5. capistrano/setupのまとめ
- SCMやデプロイ先などアプリケーション固有の変数を初期化
config/deploy.rb
の読み込みconfig/deploy/*.rb
の読み込み- SCM固有のコマンド定義を読み込み
- SSH設定を読み込み
3. capistrano/deploy
require 'capistrano/framework' load File.expand_path("../tasks/deploy.rake", __FILE__)
capistrano/framework
については以下のようになっている。
load File.expand_path("../tasks/framework.rake", __FILE__) require 'capistrano/install'
ということで、framework.rake
とdeploy.rake
を読み進める必要がある。
3.1. framework.rake
冒頭の参考記事(2つ目)にもある通り、このファイルはデプロイフローの雛形を定義している。
namespace :deploy do desc 'Start a deployment, make sure server(s) ready.' task :starting do end # ... desc 'Rollback to previous release.' task :rollback do %w{ starting started reverting reverted publishing published finishing_rollback finished }.each do |task| invoke "deploy:#{task}" end end end desc 'Deploy a new release.' task :deploy do set(:deploying, true) %w{ starting started updating updated publishing published finishing finished }.each do |task| invoke "deploy:#{task}" end end task default: :deploy
deploy
/rollback
は共に必要なコマンドを実行するだけ- このあとで詳しく見る
deploy
/rollback
以外のコマンドはデフォルトでは何もしない- 具体的な処理は
deploy.rake
で定義される
- 具体的な処理は
namespace :deploy
のデフォルトは:deploy
- なので
cap deploy
はdeploy
が呼ばれる
- なので
テンプレートメソッドっぽい。
3.2. deploy.rake
:starting
とか:updating
とか、framework.rake
のいくつかのコマンドが再定義されている。
3.2.1. deploy.rake -- deploy
フェーズ毎に簡単に処理を見てみる。
deploy:starting
該当部分だけ抜き出す。
task :starting do invoke 'deploy:check' invoke 'deploy:set_previous_revision' end desc 'Check required files and directories exist' task :check do invoke "#{scm}:check" invoke 'deploy:check:directories' invoke 'deploy:check:linked_dirs' invoke 'deploy:check:make_linked_dirs' invoke 'deploy:check:linked_files' end task :set_previous_revision do on release_roles(:all) do target = release_path.join('REVISION') if test "[ -f #{target} ]" set(:previous_revision, capture(:cat, target, '2>/dev/null')) end end end
- check系のタスクではファイルやディレクトリのチェックや準備を行う
deploy:check:directories
などのタスクは同一ファイルに定義されているexecute :mkdir, '-p', shared_path, releases_path
などをしている
:set_previous_revision
ではリリース前の最新リビジョンを変数に保存しておく- capを使ってデプロイするとデプロイ先にリビジョンが書かれた
REVISION
というファイルが作られる - バージョンによってはないらしい。参考: デプロイしたリビジョンをファイルに書き出す
- capを使ってデプロイするとデプロイ先にリビジョンが書かれた
deploy:started
デフォルトのまま。何もしない。
deploy:updating
:updating
には依存があり、先に:new_release_path
が行われる。
まずは:new_release_path
から読む。
task :new_release_path do set_release_path end
set_release_path
はlib/capistrano/dsl/paths.rb
に定義されている。
def releases_path deploy_path.join('releases') end def set_release_path(timestamp=now) set(:release_timestamp, timestamp) set(:release_path, releases_path.join(timestamp)) end
- リリース先のパスを変数に設定している
releases/20150080072500
みたいなパス- デプロイの構造は公式のStructureを参照
次にupdating
の処理の中身を読む。
task :updating => :new_release_path do invoke "#{scm}:create_release" invoke "deploy:set_current_revision" invoke 'deploy:symlink:shared' end namespace :symlink do # ... desc 'Symlink files and directories from shared to release' task :shared do invoke 'deploy:symlink:linked_files' invoke 'deploy:symlink:linked_dirs' end # ... end desc "Place a REVISION file with the current revision SHA in the current release path" task :set_current_revision do invoke "#{scm}:set_current_revision" on release_roles(:all) do within release_path do execute :echo, "\"#{fetch(:current_revision)}\" >> REVISION" end end end
:set_current_revision
は前述のREVISION
にリビジョンを書き込んでいるだけ:symlink:shared
はsymlinkの必要があるディレクトリやファイルがあれば作っている- 上記のリンク(Structure)の
shared
を参照
- 上記のリンク(Structure)の
:create_release
については少し詳しく読む。
コードを全部載せると長いので概要だけ。
# lib/capistrano/tasks/git.rake desc 'Upload the git wrapper script, this script guarantees that we can script git without getting an interactive prompt' task :wrapper do # ... end desc 'Clone the repo to the cache' task clone: :'git:wrapper' do # ... end desc 'Update the repo mirror to reflect the origin state' task update: :'git:clone' do # ... end desc 'Copy repo to releases' task create_release: :'git:update' do # ... end
- 依存関係を考慮した実行順は wrapper -> clone -> update -> create_release
- wrapper
- 専用のgit-ssh.shを配置する
- clone
- cloneされていなければcloneする
git :clone, '--mirror', repo_url, repo_path
- clone済みなら何もしない
- デプロイ先にrepo/HEADがあるかどうか
- 上記のリンク(Structure)の
repo
を参照
- 上記のリンク(Structure)の
- デプロイ先にrepo/HEADがあるかどうか
- cloneされていなければcloneする
- update
- repoの中で実行される
git :remote, :update
- create_release
- repoの中で実行される
- :repo_tree がある場合
git :archive, fetch(:branch), tree, "| tar -x --strip-components #{components} -f - -C", release_path
- ない場合
git :archive, fetch(:branch), '| tar -x -f - -C', release_path
deploy:updated
デフォルトのまま。何もしない。
deploy:publishing
task :publishing do invoke 'deploy:symlink:release' end namespace :symlink do desc 'Symlink release to current' task :release do on release_roles :all do tmp_current_path = release_path.parent.join(current_path.basename) execute :ln, '-s', release_path, tmp_current_path execute :mv, tmp_current_path, current_path.parent end end end
current -> /var/www/my_app_name/releases/20150120114500/
みたいなsymlinkを作っているだけ。
デプロイ先の構造については上記のリンク(Structure)を参照。
deploy:published
デフォルトのまま。何もしない。
deploy:finishing
task :finishing do invoke 'deploy:cleanup' end desc 'Clean up old releases' task :cleanup do on release_roles :all do |host| releases = capture(:ls, '-xtr', releases_path).split if releases.count >= fetch(:keep_releases) info t(:keeping_releases, host: host.to_s, keep_releases: fetch(:keep_releases), releases: releases.count) directories = (releases - releases.last(fetch(:keep_releases))) if directories.any? directories_str = directories.map do |release| releases_path.join(release) end.join(" ") execute :rm, '-rf', directories_str else info t(:no_old_releases, host: host.to_s, keep_releases: fetch(:keep_releases)) end end end end
descの通り:keep_releases
を超えた古いリリースファイルを消す作業。
capture(:ls, '-xtr', releases_path).split
は、こんなやり方があるのかとちょっと驚いた。
ls -xtr <pat>
とすることで「水平(x)」に「日付(t)」の「降順(r)」で出力させて、splitすることでそのまま配列として扱っている。
あとt(:keeping_releases ...)
のt
って何だと思ったが、i18nのようだった。
# lib/capistrano/dsl.rb def t(key, options={}) I18n.t(key, options.merge(scope: :capistrano)) end
deploy:finished
task :finished do invoke 'deploy:log_revision' end desc 'Log details of the deploy' task :log_revision do on release_roles(:all) do within releases_path do execute %{echo "#{revision_log_message}" >> #{revision_log}} end end end
リリース先のrevisions.log
にメッセージを書き込む。
revisions.log
についても上記のリンク(Structure)を参照。
3.2.2. deploy.rake -- rollback
deploy:starting
deploy:started
deployと同じ。
deploy:reverting
task :reverting do invoke 'deploy:revert_release' end desc 'Revert to previous release timestamp' task :revert_release => :rollback_release_path do on release_roles(:all) do set(:revision_log_message, rollback_log_message) end end task :rollback_release_path do on release_roles(:all) do releases = capture(:ls, '-xt', releases_path).split if releases.count < 2 error t(:cannot_rollback) exit 1 end last_release = releases[1] set_release_path(last_release) set(:rollback_timestamp, last_release) end end
- リリースが1つしかないならロールバック不可
- 一つ前のリリースを
:release_path
に設定
deploy:reverted
デフォルトのまま。何もしない。
deploy:publishing
処理的にはdeployと同じ。
ただし前述の通り:release_path
が直前のリリースパスになっているので、current
がそれにsymlinkされる。
deploy:published
deployと同じ。(何もしない)
deploy:finishing_rollback
task :finishing_rollback do invoke 'deploy:cleanup_rollback' end desc 'Remove and archive rolled-back release.' task :cleanup_rollback do on release_roles(:all) do last_release = capture(:ls, '-xt', releases_path).split.first last_release_path = releases_path.join(last_release) if test "[ `readlink #{current_path}` != #{last_release_path} ]" execute :tar, '-czf', deploy_path.join("rolled-back-release-#{last_release}.tar.gz"), last_release_path execute :rm, '-rf', last_release_path else debug 'Last release is the current release, skip cleanup_rollback.' end end end
- ロールバックしたリリースはいらないよね、っていう作業
rolled-back-release-#{last_release}.tar.gz
という感じのファイル名でアーカイブ- デプロイ先(currentとかと同じ階層)に置かれる
releases
にあるリリースを消す
deploy:finished
deployと同じ。
3.3. capistrano/deploy のまとめ
capistrano/framework
はフローの雛形を提供している- deploy
- デプロイ先のディレクトリなどを準備する
- 初回のみ
git clone
する - 毎回
git remote update
が行われる git archive <branch>
したものがreleases/<timestamp>
の中に置かれる- 上記リリース物と
current
をsymlinkでくっつける releases
が:keep_releases
を超えないように調整する
- rollback
- 直前のリリース物を
current
にする - 直前のリリース物をアーカイブする
- 直前のリリース物を
releases
から削除する
- 直前のリリース物を
framework
とdeploy
のおかげで利用者としては基本的にはフックを定義するだけで良い。ということをちゃんと理解出来た。
基本処理は分かったとして、一応before
/after
のフックについても仕組みを見ておきたい。
4. before/after
capistrano/dsl.rb
で読み込まれるlib/capistrano/dsl/task_enhancements.rb
にて定義されている。
def before(task, prerequisite, *args, &block) prerequisite = Rake::Task.define_task(prerequisite, *args, &block) if block_given? Rake::Task[task].enhance [prerequisite] end def after(task, post_task, *args, &block) Rake::Task.define_task(post_task, *args, &block) if block_given? post_task = Rake::Task[post_task] Rake::Task[task].enhance do post_task.invoke end end
capistranoというよりはRakeの構文の模様。
Rake で任意のタスクの前後に別のタスクを実行する
4.1. before/after のまとめ
- Rakeに委譲しているだけ
おわりに
整理と備忘のために書いたけど、忘れてもまたドキュメントとコードを読んだ方が分かりやすいような気がしてきた。