LoginSignup
30
37

More than 5 years have passed since last update.

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

Posted at

はじめに

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

というか、僕が知りたいために書いてます。
なお、前回の記事が前提条件となってますので本記事を読む前に一読すると理解が深まります。

とりあえず使いた人向け

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

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で動作させている場合(入れなくてもできる。)

必要なものを定義して

$ bundle install

を実行してください。

Capistranoのインストール

Capistranoを設定します。

$ bundle exec cap install

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

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'

基本動作に関してはこちらをみてください。

各種設定を行う

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

あとはdeploy.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'

これらの設定を行えば、おしまいです。
なお、こちらのレシピを追加するとdb:createサーバー再起動なども行えます。(後述)

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

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}
...

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

cap *** deploy

Railsのレシピを深読みする

これだけで使用できますが、実際にrequireすると何が起きているのかを見ていきます。

'capistrano/bundler'

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

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の後に実行されるタスクが定義されます。
こちらに定義している通りで、deploy:updatingはGitからファイルを配置した後になります。

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

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

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

'capistrano/rails/assets'

このrequireCoffeeScriptSASSをコンパイルするタスクになります。

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

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

デプロイ時

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

# 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から前の状態を復元します。
復元が無理の場合は再度コンパイルします。

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

設定項目

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

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と同じくらい短いのでコードを載せます。

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が実施される事がなくなります。
コードを見ると今回のリリースファイルと現在稼働しているカレントファイルの差分を取って変更があるかをチェックして実行可否を決めてます。

設定内容

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

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

'capistrano/rbenv'

今までどうやってrbenvrubyのバージョンを分けていたかがこのコードでわかりました。

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_pathrbenv_rubyによって導きだされたrubyのフォルダを引き渡すように指定してます。

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

'capistrano3/unicorn'

使ってないのでコードが手元に無いのですが、こちらを見るとunicornのタスクが書かれている事がわかります。

僕もそうですが、この辺のタスクを自前で登録していた人はこのGemを使用すると楽になると思います。
なおUnicornに関してはこちらに記載してますのでよければ見てください。

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

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

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の一番最後に以下のコードが記載されてます。

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'のように呼び出せます。

まとめ

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

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

30
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
37