A Peak Never Ending !

Asset Pipelineのファイルを結合せずに別々にプリコンパイルする

rails3.1のasset pipelineはapplication.cssやapplication.jsでrequireしているファイルをプリコンパイル時に一つのファイルにまとめてincludeしてしまうけど、場合によっては別々にプリコンパイルして条件によって読み込むものを変えたい(例えばコントローラ別に変えるとか)をするための方法。特に難しくなくてhoge.css.scssとfuga.css.scssを別々にプリコンパイルする場合は以下のようにconfig/environments/production.rbに以下の記述を追加すれば良い。

production.rb
1
config.assets.precompile += %w( hoge.css fuga.css )

view側で読み込む時はこんな感じ

application.html.erb
1
2
<%= stylesheet_link_tag  "hoge" %>
<%= stylesheet_link_tag  "fuga" %>

Nginx + unicornでrails3.1が動作する環境を作る

方針

  • 手元(Ubuntu)で開発して、サーバ(Ubuntu)にデプロイ出来るrails 3.1動作環境を作るのが目標
  • プロジェクト毎にユーザを作成する (各ライブラリをプロジェクト毎にbundlerで管理、デプロイをするため)
  • 同様の理由でrbenvを使って各ユーザ毎にrubyのバージョンを管理

構成

静的なファイルへのリクエストは直接nginxで返す構成をとります(railsのpublic配下のディレクトリにあるファイル、適宜nginxのconfigに設定を追加する必要あり)。またrails3.1からAsset Pipelineが導入されたため/assets/〜に関するリクエストに関してもnginxで直接返すようにします。加えてnginx <=> unicorn間の接続にはUnix Domain Socketを用います。イメージを図にすると下記のようになります。

ローカルでプロジェクトを作成する (Ubuntu)

新規railsプロジェクトを作成する

1
$ rails new myproj

Gemfileに必要なものを追加する

myproj/Gemfile
1
2
3
4
5
6
7
8
9
gem 'unicorn'
gem 'therubyracer'

group :development do
  gem 'capistrano'
  gem 'capistrano_colors'
  gem 'capistrano-ext'
  gem 'capistrano_rsync_with_remote_cache'
end

ローカルでもプロジェクト毎に管理してる方が便利なのでbundlerでインストールする際に–path vendor/bundleを指定してインストール bundler参考ページ –pathについて

インストール
1
2
# myproj内で
$ bundle install --path vendor/bundle

bundle installでインストールしたものがコミットされないように.gitignoreに追加しておく。ついでにデプロイ時に作成される不必要なディレクトリもignoreに追加しておく.

myproj/.gitignore
1
2
/vendor/bundle
/.rsync_cache

unicornのconfigファイルを追加する

myproj/config/unicorn/release.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
proj_root_dir = File.expand_path("../../../../", __FILE__)
worker_processes 4
listen      "#{proj_root_dir}/shared/run/universe.sock"
pid         "#{proj_root_dir}/current/tmp/pids/unicorn.pid"
stderr_path "#{proj_root_dir}/current/log/unicorn.log"
stdout_path "#{proj_root_dir}/current/log/unicorn.log"

# graceful restart用の設定 (Masterプロセスがシームレスに切り替わる)
before_fork do |server, worker|
  old_pid = "#{server.config[:pid]}.oldbin"
  # oldプロセスがいたら終了する
  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
end

unicorn起動用のスクリプトを追加する

myproj/script/unicorn/release
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/sh

PATH=/bin:/usr/bin:/sbin:/usr/sbin:~/.rbenv/bin:~/.rbenv/shims

# move to project root directory
NAME=unicorn
ENVIROMENT=production

SCRIPT_DIR=`dirname $0`
ROOT_DIR=$(cd "${SCRIPT_DIR}/../../"; pwd)

PID="${ROOT_DIR}/tmp/pids/unicorn.pid"
CONF="${ROOT_DIR}/config/unicorn/release.rb"

start()
{
    cd $ROOT_DIR
    if [ -e $PID ]; then
        echo "$NAME already started";
        exit 1;
    fi
    echo "start $NAME";
    bundle exec unicorn_rails -c ${CONF} -E ${ENVIROMENT} -D
}

stop()
{
    if [ ! -e $PID ]; then
        echo "$NAME not started";
        exit 1;
    fi
    echo "stop $NAME";
    kill `cat ${PID}`
    rm -f $PID
}

graceful_stop()
{
    if [ ! -e $PID ]; then
        echo "$NAME not started";
        exit 1;
    fi
    echo "stop $NAME";
    kill -QUIT `cat ${PID}`
    rm -f $PID
}

reload()
{
    if [ ! -e $PID ]; then
        echo "$NAME not started";
        start
        exit 0;
    fi
    echo "reload $NAME";
    kill -USR2 `cat ${PID}`
}

restart()
{
    if [ ! -e $PID ]; then
        echo "$NAME not started";
        start
        exit 0;
    fi
    echo "restart $NAME";
    stop
    start
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    graceful_stop)
        graceful_stop
        ;;
    reload)
        reload
        ;;
    restart)
        reload
        ;;
    *)
        echo "Syntax Error: release [start|stop|graceful_stop|reload|restart]"
        ;;
esac

デプロイ用の設定ファイルを配備します今回は下記の様な環境になります。

  • リポジトリにgithubを使っている
  • サーバには鍵で接続
  • 手元にブランチを落としてきてサーバにrsyncでデプロイ
  • デプロイしたファイルはサーバ上で/home/myproj/release/current以下に配備
  • staging用のデプロイ環境なども考慮してmultistageを利用 capistrano-extでステージング環境にデプロイ
myproj/Capfile
1
2
3
4
5
6
7
# 複数のデプロイ環境の作成をサポート
require "capistrano/ext/multistage"
# デプロイ時にbundle install実行する
require 'bundler/capistrano'

load 'deploy' if respond_to?(:namespace)
load 'config/deploy'
myproj/config/deploy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 変更が必要な内容
set :application, "myporj"
set :repository,  "git@github.com:semind/myproj.git"
set :user, "myproj"
set :use_sudo, false

# それ以外
ssh_options[:paranoid] = false
ssh_options[:auth_methods] = ["publickey"]
ssh_options[:port] = 22

set :scm, :git
set :rails_env,   "production"
set :keep_releases, 2

set :deploy_via, :rsync_with_remote_cache
set :rsync_options, '-az --delete --delete-excluded --exclude=.git'

set :bundle_cmd, "/home/#{user}/.rbenv/shims/bundle"
set :bundle_without, [:development, :test]

set :default_environment, {
  'PATH' => "$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH",
}

after :deploy, "deploy:cleanup"
after "deploy:setup" do
  run <<-CMD
    mkdir -p "#{shared_path}/run"
  CMD
end

namespace :deploy do
  task :start, :roles => :app, :except => { :no_release => true } do
    run "cd #{current_path} && #{unicorn_script} start"
  end
  task :stop, :roles => :app, :except => { :no_release => true } do
    run "#{unicorn_script} stop"
  end
  task :graceful_stop, :roles => :app, :except => { :no_release => true } do
    run "#{unicorn_script} graceful_stop"
  end
  task :reload, :roles => :app, :except => { :no_release => true } do
    run "cd #{current_path} && #{unicorn_script} reload"
  end
  task :restart, :roles => :app, :except => { :no_release => true } do
    run "cd #{current_path} && #{unicorn_script} restart"
  end
end
myproj/deploy/release.rb “`
1
2
3
4
5
6
7
8
9
10
11
12
# デプロイ時にrake assets:precompileが走る
load 'deploy/assets'

# ホスト名は変更が必要
server 'release', :app, :web, :db, :primary => true

set :branch,    "master"
set :deploy_to, "/home/#{user}/release"

set :unicorn_script, "/home/#{user}/release/current/script/unicorn/release"
set :unicorn_conf,   "/home/#{user}/release/current/config/unicorn/release.rb"
set :unicorn_pid,    "/home/#{user}/release/current/tmp/pids/release.pid"

サーバ側のセッティング

myprojユーザを作って公開鍵を置いておく(省略), 以下基本的にmyprojユーザで行う

rbenvをインストールする

install rbenv
1
2
3
4
$ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ source ~/.bashrc  # rbenvのPATHを通すのに必要

ruby-buildインストール

install rbenv
1
2
3
4
$ cd /usr/local/src
$ sudo git clone git://github.com/sstephenson/ruby-build.git
$ cd ruby-build
$ sudo ./install.sh

ruby(今回は1.9.3-p0)をインストールしてデフォルトに指定

install ruby 1.9.3-p0
1
2
$ rbenv install 1.9.3-p0
$ rbenv global  1.9.3-p0  # デフォルトで使用するrubyのバージョンを指定

bundler(最新版)インストール

install bundler
1
2
$ rbenv exec gem install bundler --pre
$ eval "$(rbenv init -)" # bundlerにパスを通すのに必要

ローカルマシンからデプロイ

myproj内で以下を実行します

デプロイ
1
2
$ bundle exec cap release deploy:setup
$ bundle exec cap release deploy

nginxの設定をする

  • upstreamでunix domain socket
  • 静的なファイルの場合はlocation ~を用いてrootディレクトリにpublicを指定する
/etc/nginx/nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
user www-data;
worker_processes  2;

pid  /var/run/nginx.pid;

events {
    worker_connections  512;
    use epoll;
}

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

    access_log off;

    log_format  main '$msec\t'
        '$status\t'
        '$request_time\t'
        '$remote_addr\t'
        '$upstream_addr\t'
        '$upstream_response_time\t'
        '$request\t'
        '$http_referer\t'
        '$http_user_agent';

    server_tokens off;
    sendfile       on;

    keepalive_timeout  2;
    tcp_nodelay        off;

    # upstreamにunix domain socketを指定する
    upstream myproj {
        server unix:/home/myproj/release/shared/run/universe.sock;
    }

    server {
        listen       80;
        server_name  myproj;

        access_log  /var/log/nginx/www.myproj.net-access.log main;
        error_log   /var/log/nginx/www.myproj.net-error.log;

# public以下に静的ファイルをおいた場合は同様にする
        location = /favicon.ico {
            root   /home/myproj/release/current/public;
        }

# asset pipeline
        location ~ ^/assets/ {
            root   /home/myproj/release/current/public;
        }

        location / {
            proxy_pass http://myproj;
        }
    }
}