capistranoでnginx, unicornの設定ファイルを管理しつつrailsのdeploy

# 概要
ruby on railsをcapistranoでdeployするための設定方法。
nginxとunicornを使用する。
railsプロジェクトはconfig/database.ymlとconfig/secrets.ymlがgitignoreされており、
config/database.example.ymlとconfig/secrets.example.ymlがレポジトリ内に
存在するとしておく。


全体の流れ

0. サーバ設定など
1. Gemfileに追加し`bundle`
2. `cap install`
3. Capfile, deploy.rb編集
4. tasksファイル（unicorn, nginx関係）とテンプレート作成
5. staging.rb, production.rb編集
6. `cap production deploy:check`
7. server上で`database.yml`, `secrets.yml`などを直す
8. `cap production deploy`


# サーバ上での前設定
新しいサーバで必要な作業。すでにdeployユーザの作成などができている場合は飛ばして良い。

deployユーザの作成

```bash
sudo adduser deploy
```

deployユーザをsudoerにする。

```bash
sudo adduser deploy sudo
```

deployするためのディレクトリを作成。deployユーザに権限を与える。

```bash
sudo mkdir /var/www
sudo chown deploy /var/www
sudo chgrp deploy /var/www
```

bundlerなど実行できるようにしておく。

nginxをインストールし、defaultの設定を消しておく。

```bash
sudo aptitude install nginx
sudo rm /etc/nginx/sites-enabled/default
```

log formatでltsvを使いたいので/etc/nginx/conf.d/ltsv.confを作成。

```config:/etc/nginx/conf.d/ltsv.conf
log_format  ltsv  'time:$time_local\t'
                  'host:$remote_addr\t'
                  'request:$request\t'
                  'status:$status\t'
                  'size:$body_bytes_sent\t'
                  'referer:$http_referer\t'
                  'ua:$http_user_agent\t'
                  'reqtime:$request_time\t'
                  'upsttime:$upstream_response_time';
```

sudoが必要なnginxの起動や、unicornサーバの起動、また設定ファイルの配置などを
deployユーザがパス無しでできるように設定しておくために/etc/sudoersに以下を追記する。
/etc/sudoersの編集はvisudoなどで行う。
（`sudo visudo`で編集ができ、閉じるときにSyntax errorがある場合は、表示してくれる。）

```bash:/etc/sudoers
# 以下を追記
deploy ALL=NOPASSWD: /usr/bin/apt-get up*
deploy ALL=NOPASSWD: /usr/sbin/service /usr/sbin/nginx*
deploy ALL=NOPASSWD: /bin/rm -f /etc/nginx/sites-enabled/default
deploy ALL=NOPASSWD: /bin/mv /tmp/unicorn.service /etc/systemd/system/unicorn*
deploy ALL=NOPASSWD: /bin/mv /tmp/nginx_conf /etc/nginx/sites-enabled/*
deploy ALL=NOPASSWD: /bin/mv /tmp/nginx_default_conf /etc/nginx/sites-enabled/*
deploy ALL=NOPASSWD: /usr/sbin/service nginx *
deploy ALL=NOPASSWD: /bin/systemctl * unicorn*
deploy ALL=NOPASSWD: /bin/systemctl daemon-reload
deploy ALL=NOPASSWD: /bin/mkdir -p /var/log/nginx/*
```

# capistranoの設定
Gemfileにcapistrano関連を登録する。

```ruby:Gemfile
group :development do
  gem 'capistrano-rails'
  gem 'capistrano-rbenv', '~> 2.0.4'
  gem 'capistrano-bundler'
end
```

bundleして、ライブラリをインストールした後、
capistranoで必要なファイルをcap installで作成。


```bash
% bundle
% bundle exec cap install                                                                                                                                                 kenmbp.local
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile
Capified
```

# Capfile
Capfileを修正。
rbenv, bundler, rails/assets, rails/migrationsは最初はコメントアウトされている。
rbenv_typeは:userと:systemの二種類だが、rbenvをローカルインストールしている場合は:userを指定。
rbenv_rubyで使用するrubyのバージョンを指定。

```ruby:Capfile
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

set :rbenv_type, :user
set :rbenv_ruby, '2.3.1'

Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
```

# config/deploy.rb
config/deploy.rbには、stagingとproductionで共通に使う設定を書いていく。

capistrano内でset設定した変数はfetchで取得できる。
他のset変数をsetで使いたい場合は、lamda(->となっているところ)式で呼び出す。
これで変数が後から代入することができるようになる。

デプロイ先:

1. `/var/www/app_name/production`
2. `/var/www/app_name/staging`

```ruby:config/deploy.rb
set :default_stage, 'staging'

set :deploy_to, -> { "/var/www/app_name/#{fetch(:stage)}" }
set :deploy_via, :remote_cache

set :scm, :git
set :repo_url, 'repository_url'
set :keep_releases, 5

set :pty, true
set :ssh_options, {
  forward_agent: true,
  auth_methods: %w(publickey)
}

set :untracked_files, %w(config/database.yml,
                         config/secrets.yml,
                         config/email.yml)
append :linked_files,
       'config/database.yml',
       'config/secrets.yml',
       'config/email.yml'
append :linked_dirs,
       'log',
       'tmp/pids',
       'tmp/cache',
       'tmp/sockets',
       'vendor/bundle',
       'public/system',
       'bin'
```

# 自分でタスクを定義
## 基本的なタスク
lib/capistrano/tasks/deploy.rakeにいくつか共通し使う関数とタスクを定義しておく。

```ruby:lib/capistrano/tasks/deploy.rake
namespace :deploy do
  def remote_file_exists?(full_path)
    test "[ -e #{full_path} ]"
  end

  def template(from, to)
    erb = File.read(File.expand_path("../../templates/#{from}", __FILE__))
    upload! StringIO.new(ERB.new(erb).result(binding)), to
  end

  task :untracked_files do
    on roles(:web) do
      unless remote_file_exists?("#{shared_path}/config/database.yml")
        upload! File.open('config/database.example.yml'),
                "#{shared_path}/config/database.yml"
      end
      unless remote_file_exists?("#{shared_path}/config/secrets.yml")
        upload! File.open('config/secrets.example.yml'),
                "#{shared_path}/config/secrets.yml"
      end
      puts "Now edit the config files in #{shared_path}."
    end
  end
  after 'deploy:check:make_linked_dirs', 'deploy:untracked_files'

  desc 'Make sure local git is in sync with remote.'
  task :check_revision do
    on roles(:web) do
      unless `git rev-parse HEAD` == `git rev-parse origin/#{fetch(:branch)}`
        puts "WARNING: HEAD is not the same as origin/#{fetch(:branch)}"
        puts 'Run `git push` to sync changes.'
        exit
      end
    end
  end
  before 'deploy', 'deploy:check_revision'
end
```

## nginx関連のタスク及び設定
nginxの設定もcapistranoで作成するようにする。
lib/capistrano/templates/nginx_unicorn.erbに以下のような設定を作成。

```ruby:lib/capistrano/templates/nginx_unicorn.erb
upstream unicorn_<%= fetch(:application) %> {
  server unix:/tmp/unicorn.<%= fetch(:application) %>.sock fail_timeout=0;
}

server {
  listen 80;
  server_name <%= fetch(:server_name) %>;
  root <%= current_path %>/public;
  <%= "auth_basic \"Restricted\";\n  auth_basic_user_file /etc/nginx/.htpasswd;" unless fetch(:rails_env) == 'production' %>

  client_max_body_size 100m;
  error_page 404 /404.html;
  error_page 500 502 503 504 /500.html;
  keepalive_timeout 10;
  access_log /var/log/nginx/app_name/<%= fetch(:stage) %>/<%= fetch(:application) %>.log ltsv;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn_<%= fetch(:application) %>;
  location @unicorn_<%= fetch(:application) %> {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn_<%= fetch(:application) %>;
  }
}
```

production以外ではbasic認証を書けるように設定している。
([nginxでbasic認証](http://openbook4.me/users/1/sections/452))


lib/capistrano/tasks/nginx.rakeを作成。

```ruby:lib/capistrano/tasks/nginx.rake
namespace :nginx do
  set :nginx_conf, -> { "/etc/nginx/sites-enabled/#{fetch(:application)}" }

  desc 'Setup nginx configuration for this application'
  task :setup do
    on roles(:web) do
      sudo "mkdir -p /var/log/nginx/app_name/#{fetch(:stage)}"
      template 'nginx_unicorn.erb', '/tmp/nginx_conf'
      sudo "mv /tmp/nginx_conf #{fetch :nginx_conf}"
      sudo 'service nginx restart'
    end
  end

  task :initialize do
    on roles(:app) do
      invoke 'nginx:setup' unless remote_file_exists? fetch :nginx_conf
    end
  end
  after 'deploy:check', 'nginx:initialize'

  %w(start stop restart).each do |command|
    desc "#{command} nginx"
    task command do
      on roles(:web) do
        sudo "service nginx #{command}"
      end
    end
  end
end
```

nginx:setupを実行するとtemplateから作成したnginxの設定ファイルをサーバに配置できる。
ここではafter "deploy:check", :setupを設定しており、
後ほど実行するdeploy:checkの時に自動的に実行されるので、
今は実行しなくても良い

```bash
cap prodution nginx:setup
```

ただし、**nginx設定ファイルを書きかえた場合にはnginx:setupを実行する必要**がある。

またnginxをcapistranoからstart, stop, restartできるようにタスクを作ってある。

```bash
cap production nginx:start
cap production nginx:stop
cap production nginx:restart
```

## unicorn関連のタスク及び設定

nginxと同様にunicornもtemplateを作成しておく。
unicornは起動スクリプトと、unicornの設定の二種類のtemplateを作成する。

unicornの設定のtemplateをlib/capistrano/templates/unicorn.rb.erbに作成。

```ruby:lib/capistrano/templates/unicorn.rb.erb
working_directory "<%= current_path %>"
pid "<%= current_path %>/tmp/pids/unicorn.pid"
stderr_path "<%= shared_path %>/log/unicorn.log"
stdout_path "<%= shared_path %>/log/unicorn.log"

listen "/tmp/unicorn.<%= fetch(:application) %>.sock"
worker_processes 2
timeout 30
```

unicorn起動のためのスクリプトtemplateをlib/capistrano/templates/unicorn.service.erbに作成。

```config:lib/capistrano/templates/unicorn.service.erb
[Unit]
Description=Unicorn server for <%= fetch(:application) %>

[Service]
SyslogIdentifier=unicorn-<%= fetch :application %>
User=deploy
PIDFile=<%= current_path %>/tmp/pids/unicorn.pid
WorkingDirectory=<%= current_path %>
Type=forking
Environment=RAILS_ENV=<%= fetch :rails_env %>

ExecStart=/home/deploy/.rbenv/bin/rbenv exec bundle exec unicorn -D -c <%= shared_path %>/config/unicorn.rb -E <%= fetch :rails_env %>
ExecStop=/bin/kill -s QUIT $MAINPID
ExecReload=/bin/kill -s HUP $MAINPID

[Install]
WantedBy=multi-user.target
```

これがあるとサーバを再起動した時などに、
自動的にunicornを起動させることができる。

unicorn関連のタスクをlib/capistrano/tasks/unicorn.rakeに設定

```ruby:lib/capistrano/tasks/unicorn.rake
namespace :unicorn do
  desc 'Setup Unicorn initializer and app configuration'
  task :setup do
    on roles(:app) do
      execute "mkdir -p #{shared_path}/config"
      template 'unicorn.rb.erb', "#{shared_path}/config/unicorn.rb"
      template 'unicorn.service.erb', '/tmp/unicorn.service'
      target_file = "/etc/systemd/system/unicorn_#{fetch(:application)}.service"
      sudo "mv /tmp/unicorn.service #{target_file}"
      sudo 'systemctl daemon-reload'
      sudo "systemctl start unicorn_#{fetch(:application)}"
    end
  end

  task :initialize do
    on roles(:app) do
      init_file_path =
        "/etc/systemd/system/unicorn_#{fetch(:application)}.service"
      config_file_path = "#{shared_path}/config/unicorn.rb"
      unless remote_file_exists?(init_file_path) &&
             remote_file_exists?(config_file_path)
        invoke 'unicorn:setup'
      end
    end
  end
  after 'deploy:check', 'unicorn:initialize'

  %w(start stop reload status is-active).each do |command|
    desc "#{command} unicorn"
    task command do
      on roles(:app) do
        sudo "systemctl #{command} unicorn_#{fetch(:application)}"\
             "#{command == 'status' ? ' | cat' : nil}"
      end
    end
  end

  after 'deploy:finished', 'unicorn:reload'
  after 'deploy:finished', 'unicorn:is-active'
end
```

nginxの場合と同様に、unicorn:setupを実行すると
templateから作成したunicornの設定ファイルをサーバに配置できる。
ここでもafter "deploy:check", :setupを設定しており、
後ほど実行するdeploy:checkの時に自動的に実行されるので、
今は実行しなくても良い

```bash
cap production unicorn:setup
```

**設定を変更した場合は、再びunicorn:setupを実行する必要がある。**

またnginxと同様にローカルからstart, stop, restartができるようになっている。

```bash
cap production unicorn:start
cap production unicorn:stop
cap production unicorn:restart
```

# staging及びproduction独自の設定
staging, production独自の設定に関しては、
それぞれconfig/deploy/staging.rbとconfig/deploy/production.rb
に書く。

```ruby:config/deploy/staging.rb
set :application, 'app_name_staging'
set :branch, :staging
set :rails_env, :staging
server '0.0.0.0', user: 'deploy', roles: %w{app db web}
set :server_name, 'staging.example.com'
```

```ruby:config/deploy/production.rb
set :application, 'app_name'
set :branch, :master
set :rails_env, :production
server '0.0.0.0', user: 'deploy', roles: %w{app db web}
set :server_name, 'example.com'
```

# capistranoによるサーバ上のファイル設定
cap deploy:checkで設定ファイルをサーバ上に設置する。

```
cap production deploy:check
```

deploy:checkはdeployのときに実行されるタスクではあるが、
database.yml, secrets.ymlがただしく設定できていないので、deployを実行するのではなく
deploy:checkを実行する。

たまにdeploy:checkあたりで、gitにアクセス出来ないというエラーが出ることがある。

```
Please make sure you have the correct access rights
and the repository exists.
git stderr: Nothing written
```

これはdeployユーザの公開鍵をrepositoryサーバに登録しなくても、ローカルのユーザの
鍵を使ってpullできるようにuser agentの登録をローカルで行えば良い。

```bash
ssh-add ~/.ssh/id_rsa
```

サーバ上のdatabase.ymlやsecret.ymlを変更した後、cap deployでdeployができる。

```bash
cap production deploy
```

# 参考
## capistranoタスクの流れ
### deploy

```
deploy:starting    - start a deployment, make sure everything is ready
deploy:started     - started hook (for custom tasks)
deploy:updating    - update server(s) with a new release
deploy:updated     - updated hook
deploy:publishing  - publish the new release
deploy:published   - published hook
deploy:finishing   - finish the deployment, clean up everything
deploy:finished    - finished hook
```

### rollback

```
deploy:starting
deploy:started
deploy:reverting           - revert server(s) to previous release
deploy:reverted            - reverted hook
deploy:publishing
deploy:published
deploy:finishing_rollback  - finish the rollback, clean up everything
deploy:finished
```

### deploy:check

```
deploy:check:directories
deploy:check:linked_dirs
deploy:check:make_linked_dirs
deploy:check:linked_files
```

#実際に陥りやすいポイント

## unicorn.rbが移行されず、エラーが出て止まる
上記の順番で実行した時に、unicorn.rbがうまく`shared/config/`以下に移せなかった。
この時には、`cap production unicorn:setup`を実行するとよい。
もしかすると、`remote_exist_file?`らへんの条件がうまく機能していないのかもしれない。

## No passrd for user '....' というエラーで止まる

database.ymlの設定で、dbにつなげていない場合に起こる。
解決方法の一つとして、`deploy`でログインして、`createdb app_db_name`を実行（Postgresの場合）し、deployとそのパスワードをdatabase.ymlに書くことで解決するかも。


##　The deploy has failed with an error

```
The deploy has failed with an error: Exception while executing as deploy@example.com: rake exit status: 1
rake stdout: config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly:

  * development - set it to false
  * test - set it to false (unless you use a tool that preloads your test environment)
  * production - set it to true

rake aborted!
```

`cp config/environments/production.rb config/enviroments/staging.rb`
を実行し、`staging.rb`を作る

## PG::ConnectionBad: FATAL:  password authentication failed for user "deploy"

database.ymlの情報ミス


## sudo stdout: appname_staging: unrecognized service

```
 aborted!

...
SSHKit::Command::Failed: sudo exit status: 1
...
Tasks: TOP => unicorn:restart
(See full trace by running task with --trace)

sudo stderr: Nothing written
```

`deploy staging unicorn:setup`して解消