Cloudn上にCapistrano3を使ってNginx+unicorn+Railsアプリをデプロイしてみた


今回はRailsアプリawesome_events（書籍"パーフェクトRuby on Rails"で写経したやつ）をCloudn上で動かすようにします。

capistrano設定についてはQiita記事を参考に構築しましたが、つまったところがあるので自分メモとして整理しておきます。

参考：

- [unicorn + rails 用 Capistrano 3 の設定ファイル](http://qiita.com/satococoa/items/9b0cc416ffc042680b9b)
- [Capistrano3でrailsをdeployしてみる](http://qiita.com/ea54595/items/12ab7b3a8213b35cca10)

## Cloudn上でRailsが動く環境を構築

### Cloudn上に新しいセキュリティグループtestappを作成

1.ネットワークの"セキュリティグループの追加"から新しいグループtestappを追加します。

2.testappを開き、次のポート設定を追加します。

受信規則にport:22 0.0.0.0./0
受信規則にport:443 0.0.0.0./0
受信規則にport:80 0.0.0.0./0

*理由はわかってませんが、セキュリティグループの状態によっては、初期パスワードでログインできませんでした。
（`Permission denied, please try again.`と表示されました）
新規でセキュリティグループを作ればログインできました。*

### Cloudn上に新しい仮想サーバーtestappを作成

1."仮想サーバーの追加"から新しいインスタンスを作成します。

```
ISO またはテンプレートの選択:テンプレート
テンプレート：Ubuntu Server v14.04 64bit
仮想サーバープラン：t1.micro
データディスク：設定しない
セキュリティグループ：testapp
名前：testapp
```

2.初期パスワードをメモします。
1でインスタンスを作成すると、最後にダイアログが表示され、初期パスワードが表示されます。
それをメモしておきます。

3.仮想サーバーにログインします。
仮想サーバー一覧から作成したtestappを選択し、NICタブからIPアドレスを確認します。

そのアドレスに対しsshでログインします。（Ubuntuインスタンスの初期ユーザーはubuntu）

`ssh ubuntu@xxx.xxx.xxx.xxx`

パスワードは先ほどメモしたものです。

`もしWARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!と表示された場合は、~/.ssh/known_hostsから該当するIPアドレスの行を削除します`

4.パスワードを変更します

`passwd`

## 仮想サーバーにrubyの環境を構築

chef等で一気にやるべきと思いますが、[自前のDockerfile](https://github.com/t-oginogin/dockerfiles/blob/master/ubuntu/Dockerfile_ruby)の手順を手動で行いました。
rubyのバージョン管理にはrbenvを使います。

```bash
sudo apt-get update
sudo apt-get install -y build-essential wget curl git
sudo apt-get install -y zlib1g-dev libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt-dev
sudo apt-get install -y sqlite3 libsqlite3-dev
sudo apt-get clean

sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

git clone https://github.com/sstephenson/ruby-build.git .ruby-build
sudo .ruby-build/install.sh
rm -fr .ruby-build

sudo ruby-build 2.1.2 /usr/local

git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
source ~/.bash_profile
rbenv install 2.1.2
rbenv global 2.1.2

gem update --system
gem install bundler --no-rdoc --no-ri
rbenv rehash
```

## RailsアプリにCapistrano設定を追加

###deploy用のstaging環境用の設定を作成します。

database.ymlにstaging設定を追加します。

```yaml:config/database.yml
:
staging:
  <<: *default
  database: db/staging.sqlite3
:
```

config/environments/development.rbをコピーしてconfig/environments/staging.rbを作成します。
（staging環境の位置づけによってはproduction.rbからコピーした方が良いと思います）

```rb:config/environments/staging.rb
Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded on
  # every request. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.cache_classes = false

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports and disable caching.
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Debug mode disables concatenation and preprocessing of assets.
  # This option may cause significant delays in view rendering with a large
  # number of complex assets.
  config.assets.debug = true

  # Adds additional error checking when serving assets at runtime.
  # Checks for improperly declared sprockets dependencies.
  # Raises helpful error messages.
  config.assets.raise_runtime_errors = true

  # Raises error for missing translations
  # config.action_view.raise_on_missing_translations = true
end
```

config/secrets.ymlにsecret_keyを追加します。
keyの値(xxxxxxxxxxxxxxの部分)はdevelopmentと同じ値を入れました。

```yaml:config/secrets.yml
:
staging:
  secret_key_base: xxxxxxxxxxxxxx
  <<: *default_twitter
:
```

###JavaScriptエンジンの設定を追加します。
ターゲットがUbuntuなのでGemfileのtherubyracerを有効にします。

```rb:Gemfile
:
gem 'therubyracer',  platforms: :ruby
:
```
`bundle install`を実行します。

###unicornの設定を追加します。

Gemfileのunicornの設定を有効にします。

```rb:Gemfile
:
gem 'unicorn'
:
```
`bundle install`を実行します。

config/unicorn/staging.rbを作成します。

```rb:config/unicorn/staging.rb
# 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

# 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.
app_path = '/var/www/awesome_events'
app_shared_path = "#{app_path}/shared"
working_directory "#{app_path}/current/" # available in 0.94.0+

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

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

# feel free to point this anywhere accessible on the filesystem
pid "#{app_shared_path}/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 "#{app_shared_path}/log/unicorn.stderr.log"
stdout_path "#{app_shared_path}/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

before_fork do |server, worker|
  ENV['BUNDLE_GEMFILE'] = File.expand_path('Gemfile', ENV['RAILS_ROOT'])

  # 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!

  # 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
  #
  # 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/unicorn/staging.rbは、`~/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/unicorn-4.8.3/examples/unicorn.conf.rb`をコピーして作りました。

path等の設定だけではcap実行時にunicornのreloadでこけてしまいました。
そこは次の2つの対応で解決しました。

```
ENV['BUNDLE_GEMFILE'] = File.expand_path('Gemfile', ENV['RAILS_ROOT'])
```

の行を追加。

```
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
```

の行を有効化。

###capistrano関連を追加します。

Gemfileにcapistranoを追加します。

```:Gemfile
group :development, :test do
  gem 'capistrano', :require => false
  gem 'capistrano-rails', :require => false
  gem 'capistrano-rbenv', :require => false
  gem 'capistrano-bundler', :require => false
end
```
`bundle install`を実行します。

`bundle exec cap install`を実行し、capistrano関連ファイルを生成します。

Capfileを修正します。

```rb: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
#
# require 'capistrano/rvm'
require 'capistrano/rbenv'
set :rbenv_type, :user
set :rbenv_ruby, '2.1.2'
# require 'capistrano/chruby'
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 }
```

最後の行はもともとtasks/*.rbとなっていたので、tasks/*.capに変更しています。
（そのまま使う場合は、後述の`lib/capistrano/tasks/unicorn.cap`を`lib/capistrano/tasks/unicorn.rb`という名前にすれば良いでしょう）

config/deploy.rbを修正します。
URL等は自分の環境に置き換えてください。

```rb:config/deploy.rb
# config valid only for Capistrano 3.1
lock '3.2.1'

set :application, 'awesome_events'
set :repo_url, 'https://github.com/t-oginogin/awesome_eventes.git'

# Default branch is :master
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call
set :branch, :master

# Default deploy_to directory is /var/www/my_app
set :deploy_to, '/var/www/awesome_events'

# 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, %w{config/database.yml}

# Default value for linked_dirs is []
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system public/assets}

# Default value for default_env is {}
set :default_env, { path: "/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH" }

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

namespace :deploy do

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

  after :publishing, :restart

end
```

config/deploy/staging.rbを修正します。

ターゲットサーバーのアドレス、ログイン情報は環境変数から取得するようにしています。

```rb:config/deploy/staging.rb
set :stage, :staging
#
# Simple Role Syntax
# ==================
# Supports bulk-adding hosts to roles, the primary server in each group
# is considered to be the first unless any hosts have the primary
# property set.  Don't declare `role :all`, it's a meta role.

role :app, %W{#{ENV['AWESOME_USER']}@#{ENV['AWESOME_ADDRESS']}}
role :web, %W{#{ENV['AWESOME_USER']}@#{ENV['AWESOME_ADDRESS']}}
role :db,  %W{#{ENV['AWESOME_USER']}@#{ENV['AWESOME_ADDRESS']}}


# Extended Server Syntax
# ======================
# This can be used to drop a more detailed server definition into the
# server list. The second argument is a, or duck-types, Hash and is
# used to set extended properties on the server.

server "#{ENV['AWESOME_ADDRESS']}", user: "#{ENV['AWESOME_USER']}", roles: %w{web app db}

# Custom SSH Options
# ==================
# You may pass any option but keep in mind that net/ssh understands a
# limited set of options, consult[net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start).
#
# Global options
# --------------
set :ssh_options, {
#    keys: %w(/home/rlisowski/.ssh/id_rsa),
#    forward_agent: false,
  auth_methods: %w(password),
  password: ENV['AWESOME_PASS']
}
#
# And/or per server (overrides global)
# ------------------------------------
# server 'example.com',
#   user: 'user_name',
#   roles: %w{web app},
#   ssh_options: {
#     user: 'user_name', # overrides user setting above
#     keys: %w(/home/user_name/.ssh/id_rsa),
#     forward_agent: false,
#     auth_methods: %w(publickey password)
#     # password: 'please use keys'
#   }
```

lib/capistrano/tasks/unicorn.capを作成します。
Nginxとはport8080で通信する想定です。

```rb:lib/capistrano/tasks/unicorn.cap
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_rails, "-c #{fetch(:unicorn_config)} -E #{fetch(:rails_env)} -p 8080 -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
```

###ターゲットサーバー上でNginxを設定します。
本当はここも自動化しておきたいところですが、今回は手動で設定しました。
（[jenkinsのインストール](https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu)が参考になります）

```bash
sudo aptitude -y install nginx
cd /etc/nginx/sites-available
sudo rm default ../sites-enabled/default
```

/etc/nginx/sites-availableにawesome_eventsを配置します。
unicornとはport8080で通信させます。

```bash:/etc/nginx/sites-available/awesome_events
upstream app_server {
    server 127.0.0.1:8080 fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80 default ipv6only=on;
    server_name awesome_events;

    location / {
        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://app_server;
            break;
        }
    }
}
```

```bash
sudo ln -s /etc/nginx/sites-available/awesome_events /etc/nginx/sites-enabled/
sudo service nginx restart
```

###deploy場所を作っておきます。

```bash
sudo mkdir /var/www
sudo chown ubuntu /var/www
sudo chgrp ubuntu /var/www
sudo chmod +rwx /var/www
```

##ローカルマシンからdepoyを実行します。

ローカルマシンに戻って、capコマンドでデプロイします。
いくつか環境変数で設定するようにしているので、忘れず設定します。

```bash
bundle exec cap staging deploy AWESOME_ADDRESS=xxx.xxx.xxx.xxx AWESOME_USER=ubuntu  AWESOME_PASS=your_password
```

無事デプロイできました。

ソースはGitHubに置いています。
https://github.com/t-oginogin/awesome_eventes