Capistrano3でRailsを動かすコードを読んでみる

## はじめに
Railsをデプロイするために使っている`各Gem`ファイルが何をしているかを知りたい人向けの記事です。

というか、僕が知りたいために書いてます。
なお、[前回の記事](http://qiita.com/syou007/items/9b8a2f4d1ff099c64fc5)が前提条件となってますので本記事を読む前に一読すると理解が深まります。

## とりあえず使いた人向け
特に何も考えずにデプロイだけ行いたいという人は以下のGemを入れて実行してください。
デプロイするフォルダになりますので、適当にフォルダを掘ってから行うと良いです。

```rb:Gemfile
source 'https://rubygems.org'

gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano-rbenv' # rbenvを使ってない場合は不要
gem 'capistrano-bundler'
gem 'capistrano-sidekiq'	# sidekiqで遅延処理管理をしている場合
gem 'whenever'	# wheneverでスケジューリングしている場合
gem 'unicorn'		# unicornで動作させている場合（入れなくてもできる。）
```

必要なものを定義して

```sh
$ bundle install
```

を実行してください。

### Capistranoのインストール

Capistranoを設定します。

```sh
$ bundle exec cap install
```

出来上がったフォルダのCapfileを編集します。

```rb:Capfile
# Capistranoの基本動作を設定。
require 'capistrano/setup'
require 'capistrano/deploy'

# rvm/rbenv/chrubyを使用する場合に合わせてコメントアウトすること。
# require 'capistrano/rvm'
# require 'capistrano/rbenv'
# require 'capistrano/chruby'

# Railsのレシピを読み込みます。
require 'capistrano/rails'
# capistrano/rails内には以下が定義されています。
# require 'capistrano/bundler'
# require 'capistrano/rails/assets'
# require 'capistrano/rails/migrations'

# 必要に応じてコメントを外してください。
# require 'capistrano/passenger'
# require 'capistrano/sidekiq'
# require 'whenever/capistrano'
# require 'sshkit'
# require 'capistrano3/unicorn'
```

基本動作に関しては[こちら](http://qiita.com/syou007/items/9b8a2f4d1ff099c64fc5)をみてください。

### 各種設定を行う
ここまでの設定でCapistranoの設定は半分終わってます。
この辺りがCapistrano3の良いところですね。

あとは`deploy.rb`に必要に応じて設定内容を記載します。
以下を参考にしてください。

```rb:config/deploy.rb
# 基本設定
set :application, 'application_name' # application名はなんでも良い。
set :repo_url, 'git@gitpub.****.git' # デプロイ対象のリポジトリ
set :branch,  'master' # ブランチの指定をしたい場合はここに記載。

# デプロイ先
set :deploy_to, -> { "/var/www/#{fetch(:application)}" } # デプロイする先（default:"/var/www/#{fetch(:application)"）
# set :deploy_to, "~/#{fetch(:application)}/#{fetch(:rails_env)}" # HOMEを指定するとパーミッションに悩まされなくて良いかも。

set :pty, true		# sudoを使用する場合はtrueにする。
set :log_level, :info	# Capistranoの出力ログの制御
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')	# sharedにシンボリックリンクを張るディレクトリ指定
# set :linked_dirs, %w{log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system} # ここでしか使わないならこれでもOK。
set :linked_files, %w{.env}	# シンボリックリンクを張るファイル
set :keep_releases, 5	# リリースフォルダをいくつまで保持するか？
set :default_env, { path: "/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH" } # capistrano用bundleするのに必要

# bundleインストール先（以下全てデフォルト。記載なしの場合）
set :bundle_bins, %w{gem rake rails}

set :bundle_roles, :all
set :bundle_servers, -> { release_roles(fetch(:bundle_roles)) }
set :bundle_path, -> { shared_path.join('bundle') } # この値以外は多分操作することはない。
set :bundle_without, %w{development test}.join(' ')
set :bundle_flags, '--deployment --quiet'

# migrateの設定
set :migration_role, :db	# どのロールで実施するか？（default:db）
set :conditionally_migrate, false		# 不要の場合は実施しないか？（default:false）

# rbenvの設定
set :rbenv_type, :system 	# rbenv_custom_pathを指定していれば不要
set :rbenv_ruby, '2.2.3'	# rubyのバージョン（事前に指定バージョンをインストールしておく必要あり。）
set :rbenv_custom_path, '/opt/rbenv'	# rbenvのインストール先
set :rbenv_map_bins, %w{rake gem bundle ruby rails} # rbenv execをつけたいコマンド

# SSH接続時の情報
set :ssh_options, {
  keys: [File.expand_path('~/.ssh/id_rsa')],	# 鍵の格納場所
  forward_agent: true,	# サーバーを跨いで鍵を使いたい場合
  auth_methods: %w(publickey) # 認証方法(passwordも可能)
  #password: 'xxxxx' # password指定
}

# rakeコマンドを実行する際のコマンド指定
SSHKit.config.command_map[:rake] = 'bundle exec rake'
```

これらの設定を行えば、おしまいです。
なお、[こちら](http://qiita.com/syou007/items/9b8a2f4d1ff099c64fc5#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%AB%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%9F%E3%81%84)のレシピを追加すると`db:create`や`サーバー再起動`なども行えます。(後述)

あとは、各サーバー毎に変更する値を記載します。

```rb:config/deploy/***.rb
# rails_envの設定。大抵はrails_envのみ設定すれば問題ない。
set :stage,          :staging
set :rails_env,      fetch(:stage)

# デプロイ対象のサーバーを指定。
server '172.1.1.1', user: 'app', roles: %w{web app db batch}
server '172.1.1.2', user: 'app', roles: %w{web app}
...
```

あとは以下のコマンドを実行して完了です。

```sh
cap *** deploy
```

## Railsのレシピを深読みする
これだけで使用できますが、実際に`require`すると何が起きているのかを見ていきます。

### `'capistrano/bundler'`
この`require`は簡単な動作をしています。
コメント部分を取り除いたコードは以下のようになってます。

```rb
namespace :bundler do
  task :install do
    on fetch(:bundle_servers) do
      within release_path do
        with fetch(:bundle_env_variables, {}) do
          options = ["install"]
          options << "--binstubs #{fetch(:bundle_binstubs)}" if fetch(:bundle_binstubs)
          options << "--gemfile #{fetch(:bundle_gemfile)}" if fetch(:bundle_gemfile)
          options << "--path #{fetch(:bundle_path)}" if fetch(:bundle_path)
          options << "--without #{fetch(:bundle_without)}" if fetch(:bundle_without)
          options << "--jobs #{fetch(:bundle_jobs)}" if fetch(:bundle_jobs)
          options << "#{fetch(:bundle_flags)}" if fetch(:bundle_flags)

          execute :bundle, options
        end
      end
    end
  end

  task :map_bins do
    fetch(:bundle_bins).each do |command|
      SSHKit.config.command_map.prefix[command.to_sym].push("bundle exec")
    end
  end

  before 'deploy:updated', 'bundler:install'
end

Capistrano::DSL.stages.each do |stage|
  after stage, 'bundler:map_bins'
end

namespace :load do
  task :defaults do
    set :bundle_bins, %w{gem rake rails}

    set :bundle_roles, :all
    set :bundle_servers, -> { release_roles(fetch(:bundle_roles)) }
    set :bundle_path, -> { shared_path.join('bundle') }
    set :bundle_without, %w{development test}.join(' ')
    set :bundle_flags, '--deployment --quiet'
  end
end
```

ポイントとなる箇所は`before 'deploy:updated', 'bundler:install'`になってます。
この定義によって`deploy:updated`の前すなわち`deploy:updating`の後に実行されるタスクが定義されます。
[こちら](http://qiita.com/syou007/items/9b8a2f4d1ff099c64fc5#require-capistranodeploy)に定義している通りで、`deploy:updating`はGitからファイルを配置した後になります。

その時に`bundle install`を行っていることがわかります。
また、コマンド実行時に指定した引数を渡していることがわかります。

これによって、新しいサーバーに必要な`Gem`がインストールされます。

`map_bins`も注目の箇所です。
簡単に書くと`bundle exec`を付けて実行するコマンドを`bundle_bins`で定義してます。

### `'capistrano/rails/assets'`
この`require`は`CoffeeScript`や`SASS`をコンパイルするタスクになります。

```rb
after 'deploy:updated', 'deploy:compile_assets'
after 'deploy:updated', 'deploy:normalize_assets'
after 'deploy:reverted', 'deploy:rollback_assets'
```

追っていくと以下のことをしていることがわかります。

#### デプロイ時
デプロイ時は以下の順番に実施されます。

```rb
# deploy:compile_assets
execute :rake, "assets:precompile"

backup_path = release_path.join('assets_manifest_backup')
execute :mkdir, '-p', backup_path
execute :cp,
	detect_manifest_path,
	backup_path
	
# deploy:normalize_assets
assets = fetch(:normalize_asset_timestamps)
if assets
	execute :find, "#{assets} -exec touch -t #{asset_timestamp} {} ';'; true"
end
```

見ると`assets_manifest_backup`にバックアップを取っていることがわかります。

#### ロールバック時
`assets_manifest_backup`から前の状態を復元します。
復元が無理の場合は再度コンパイルします。

```rb
begin
  invoke 'deploy:assets:restore_manifest'
rescue Capistrano::FileNotFound
  invoke 'deploy:compile_assets'
end
```

#### 設定項目
以下の項目が設定内容として使用できます。

```rb
set :assets_roles, fetch(:assets_roles, [:web])
set :assets_prefix, fetch(:assets_prefix, 'assets')
set :linked_dirs, fetch(:linked_dirs, []).push("public/#{fetch(:assets_prefix)}")
```

### `'capistrano/rails/migrations'`
このタスクも`bundle`と同じくらい短いのでコードを載せます。

```rb
namespace :deploy do

  desc 'Runs rake db:migrate if migrations are set'
  task :migrate => [:set_rails_env] do
    on primary fetch(:migration_role) do
      conditionally_migrate = fetch(:conditionally_migrate)
      info '[deploy:migrate] Checking changes in /db/migrate' if conditionally_migrate
      if conditionally_migrate && test("diff -q #{release_path}/db/migrate #{current_path}/db/migrate")
        info '[deploy:migrate] Skip `deploy:migrate` (nothing changed in db/migrate)'
      else
        info '[deploy:migrate] Run `rake db:migrate`'
        within release_path do
          with rails_env: fetch(:rails_env) do
            execute :rake, "db:migrate"
          end
        end
      end
    end
  end

  after 'deploy:updated', 'deploy:migrate'
end

namespace :load do
  task :defaults do
    set :conditionally_migrate, fetch(:conditionally_migrate, false)
    set :migration_role, fetch(:migration_role, :db)
  end
end
```

`bundle`同様で`after 'deploy:updated', 'deploy:migrate'`のタイミングに実施されます。

`on primary`で実施されいる点が味噌になります。
なので、`db`ロールを誤って二つ設定しても二重に実行されることはありません。

`conditionally_migrate`は面白い項目です。
僕は知らなかったのですが、この値を`true`にしておけば毎回`db:migrate`が実施される事がなくなります。
コードを見ると今回のリリースファイルと現在稼働しているカレントファイルの差分を取って変更があるかをチェックして実行可否を決めてます。

#### 設定内容
設定内容は以下です。特に変えることは無いと思います。

```rb
set :conditionally_migrate, fetch(:conditionally_migrate, false)
set :migration_role, fetch(:migration_role, :db)
```

### `'capistrano/rbenv'`
今までどうやって`rbenv`で`ruby`のバージョンを分けていたかがこのコードでわかりました。

```rb
namespace :rbenv do
  task :validate do
    on release_roles(fetch(:rbenv_roles)) do
      rbenv_ruby = fetch(:rbenv_ruby)
      if rbenv_ruby.nil?
        error "rbenv: rbenv_ruby is not set"
        exit 1
      end

      unless test "[ -d #{fetch(:rbenv_ruby_dir)} ]"
        error "rbenv: #{rbenv_ruby} is not installed or not found in #{fetch(:rbenv_ruby_dir)}"
        exit 1
      end
    end
  end

  task :map_bins do
    SSHKit.config.default_env.merge!({ rbenv_root: fetch(:rbenv_path), rbenv_version: fetch(:rbenv_ruby) })
    rbenv_prefix = fetch(:rbenv_prefix, proc { "#{fetch(:rbenv_path)}/bin/rbenv exec" })
    SSHKit.config.command_map[:rbenv] = "#{fetch(:rbenv_path)}/bin/rbenv"

    fetch(:rbenv_map_bins).each do |command|
      SSHKit.config.command_map.prefix[command.to_sym].unshift(rbenv_prefix)
    end
  end
end

Capistrano::DSL.stages.each do |stage|
  after stage, 'rbenv:validate'
  after stage, 'rbenv:map_bins'
end

namespace :load do
  task :defaults do
    set :rbenv_path, -> {
      rbenv_path = fetch(:rbenv_custom_path)
      rbenv_path ||= if fetch(:rbenv_type, :user) == :system
        "/usr/local/rbenv"
      else
        "~/.rbenv"
      end
    }

    set :rbenv_roles, fetch(:rbenv_roles, :all)

    set :rbenv_ruby_dir, -> { "#{fetch(:rbenv_path)}/versions/#{fetch(:rbenv_ruby)}" }
    set :rbenv_map_bins, %w{rake gem bundle ruby rails}
  end
end
```

`rbenv_map_bins`に指定されているコマンドが実行される場合に`rbenv_path`と`rbenv_ruby`によって導きだされた`ruby`のフォルダを引き渡すように指定してます。

`SSHKit.config.command_map`の便利さを知るコードですね。

### `'capistrano3/unicorn'`
使ってないのでコードが手元に無いのですが、[こちら](https://github.com/tablexi/capistrano3-unicorn/blob/master/lib/capistrano3/tasks/unicorn.rake)を見ると`unicorn`のタスクが書かれている事がわかります。

僕もそうですが、この辺のタスクを自前で登録していた人はこの`Gem`を使用すると楽になると思います。
なおUnicornに関しては[こちら](http://qiita.com/syou007/items/555062cc96dd0b08a610)に記載してますのでよければ見てください。

この`gem`を使えば`invoke 'unicorn:restart'`でUnicornの再起動ができます。

自前で行う場合は以下のファイルを配置すれば実施できます。
（結構あちらこちらに落ちているコードです。）

```rb:lib/capistrano/tasks/unicorn.rake
namespace :load do
  task :defaults do
    set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }
    set :unicorn_config, -> { "#{current_path}/config/unicorn.rb" }
  end
end

namespace :unicorn do
  task :environment do
  end

  def start_unicorn
    within current_path do
      execute :bundle, :exec, "unicorn_rails -c #{fetch(:unicorn_config)} -E #{fetch(:rails_env)} -D"
    end
  end

  def stop_unicorn
    execute :kill, "-s QUIT $(< #{fetch(:unicorn_pid)})"
  end

  def reload_unicorn
    execute :kill, "-s USR2 $(< #{fetch(:unicorn_pid)})"
  end

  def force_stop_unicorn
    execute :kill, "$(< #{fetch(:unicorn_pid)})"
  end

  desc "Start unicorn server"
  task :start => :environment do
    on roles(:app) do
      start_unicorn
    end
  end

  desc "Stop unicorn server gracefully"
  task :stop => :environment do
    on roles(:app) do
      stop_unicorn
    end
  end

  desc "Restart unicorn server gracefully"
  task :restart => :environment do
    on roles(:app) do
      if test("[ -f #{fetch(:unicorn_pid)} ]")
        reload_unicorn
      else
        start_unicorn
      end
    end
  end

  desc "Stop unicorn server immediately"
  task :force_stop => :environment do
    on roles(:app) do
      force_stop_unicorn
    end
  end

  desc "Restart unicorn server immediately"
  task :force_restart => :environment do
    on roles(:app) do
      stop_unicorn
      start_unicorn
    end
  end
end
```

### 自前タスク
自前タスクを`deploy`の中に書いていっても良いのですが、上記のようにタスク毎にまとめるとスマートに管理ができます。

`Capfile`の一番最後に以下のコードが記載されてます。

```rb
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

# 古いバージョンでは以下のように記載されてます。
Dir.glob('lib/capistrano/tasks/*.cap').each { |r| import r }
```

なので`lib/capistrano/tasks/*.rake`に上記と同じ形式のファイルを定義すれば自動的に読み込まれます。
また、呼び出すときには`invoke 'unicorn:restart'`のように呼び出せます。

## まとめ
他にも`whenever`や`sidekiq`、`passenger`などのレシピがありますので必要に応じて読めばタスクの内容が大体わかりました。
一部を除けばそこまで難しい事はかかれてい無いので、必要に応じてタスクを読む程度で`require`を使いこなせる事がわかりました。

あと、僕があまり把握していなかったので`require 'sshkit'`については言及しませんでしたが`SSH`に関する事をやりたい場合はこのような事もできるみたいです。