Capistrano3 を使って Rails4 + unicorn + nginx + rbenv にデプロイする

  • 74
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

ソースリポジトリ

ソースは以下のリポジトリを参照してください。
https://github.com/katsuhiko/sample_app_rails_4

バージョン

version
rails 4.2.3
capistrano 3.4.0
capistrano-rails 1.1.3
capistrano-rbenv 2.0.3
capistrano3-unicorn 0.2.1
unicorn 4.9.0
unicorn-worker-killer 0.4.3

概要

Ansible を使って EC2 に Railsサーバーを立ち上げる
で作成した EC2 インスタンスに対して sample_app_rails_4 をデプロイします。

インストール

Gemfile に追加します。
追加分のみを記載します。Gemfile 全体は https://github.com/katsuhiko/sample_app_rails_4/blob/master/Gemfile を参照してください。

Gemfile
# Use Unicorn as the app server
gem 'unicorn'
gem 'unicorn-worker-killer'

group :development do
  # Use Capistrano for deployment
  gem 'capistrano', '3.4.0'
  gem 'capistrano-rails'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano3-unicorn'
end

bundle install を実施し、capistrano の設定ファイルの雛形を作成します。

$ bundle install
$ bundle exec cap install

デプロイファイルと unicorn 関連ファイル

Capfile

'capistrano/rails' を読み込めば 'capistrano/bundler', 'capistrano/rails/assets', 'capistrano/rails/migrations' を読み込む必要はありません。

unicorn を使うために 'capistrano3/unicorn' を読み込みます。

Capfile
# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'

# Include tasks from other gems included in your Gemfile
require 'capistrano/rbenv'
require 'capistrano/rails'
require 'capistrano3/unicorn'

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

deploy.rb

ある程度、環境変数で切り替えれるようにしています。
AWS のタグ情報を利用しデプロイするため、aws_region, ssh_keys, tag_role を用意しています。

rails_config で環境を切り替えるため、linked_files は database.yml ではなく production.yml を指定しています。

rails_config については
Rails4 rails_config を使って環境ごとの情報を切り替える
を参照してください。

config/deploy.rb
# config valid only for current version of Capistrano
lock '3.4.0'

set :application, ENV['APPLICATION'] || 'sample_app_rails_4'
set :repo_url, 'https://github.com/katsuhiko/sample_app_rails_4.git'

# 独自の設定項目
set :aws_region, ENV['AWS_REGION'] || 'ap-northeast-1'
set :aws_tag_role, ENV['AWS_TAG_ROLE'] || 'rails'
set :deploy_user, ENV['DEPLOY_USER'] || 'ec2-user'
set :deploy_ssh_keys, ENV['DEPLOY_SSH_KEYS'] || '~/.ssh/amazon.pem'

# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
set :branch, ENV['BRANCH'] || 'master'

# Default deploy_to directory is /var/www/my_app_name
# set :deploy_to, '/var/www/my_app_name'
set :deploy_to, "/var/apps/#{fetch(:application)}"

# Default value for :scm is :git
# set :scm, :git

# Default value for :format is :pretty
# set :format, :pretty

# Default value for :log_level is :debug
# set :log_level, :debug

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
# set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
set :linked_files, fetch(:linked_files, []).push('config/settings/production.yml')

# Default value for linked_dirs is []
# set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle')

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for keep_releases is 5
# set :keep_releases, 5

# rbenv の設定 see: https://github.com/capistrano/rbenv/
set :rbenv_type, :user # :system or :user
set :rbenv_ruby, '2.2.2'
set :rbenv_prefix, "#{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w(rake gem bundle ruby rails)
set :rbenv_roles, :all # default value

# Rails の設定 see: https://github.com/capistrano/rails/
set :rails_env, 'production'

# Unicorn の設定 see: https://github.com/tablexi/capistrano3-unicorn
# shared_path から取得するディレクトリが意図したディレクトリにならないため修正しました。
# http://stackoverflow.com/questions/20789080/capistrano-3-wrong-path-in-the-shared-path-variable
# set :unicorn_pid, "#{shared_path}/tmp/pids/unicorn.pid"
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }
set :unicorn_config_path, "config/unicorn.rb"
set :unicorn_rack_env, 'deployment' # "development", "deployment", or "none"

namespace :deploy do

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke 'unicorn:restart'
    end
  end

  after :publishing, :restart

  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end

end

/var/apps/ 配下にデプロイします。
unicorn の socket ファイルは shared 配下に置きます。
アプリに関するファイルすべてを /var/apps/ 配下に集中させたいと考えているからです。
nginx の実行ユーザーは「nginx」、unicorn の実行ユーザーは「ec2-user」で socket ファイルは apps ディレクトリ配下にあるため、以下のように apps ディレクトリに other 実行権を付与します。

drwxr-xr-x  3 ec2-user ec2-user 4096 Jul 18 15:20 apps

production.rb

http://qiita.com/ShotaKameyama/items/c66d64551411897082a1
こちらを参考にし、aws-sdk v2 を利用したデプロイを行います。

今はローカル環境からデプロイしているため Public IP アドレスを取得しています。
本当は、デプロイする EC2 の VPC 内にデプロイサーバーを立ち上げ、Private IP アドレス経由としたほうが良いと思います。
不要な SSH ポートは解放しないほうが良いと思います。

デプロイ対象となるサーバーを動的に取得しているため、staging 等の環境による記述内容の違いは無くなっています。
production.rb の内容は deploy.rb に移動しても良いかもしれません。

config/deploy/production.rb
require 'aws-sdk-core'

# Shared Credentials を利用する。 see: http://muramasa64.fprog.org/diary/?date=20150217
ec2 = Aws::EC2::Client.new(region: fetch(:aws_region))

# タグと起動中の EC2 で対象を絞り込む。
ec2_filtered = ec2.describe_instances(
    filters:[
        {name: "tag:env", values: [fetch(:rails_env)]},
        {name: "tag:role", values: [fetch(:aws_tag_role)]},
        {name: 'instance-state-name', values: ['running']}
    ])

# TODO deploy 用のサーバーを用意し、private_ip_address を利用して、VPC内から SSH したほうが良い。
instances = ec2_filtered.reservations.map(&:instances).flatten.map(&:public_ip_address)

role :app, *instances
role :web, *instances
role :db, [instances.first]
server *instances,
    user: fetch(:deploy_user),
    ssh_options: {
        forward_agent: true,
        auth_methods: ['publickey'],
        # if you want to debug capistrano set verbose to debug
        # verbose: :debug,
        keys: fetch(:deploy_ssh_keys)
    }

unicorn.rb

http://unicorn.bogomips.org/examples/unicorn.conf.rb
https://github.com/tablexi/capistrano3-unicorn/blob/master/examples/unicorn.rb

上記の2つをテンプレート/参考として利用しています。

unicorn.sock, unicorn.pid ともに shared 配下に持っていくようにしています。

ENV['BUNDLE_GEMFILE'] に値を設定している箇所は重要です。
この設定がない場合、Gemfile を変更しても unicorn worker プロセスに反映されなません。

woker_processes 数が気になると思いますが、woker プロセス数は capistrano タスクで増減させることができます。デプロイ後、状況に応じて調整します。

config/unicorn.rb
# template: http://unicorn.bogomips.org/examples/unicorn.conf.rb
# see: https://github.com/tablexi/capistrano3-unicorn/blob/master/examples/unicorn.rb

app_path = File.dirname(File.dirname(Dir.pwd))

# Sample verbose configuration file for Unicorn (not Rack)
#
# This configuration file documents many features of Unicorn
# that may not be needed for some applications. See
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
# for a much simpler configuration file.
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.

# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
#worker_processes 4
worker_processes 2

# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
# user "unprivileged_user", "unprivileged_group"

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
#working_directory "/path/to/app/current" # available in 0.94.0+
working_directory "#{app_path}/current"

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
#listen "/path/to/.unicorn.sock", :backlog => 64
#listen 8080, :tcp_nopush => true
listen "#{app_path}/shared/tmp/sockets/unicorn.sock", :backlog => 64

# nuke workers after 30 seconds instead of 60 seconds (the default)
#timeout 30
timeout 60

# feel free to point this anywhere accessible on the filesystem
#pid "/path/to/app/shared/pids/unicorn.pid"
pid "#{app_path}/shared/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
#stderr_path "/path/to/app/shared/log/unicorn.stderr.log"
#stdout_path "/path/to/app/shared/log/unicorn.stdout.log"
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"

# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
    GC.copy_on_write_friendly = true

# Enable this flag to have unicorn test client connections by writing the
# beginning of the HTTP headers before calling the application.  This
# prevents calling the application for connections that have disconnected
# while queued.  This is only guaranteed to detect clients on the same
# host unicorn runs on, and unlikely to detect disconnects even on a
# fast LAN.
check_client_connection false

# local variable to guard against running a hook multiple times
run_once = true

# Gemfile を変更したときにも読み込まれるようにする。 see: http://qiita.com/tachiba/items/7eef03cce6a917a957dc
before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = "#{app_path}/current/Gemfile"
end

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
      ActiveRecord::Base.connection.disconnect!

  # Occasionally, it may be necessary to run non-idempotent code in the
  # master before forking.  Keep in mind the above disconnect! example
  # is idempotent and does not need a guard.
  if run_once
    # do_something_once_here ...
    run_once = false # prevent from firing again
  end

  # The following is only recommended for memory/DB-constrained
  # installations.  It is not needed if your system can house
  # twice as many worker_processes as you have configured.
  #
  # # This allows a new master process to incrementally
  # # phase out the old master process with SIGTTOU to avoid a
  # # thundering herd (especially in the "preload_app false" case)
  # # when doing a transparent upgrade.  The last worker spawned
  # # will then kill off the old master process with a SIGQUIT.
  # old_pid = "#{server.config[:pid]}.oldbin"
  # if old_pid != server.pid
  #   begin
  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
  #     Process.kill(sig, File.read(old_pid).to_i)
  #   rescue Errno::ENOENT, Errno::ESRCH
  #   end
  # end
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
  #
  # Throttle the master from forking too quickly by sleeping.  Due
  # to the implementation of standard Unix signal handlers, this
  # helps (but does not completely) prevent identical, repeated signals
  # from being lost when the receiving process is busy.
  # sleep 1
end

after_fork do |server, worker|
  # per-process listener ports for debugging/admin/migrations
  # addr = "127.0.0.1:#{9293 + worker.nr}"
  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
      ActiveRecord::Base.establish_connection

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

config.ru

unicorn を安定稼働させるために unicorn-worker-killer を設定します。

config.ru
# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment', __FILE__)

# Unicorn self-process killer
require 'unicorn/worker_killer'

# Max requests per worker
max_request_min =  3072
max_request_max =  4096
use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max

# Max memory size (RSS) per worker
oom_min = (192 * (1024**2))
oom_max = (256 * (1024**2))
use Unicorn::WorkerKiller::Oom, oom_min, oom_max

run Rails.application

デプロイサーバー(or ローカル環境)のファイル

credentials

AWS への接続は Shared Credentials を利用します。
デプロイするサーバーに ~/.aws/credentials を作成し Key 情報を設定します。

Key 情報を作成するのは以下サイトが参考になると思います。
http://dev.classmethod.jp/cloud/aws-cli-credential-config/

~/.aws/credentials
[default]
aws_access_key_id = XXXXXXXX
aws_secret_access_key = XXXXXXXX

amazon.pem

SSH 接続できるように pem ファイルを用意し、deploy.rb に指定した場所に配置します。

EC2 サーバー(Rails サーバー)のファイル

タグ作成

AWS コンソールから、デプロイする EC2 サーバーに以下のタグを作成します。

キー
env production
role rails

production.yml

bundle exec cap production deploy:check を実施します。
linked_files を用意していないためエラーは発生しますが、必要なディレクトリを作成してくれます。

secret_key_base は bundle exec rake secretで生成した値を設定します。
DBについては、各環境に合わせて設定します。

/var/apps/sample_app_rails_4/shared/config/settings/production.yml
secret:
  secret_key_base: xxxxxxx

databases:
  pool: 5
  host: endpoint
  port: 3306
  username: xxxxxxxx
  password: xxxxxxxx
  production:
    database: sample_app_rails_4_production

nginx.conf

/var/apps/ 配下にアプリに関するファイルをすべて持ってきたいため、nginx.conf を /var/apps/sample_app_rails_4/shared/config/ 配下に作ります。

本来は、サブドメイン名でバーチャルホストを切るのですのが、今回は、ポート番号でわけています。

/var/apps/sample_app_rails_4/shared/config/nginx.conf
upstream sample_app_rails_4 {
  server unix:/var/apps/sample_app_rails_4/shared/tmp/sockets/unicorn.sock fail_timeout=0;
}

# redirect https from http
#server {
#  listen 80;
#  server_name sample.example.com;
#
#  location / {
#    return 301 https://$host$request_uri;
#  }
#}

server {
  listen 443;
  #server_name sample.example.com;

  access_log /var/log/nginx/sample_app_rails_4.access.log;
  error_log /var/log/nginx/sample_app_rails_4.error.log;

  ssl on;
  ssl_certificate /etc/nginx/certs/local-ssl.crt;
  ssl_certificate_key /etc/nginx/certs/local-ssl.key;

  root /var/apps/sample_app_rails_4/current/public;

  proxy_connect_timeout 70;
  proxy_read_timeout    70;
  proxy_send_timeout    70;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    if (!-f $request_filename) {
      proxy_pass http://sample_app_rails_4;
      break;
    }
  }
}

/etc/nginx/nginx.conf には、/var/apps/ 配下の confg を読み込むようにします。include 部分が該当の処理です。

/etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log;
#error_log  /var/log/nginx/error.log  notice;
#error_log  /var/log/nginx/error.log  info;

pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    #
    server_tokens off;
    #

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
    include /var/apps/*/shared/config/nginx.conf;

    index   index.html index.htm;

    server {
        listen       80;
        server_name  localhost;
        root         /usr/share/nginx/html;

        #charset koi8-r;

        #access_log  /var/log/nginx/host.access.log  main;

        location / {
        }

        # redirect server error pages to the static page /40x.html
        #
        error_page  404              /404.html;
        location = /40x.html {
        }

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
        }
    }
}

作成した nginx.conf を読み込むために nginx を restart します。

$ sudo service nginx restart

デプロイの実施

これでデプロイの準備ができました。

デプロイサーバー(or ローカル環境)で sample_app_rails_4 を clone したディレクトリに移動し、デプロイを実施します。

$ bundle exec cap -t production deploy

長かったですが、デプロイについては以上です。

Shared Credentials の内容が正しいにも関わらず、以下のエラーが発生する場合、時刻にズレが発生していないか確認してください。

Aws::EC2::Errors::AuthFailure: AWS was not able to validate the provided access credentials

よく使う capistrano タスク

よく使うタスクを載せます。

デプロイする

-t (--trace) オプションをつけています。どんな順番でタスクが処理されているかがわかるので好きです。

$ bundle exec cap -t production deploy

環境変数を渡したい場合、後ろに指定しています。

$ bundle exec cap -t production deploy BRANCH=feature/new-actions

タスクを確認する

どんなタスクがあるかを確認できます。

$ bundle exec cap -T

unicorn を停止する

設定を反映させるために停止したい場合、使います。
個別に kill するより便利です。

$ bundle exec cap -t production unicorn:stop

unicorn を開始する

unicorn を開始するときにも capistrano は便利です。

$ bundle exec cap -t production unicorn:start

unicorn worker プロセスを増やす

unicorn worker プロセス数も capistrano から調整できます。

$ bundle exec cap -t production unicorn:add_worker

unicorn worker プロセスを減らす

$ bundle exec cap -t production unicorn:remove_worker

感想

情報はたくさんあるので調査しやすかったです。

ソース/デプロイサーバー/EC2 サーバーとそれぞれに記載することがあり、分かりづらくなっています。読み返しながら整理していく予定です。

デプロイツール/構成管理ツール、それぞれやるべきことがあると思います。
どちらか一方のみではなく、上手に両方を使っていきたいです。