Rails5+Puma+Nginxな環境をCapistrano3でEC2にデプロイする(後編)

Rails5系で新しく開発しているウェブアプリケーションを、AWSのEC2で稼働している本番環境にCapistranoでデプロイすることになったときのお話です。これまで個人でなにかを開発する際にはRails4系でアプリケーションサーバにUnicornを採用していましたが、今回からはPumaを採用することにしたのでその手順です。

こちらの記事の後編です。
[Rails5+Puma+Nginxな環境をCapistrano3でEC2にデプロイする(前編)](http://qiita.com/kappy0322/items/87d54752021879e1ec89)


## 前提
- Rails 5.0.0
- Ruby 2.4.0
- rbenv 0.4.0
- Puma
- Nginx
- Capistrano 3.7.0
- Amazon Linux AMI 2017.03.0 (HVM)


## 手順
- 1. EC2にRails環境を構築
- 2. Nginxのインストールと起動
- 3. Nginxの設定を修正
- 4. デプロイ設定を修正（ローカル） 
- 5. アプリケーションログを確認
- 6. データベースを設定
- 7. アプリケーションサーバを起動
- 8. デプロイタスクを修正（ローカル）


### 1. EC2にRails環境を構築

ここまで手順通りに進めている場合にはEC2環境にはまだRailsアプリケーションを動かす環境が整っていないので順番にいれていく。

```
$ pwd
/home/deploy
```

1) yum

```
$ sudo yum update

$ sudo yum install git
$ sudo yum install gcc
$ sudo yum install gcc-c++
$ sudo yum install openssl-devel
$ sudo yum install readline-devel
$ sudo yum install mysql-devel
```

2) ruby-build

```
$ git clone git://github.com/sstephenson/ruby-build.git
$ sudo ruby-build/install.sh
```

3) rbenv

```
$ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
$ exec $SHELL -l

$ rbenv --version
rbenv 1.1.1
```

4) rbenv-vars

```
$ git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars
```

5) Ruby 2.4.0

```
$ rbenv install 2.4.0
$ rbenv global 2.4.0
$ rbenv rehash

$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]
```

6) bundler

```
$ gem install bundler
```

7) rubyracer

```
$ gem install therubyracer
```

8) Rails 5.0.0

```
$ gem install rails -v 5.0.0

$ rails -v
Rails 5.0.0
```

9) Puma

```
$ gem install puma
```

10) MySQL

```
$ sudo yum install mysql-devel
$ sudo yum install mysql-server
$ sudo /etc/init.d/mysqld start
$ sudo chkconfig mysqld on
```
2018-09-02 updated：
mysql-serverをyumでインストールできない場合はこちらを参考に。
[AWS Amazon Linux2にMySQL5.7を構築する
](https://qiita.com/himatani/items/e1e650992dbc12b8a9b3)


### 2. Nginxのインストールと起動

まずは本番環境にNginxをインストールする。以下のコマンドで正しく起動できない場合は `/var/log/nginx/error.log` にエラーログが出力されているはずなので確認する。ブラウザからアクセスしてNginxのwelcome画面が表示されていれば正しく起動できている。

```
$ sudo yum -y install nginx
$ sudo /etc/init.d/nginx start
```

設定ファイルはデフォルトでこちら `/etc/nginx/nginx.conf` が読まれる。初期設定で生成されているファイルは念のためコピーしておくことにする。

```
$ sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.cpy
```


### 3. Nginxの設定を修正

Nginxの設定ファイルを修正する。と言っても、デフォルトで生成されたものを何箇所か変更した程度。以下にサンプルを載せておくが自身の環境に応じてよしなに修正する。

```
$ sudo vi /etc/nginx/nginx.conf
```

```/etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

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

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

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

    include /etc/nginx/conf.d/*.conf;

    index   index.html index.htm;

    upstream puma {
        server unix:///var/www/my-app-name/shared/tmp/sockets/puma.sock;
    }
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /var/www/my-app-name/current/public;

        include /etc/nginx/default.d/*.conf;

        location / {
            try_files $uri $uri/index.html $uri.html @webapp;
        }

        location @webapp {
            proxy_read_timeout 300;
            proxy_connect_timeout 300;
            proxy_redirect off;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://puma;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}
```

Nginxの設定ファイルの例はこちらに詳しく書かれている。
[Nginx configuration example file](https://github.com/puma/puma/blob/master/docs/nginx.md)


### 4. デプロイ設定を修正（ローカル）

デプロイ対象の本番環境には `/var/www/my-app-name/shared/` 配下に以下のディレクトリが並ぶようにする。development環境ではアプリケーションのルート直下にこれらのファイルが存在したが、Capistranoでデプロイした本番環境ではこれらは `/shared` に配置する。デフォルトではデプロイしない設定になっているため修正する。

- log
- public
- tmp

ローカルに戻り、以下の設定を追記し再度デプロイを行う。上記のディレクトリ群が `/shared` 配下に生成されていることを確認する。

```ruby:config/puma.rb
app_dir = File.expand_path("../..", __FILE__)
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
pidfile "#{app_dir}/tmp/pids/puma.pid"
state_path "#{app_dir}/tmp/pids/puma.state"
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true
```

```ruby:deploy.rb
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"
```

```
$ bundle exec cap production deploy
```


### 5. アプリケーションログを確認

本番環境に戻りPumaを再起動し、socketファイルやpidファイルが生成されることを確認する。

```
$ ssh deploy@xx.xxx.xxx.xxx
$ cd /var/www/my-app-name/current

$ bundle exec pumactl start
```

ブラウザからアクセスし `shared/log/` 配下にRailsのアプリケーションログやPumaのログが落ちることを確認する。

```
$ tail -f shared/log/production.log
$ tail -f shared/log/puma.stderr.log
$ tail -f shared/log/puma.stdout.log
```


### 6. データベースを設定

MySQLにrootでログインし新規ユーザを作成する。作成するユーザ名は `database.yaml` で設定した名前と揃える。

```
$ mysql -u root

mysql> GRANT ALL PRIVILEGES ON `my-app-name_production`.* TO `my-app-name`@localhost IDENTIFIED BY 'my-app-password';
```

初期設定では文字コードがlatin1になっているのでutf8に変更していく。`character_set_filesystem ` と `character_sets_dir` 以外をすべて `utf8` に設定する。

```
mysql> show variables like "chara%";
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | latin1                     |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | latin1                     |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
```

設定ファイルに以下の2行を追記して再起動する。

```/etc/my.cnf
[mysqld]
character-set-server=utf8

[client]
default-character-set=utf8
```

```
$ sudo /etc/init.d/mysqld restart
```

文字コードが正しくutf8に設定されていることを確認する。問題なければ作成したユーザで `rails db:create` および `rails db:migrate` をおこなっておく。

```
$ bundle exec rails db:create RAILS_ENV=production
$ bundle exec rails db:migrate RAILS_ENV=production
```


### 7. アプリケーションサーバを起動

Pumaをproductionモードで起動し表示確認をおこなう。pumaコマンドで起動する場合は `-C` オプションで設定ファイルを指定しないと読みにいかないので気をつける。また、今回はNginx側の設定でsocketファイル読みにいくように設定してるため `-p` オプションでポートを指定してしまうとsocketファイルがlistenされなくなるため気をつける。

```
$ bundle exec puma -t 5:5 -e production -C config/puma.rb

# もしくは

$ bundle exec pumactl start
```


### 8. デプロイタスクを修正（ローカル）

デプロイコマンド実行時に関連するタスクを実行するように以下をデプロイタスクに追加する。

1) デプロイ時にbundlerを起動

```ruby:Gemfile
group :development do
  gem 'capistrano-bundler'
end
```

```ruby:Capfile
require "capistrano/bundler"
```

2) デプロイ時にmigrationを実行

```ruby:Capfile
require "capistrano/rails/migrations"
```

3) デプロイ時にPumaを再起動

```ruby:Gemfile
group :development do
  gem 'capistrano3-puma'
end
```

```ruby:Capfile
require 'capistrano/puma'
install_plugin Capistrano::Puma
```

capistrano3-pumaを利用するための設定ファイル追加する。 `puma:config` コマンドでローカルで自動生成した設定ファイルをリモートサーバにアップロードできる。

```
$ bundle exec cap production puma:config
```

設定ファイルはshared配下に配置され、下記のようなコマンドでローカルからPumaの起動、停止、再起動ができるようになる。

```
$ bundle exec cap production puma:start
$ bundle exec cap production puma:stop
$ bundle exec cap production puma:restart
$ bundle exec cap production puma:phased-restart
```

デプロイタスクにPumaの再起動タスクを組み込む。

```ruby:config/deploy.rb
namespace :deploy do
  desc "Make sure local git is in sync with remote."
  task :confirm do
    on roles(:app) do
      puts "This stage is '#{fetch(:stage)}'. Deploying branch is '#{fetch(:branch)}'."
      puts 'Are you sure? [y/n]'
      ask :answer, 'n'
      if fetch(:answer) != 'y'
        puts 'deploy stopped'
        exit
      end
    end
  end

  desc "Initial Deploy"
  task :initial do
    on roles(:app) do
      before 'deploy:restart', 'puma:start'
      invoke 'deploy'
    end
  end

  desc "Restart Application"
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke 'puma:restart'
    end
  end

  before :starting, :confirm
end
```

デプロイ実行時にPumaが正しく再起動されていることを確認する。

```
$ bundle exec cap production deploy
```


## おまけ

エラーログをちゃんと読めばだいたいすぐに解決すると思うが、今回も念のため想定されるエラーと解決方法を載せておく。

**1. ディレクトリにアクセスする権限がない**

```
directory index of "/var/www/my-app-name/current/public/" is forbidden, client: yyy.yyy.yy.yyy, server: localhost, request: "GET / HTTP/1.1", host: "xx.xxx.xxx.xxx"
```

→ locationディレクティブの中にで `try_files` の記述が足りない。

```/etc/nginx/nginx.conf
location / {
    try_files $uri $uri/index.html $uri.html @webapp;
}

location @webapp {
    proxy_read_timeout 300;
    proxy_connect_timeout 300;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://puma;
}
```

**2. socketファイルが正しく読めない**

```
connect() to unix:///my-app-name/path/to/sockets/puma.sock failed (2: No such file or directory) while connecting to upstream, client: yyy.yyy.yy.yyy, server: localhost, request: "GET / HTTP/1.1", upstream: "http://unix:///my-app-name/path/to/sockets/puma.sock:/", host: "xx.xxx.xxx.xxx"
```

→ socketファイルが正しく生成されていない。もしくは生成されているが名前やパスが正しくない。

```ruby:/var/www/my-app-name/current/config/puma.rb
app_dir = File.expand_path("../..", __FILE__)
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
```

```ruby:/var/www/my-app-name/current/config/deploy.rb
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"
```

```/etc/nginx/nginx.conf
upstream puma {
    server unix:///var/www/my-app-name/shared/tmp/sockets/puma.sock;
}

location @webapp {
    proxy_pass http://puma;
}
```

**3. socketファイルで通信ができない**

```
connect() to unix:///var/www/my-app-name/shared/tmp/sockets/puma.sock failed (111: Connection refused) while connecting to upstream, client: yyy.yyy.yy.yyy, server: localhost, request: "GET / HTTP/1.1", upstream: "http://unix:///var/www/my-app-name/shared/tmp/sockets/puma.sock:/", host: "xx.xxx.xxx.xxx"
```

→ Nginxがsocketファイルで通信できなかったよと言っている。以下のようなコマンドでPumaを起動する際 `-p` オプションでポートを指定すると、デフォルトでsocketファイルをlistenしない仕様になっている。今回のようにsocketファイルで通信をおこないたい場合にはポートの指定はしない。

```
# NG ->
$ bundle exec puma -t 5:5 -p 3000 -e production -C config/puma.rb

Puma starting in single mode...
* Version 3.9.1 (ruby 2.4.0-p0), codename: Private Caller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
```

portの指定を外すと正しくsocketファイルをlistenしてくれる。

```
# OK ->
$ bundle exec puma -t 5:5 -e production -C config/puma.rb

Puma starting in single mode...
* Version 3.9.1 (ruby 2.4.0-p0), codename: Private Caller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
* Listening on unix:///var/www/my-app-name/releases/20170620162839/tmp/sockets/puma.sock
Use Ctrl-C to stop
```

```
# OK ->
$ bundle exec pumactl start

Puma starting in single mode...
* Version 3.9.1 (ruby 2.4.0-p0), codename: Private Caller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
* Listening on unix:///var/www/my-app-name/releases/20170620162839/tmp/sockets/puma.sock
Use Ctrl-C to stop
```

**4. MySQLのsocketファイルが読めない** 

```
#<Mysql2::Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)>
```

→ MySQLのsocketファイルが見つからないよと言っている。以下のコマンドでsocketファイルのパスを確認し `database.yaml` のproduction設定に追加する。

```
$ mysqladmin -u root version
```

```ruby:config/database.yaml
production:
  <<: *default
  socket: /var/lib/mysql/mysql.sock
```

もしくは、MySQLが正しく起動できているにも関わらずこのエラーが出る際にはPumaがdevelopmentモードで起動されており `database.yaml` のdevelopmentの設定を読みにいっている可能性があるため、正しくproductionモードで起動できているか確認する。

**5. secret_key_baseが設定されていない**

```
#<RuntimeError: Missing `secret_key_base` for 'production' environment, set this value in `config/secrets.yml`>
```

→ 環境変数が正しく読めていない。アプリケーションのルート直下で `$ rbenv vars` コマンドを実行し  が正しく動作しているか確認する。 `rbenv: no such command 'vars'` とおこられるときは正しくインストールできていない。以下のコマンドでrbenv-varsがインストールされ `/var/deploy/.rbenv/plugins` 配下に `rbenv-vars` ディレクトリが生成されることを確認する。

```
$ git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars
```

```
$ rbenv vars

# /var/www/my-app-name/current/.rbenv-vars
export DATABASE_PASSWORD=''
export SECRET_KEY_BASE=''
```


## 参考
- [インフラ初心者がNginx+PumaのRails5アプリケーションをCapistrano3でデプロイした話](http://qiita.com/nrk_baby/items/ce4780e800696beee332)
- [rails5 + puma + nginx で AWSテスト環境を構築してみる](http://sodex.hatenablog.com/entry/2017/02/05/141803)
- [EC2にSSH接続用のユーザーを作成する](http://qiita.com/nekonoprotocol/items/e1fc5c212bf6a7365f8f)
[EC2のSSH接続でPermission denied(publickey)と出た時の対処法](http://qiita.com/katsukii/items/c3df0653ac07d41fe03f#authorizedkeysfile)