bekkou68 の日記

Gogengo! や IT 技術など。

Rails 3 + Nginx/Unicorn を Amazon AWS に Capistrano 3 でデプロイする

はじめに

Amazon AWS 環境下で Rails 3 のアプリを Nginx/Unicorn で動くように Capistrano 3 でデプロイする手順をまとめました。

以下を前提に話を進めます

デプロイ先ディレクトリの準備

アプリを /var/www/myproject にデプロイすることとします。
アプリインスタンスSSHログインし、各ディレクトを作成し、パーミッションを変更します。

$ cd /var
$ sudo mkdir www
$ cd www
$ sudo mkdir myproject
$ sudo chown ec2-user myproject
$ sudo chgrp ec2-user myproject

Nginx をセットアップ

Nginx のログは ephemeral disk に書き込みたいので、そのための出力先をつくります。
他のディレクトリも必要に応じて作成しましょう。たとえば Nginx HttpProxyModule を使いキャッシュの出力先が欲しければ、このタイミングでつくっておくといいかと思います。

$ cd /media/ephemeral0
$ sudo mkdir -p nginx/log # 必要に応じて nginx/cache などを追加
$ sudo chown -R ec2-user nginx
$ sudo chgrp -R ec2-user nginx

nginx.conf を以下のように編集します。

user  ec2-user;
worker_processes  2;

error_log  /media/ephemeral0/nginx/log/error.log;

pid        /var/run/nginx.pid;

events {
  worker_connections  4096;
}

http {
  include       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  /media/ephemeral0/nginx/log/access.log  main;

  sendfile        on;

  keepalive_timeout  65;

  gzip  on;

  upstream backend {
    server unix:/var/www/myproject/shared/tmp/sockets/unicorn.sock;
  }

  # ユーザアクセス時に初めに呼ばれる
  server {
    listen       80;
    server_name  production.example.com;

    # 必要に応じて静的コンテンツの root 指定・キャッシュなどの設定を書く

    # 上記以外のすべてのURL
    location / {
      proxy_pass http://end.example.com;
    }
  }

  server {
      listen 80;
      server_name end.example.com;

      root /var/www/myproject/public;

      proxy_connect_timeout 60;
      proxy_read_timeout    60;
      proxy_send_timeout    60;

      location / {
        index index.html;

        try_files $uri @app; # $url がなければ @app へ
      }

      location @app {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;

        proxy_pass http://backend; # Unicorn へ
      }
  }
}

アプリインスタンスでは Webサーバとアプリケーションサーバを共存させます。そのため /etc/hosts を以下のように編集します。

127.0.0.1   end.example.com

Nginx を起動します。

$ sudo service nginx start

Capistrano をセットアップ

Gemfile を以下のように編集します。

group :development, :test do
  gem 'capistrano', '~> 3.0.1', require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano-rvm', '~> 0.1.0'
  gem 'capistrano-rails'
end

インストールします。環境ごとの設定ファイルが生成されます。

$ bundle exec cap install

Capfile を以下のように編集します。

# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# Includes tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails/tree/master/assets
#   https://github.com/capistrano/rails/tree/master/migrations
#
# require 'capistrano/rbenv'
# require 'capistrano/chruby'
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

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

config/deploy.rb を以下のように編集します。

set :application, 'myproject'
set :scm, :git
set :repo_url, '<your repository path>'
set :deploy_to, '/var/www/myproject'
set :branch, 'master'

set :log_level, :info

# 各ディレクトリの覚書
# - bin: 不要だと思ったが、bundle 時に --binstubs で使われていた
# - bundle: capistrano/bundler のバージョンによって異なるので注意。以前は vendor/bundle だった
#     see: https://github.com/capistrano/bundler/commit/4b4b43a825d675f32c95cee63a094de24d1130c9#diff-91990b1845e3608397d87393341f0544
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets bundle public/system public/assets}

set :ssh_options, {
  keys: [File.expand_path('~/.ssh/myproject.pem')],
  forward_agent: true,
  auth_methods: %w(publickey)
}

namespace :deploy do
  desc 'Restart application'
  task :restart do
    invoke 'unicorn:restart'
  end
end

set :default_env, { rvm_bin_path: '~/.rvm/bin' }
set :rvm_ruby_version, 'ruby-1.9.3-p194'

config/deploy/production.rb を以下のように編集します。

set :stage, :production
set :rails_env, 'production'
set :migration_role, 'db'

server 'production.example.com', user: 'ec2-user', roles: %w{web app db}

Unicorn の task を用意します。lib/capistrano/tasks/unicorn.cap を以下のように作成します。このファイルは Capfile から読み込まれます。Capistrano 2 以前では独自の task を配置するディレクトリはそれぞれの開発者が決めていたと思いますが、3では促されていて素敵です。

namespace :unicorn do
  task :environment do
    set :unicorn_pid, "#{shared_path}/tmp/pids/unicorn.pid"
    set :unicorn_config, "#{current_path}/config/unicorn/#{fetch(:rails_env)}.rb"
  end

  def start_unicorn
    within current_path do
      execute :bundle, :exec, :unicorn, "-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
end

Unicorn をセットアップ

Unicorn の Gem はインストール済みとします。

config/unicorn/production.rb を編集します(参考)。

# coding: utf-8

app_path = '/var/www/myproject'
app_shared_path = "#{app_path}/shared"

worker_processes 5

# 実態は symlink。
# SIGUSR2 を送った時にこの symlink に対して
# Unicorn のインスタンスが立ち上がる
working_directory "#{app_path}/current/"

listen "#{app_shared_path}/tmp/sockets/unicorn.sock"

stdout_path "#{app_shared_path}/log/unicorn.stdout.log"
stderr_path "#{app_shared_path}/log/unicorn.stderr.log"

pid "#{app_shared_path}/tmp/pids/unicorn.pid"

# ダウンタイムをなくす
preload_app true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  old_pid = "#{ server.config[:pid] }.oldbin"
  unless old_pid == server.pid
    begin
      Process.kill :QUIT, File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

デプロイ

以下のコマンドでデプロイします。

$ bundle exec cap production deploy

Capistrano 2 では shared/ 配下のディレクトリを作成するのに deploy:setup を叩きました。3 では deploy に統合されています(もし以前の deploy:setup に相当することだけをやりたければ deploy:check がだいたい同じことをやってくれます)。
実行が正常に終わりましたら、http://production.example.com/ にアクセスしましょう。Nginx/Unicorn が正常に連携してアプリが動いていれば完了です。

トラブルシューティング

Nginx でベーシック認証をかける手順メモ

nginx.conf ファイルの存在するディレクトリに移動して htpasswd ファイルを作成する。

$ cd /etc/nginx
$ sudo htpasswd -c htpasswd <username>

新規に作成するユーザのパスワードが求められるので入力する。

nginx.conf を編集する前に念のためバックアップとる。

$ sudo cp nginx.conf nginx.conf.20131005

以下のように編集。/administrator/* にベーシック認証をかける例。

+ location ~ ^/administrator/* {
+   auth_basic "Restricted";
+   auth_basic_user_file htpasswd;
+   proxy_pass http://backend.example.com; # as you like
+ }

Nginx をリロード。

$ sudo service nginx reload

これで対象ページにアクセスしてベーシック認証がかかってればOK。

How to get status of Nginx HttpProxyModule (proxy_cache) - Just Logging and Collecting Log

Introduction

I run a Unicorn + Nginx server with cache by HttpProxyModule. I'd like to get status of cache, for example, in memcached, we can get status like this.
But, it seems there are no commands for doing that. Finally, my co-worker advised me and the problem was solved.

The method is logging and collecting log. Now, let's do it ;)

1. Logging

Add more log_format and access_log directives to /etc/nginx/nginx.conf.

/etc/nginx/nginx.conf

log_format cache '$remote_addr - $remote_user [$time_local] "$request" '
                   '$status $body_bytes_sent "$http_referer" '
                   '"$http_user_agent" "$http_x_forwarded_for" $upstream_cache_status';
access_log  /var/log/nginx/cache.log  cache;

Reload nginx.

$ sudo service nginx reload

Then log including cache will be outputted like below:

$ cat /var/log/nginx/cache.log
1.2.3.4 ..(snip).. "GET /something HTTP/1.1" 200 ..(snip).. HIT
1.2.3.4 ..(snip).. "GET /another_one HTTP/1.1" 200 ..(snip).. MISS

The last them, HIT and MISS, are status of cache. How great!

An explanation for each status is in here.

2. Collecting log

Write Ruby script as follows:
(It's dirty code, but enough to work)

ache_count = 0 
miss_count = 0 
expired_count = 0 
updating_count = 0 
stale_count = 0 
hit_count = 0 

# Change arbitrarily
lines = File::open('/var/log/nginx/cache.log').read.split("\n").last(10000)

lines.each do |line|
  if line.match(/MISS$/)
    miss_count = miss_count + 1 
    cache_count = cache_count + 1 
  end 

  if line.match(/EXPIRED$/)
    expired_count = expired_count + 1 
    cache_count = cache_count + 1 
  end 

  if line.match(/UPDATING$/)
    updating_count = updating_count + 1 
    cache_count = cache_count + 1 
  end 

  if line.match(/STALE$/)
    stale_count = stale_count + 1 
    cache_count = cache_count + 1 
  end 

  if line.match(/HIT$/)
    hit_count = hit_count + 1 
    cache_count = cache_count + 1 
  end 
end

puts "HIT:%12d (%5.2f%%)\nMISS:%11d (%5.2f%%)\nEXPIRED:%8d (%5.2f%%)\nUPDATING:%7d (%5.2f%%)\nSTALE:%10d (%5.2f%%)" % [ 
  hit_count, (hit_count / (cache_count + 0.0)) * 100,
  miss_count, (miss_count / (cache_count + 0.0)) * 100,
  expired_count, (expired_count / (cache_count + 0.0)) * 100,
  updating_count, (updating_count / (cache_count + 0.0)) * 100,
  stale_count, (stale_count / (cache_count + 0.0)) * 100 
]

Execute script.

$ sudo ruby cache.rb 
HIT:        3586 (85.63%)
MISS:        168 ( 4.01%)
EXPIRED:     434 (10.36%)
UPDATING:      0 ( 0.00%)
STALE:         0 ( 0.00%)

Oh, that's fantastic ;)

Optional

If you'd like to draw a graph of status, fluentd + growthforecast will be nice combination.

At the end

Have a cool Nginx cache life X)