Rails 3 + Nginx/Unicorn を Amazon AWS に Capistrano 3 でデプロイする
はじめに
Amazon AWS 環境下で Rails 3 のアプリを Nginx/Unicorn で動くように Capistrano 3 でデプロイする手順をまとめました。
以下を前提に話を進めます
- デプロイ対象のアプリ/DBインスタンスはすでにつくられているとします
- デプロイ対象のアプリインスタンスのドメインは production.example.com とします
- アプリインスタンスは ephemeral disk がマウントされているとします
- プロジェクト名は myproject とします。ご自身のプロジェクト名に読み替えてください
- アプリインスタンスに SSHログインするための秘密鍵は ~/.ssh/myproject.pem に配置してあるとします
- RVM を使ってます。rubyenv での設定はこちらの記事が参考になるかと思います
デプロイ先ディレクトリの準備
アプリを /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 が正常に連携してアプリが動いていれば完了です。
トラブルシューティング
- bundle install 時に "bundle stdout: Nothing written" というエラー
- --path で指定しているパスにディレクトリがなかった。capistrano/bundler のバージョンによってパスが変わるので注意
- capistrano-rvm の指定がまるごと抜けていた。指定したら動いた
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)