Rails 中級者が Capistrano3 で Rails アプリを AWS EC2 にデプロイするためにがんばったこと（2017年版）

## はじめに

http://qiita.com/kizashi1122/items/b54b0f1033ff5d7fbd66 に2014年に書いた記事は今でもまあまあ見られているので、更新してみようと思います。今回は前回の自分自身の記事をみて試したところ、ファイルのパスがわかりにくいとか作業順がアレだなとか思うところがあったので、そういった点も修正できればと思います。

また前回の記事では自分のローカル環境から（capistrano経由で）デプロイすることができるようになるというところで終わっていましたが、今回は `master` を `git push` すれば（`master` にマージされたら）、自動的にデプロイされるところまでやってみたいと思います。

## AWS のサービスあれこれ

AWS (Amazon Web Services) には

- CodeDeploy
- Elastic Beanstalk
- AWS CloudFormation
- AWS OpsWorks

など、微妙に似たようなサービスが多いです。

本当はこれらのサービスを組み合わせて環境構築したりデプロイするのがスジなんだろうとは思いますが、まとまった情報もなく、いくつか試したけどうまくいかなかったため、前回の記事の更新版ということになりますが、手で環境構築をするバージョンを以下に書いていきます。

## 目標

- Github 上にある Rails アプリを、
- （今のところ）無料のCI/CDサービスである Wercker ( https://app.wercker.com/ )を利用して、
- master に push されたら、自動テストが走るようにして、
- さらにテストが通ったらデプロイされるようにする

ところまでを書いてみたいと思います。Web(AP)サーバーは1台という前提です。

環境構築時はローカルにインストールした capistrano からデプロイのテストします。
ただ最終的には Wercker からデプロイできるようになるので、複数人で開発もしやすくなるはずです。
やはり、認証などが入ってくるアプリだと、SSL必須になると思うので、LetsEncrypt よりSSL証明書を取得していますが、詳細はここでは省略します。

## 前提条件

下記のミドルウェアはインストールされている前提とします。

- nginx (nginx-1.10.2-1.30.amzn1.x86_64)
- postgresql (postgresql95-9.5.4-1.71.amzn1.x86_64)

カッコ内は私が実施にインストールしたバージョンです。
別に今回の設定にpostgresqlが依存しているわけではないので、mysql が好きな人はそれでもいいし、RDS を使っている人は、`database.yml` にそのホストを指定してもらえればいいと思います。

## Ruby インストール

以下の作業は **EC2 上での作業** となります。

Ruby がないと Rails が動きませんのでなにはともあれ Ruby をインストールします。
Ruby のインストールも前提条件にいれておいてもよかったのですが、手元にメモがあるので書いておきます。
色々やり方はあると思いますが、手慣れた rbenv を使います。

以下の作業は **EC2 上での作業** となります。

```shell-session
$  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
$  git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$  rbenv versions
$  sudo yum install -y readline-devel
$  rbenv install 2.3.3
$  rbenv versions
$  rbenv global 2.3.3
$  rbenv rehash
$  gem install bundler
```

## nginx 設定ファイル修正

設定したい内容としては、以下のとおりです。

- http にアクセスがあったら、https にリダイレクトする
- unicorn 連携 (rails 連携)
- SSL設定

nginx に関する設定なので、もちろん **EC2 上での作業** となります。

Unicorn とは同一サーバ上で動かしているため、Unix Domain Socket で接続します。
まだ unicorn の設定もできてないのですが、先に設定ファイルを編集しておきます。
設定を反映する再起動はすべての設定が終わったらおこないます。

```/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;
    server_tokens off;                  # 1
    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 hoge.example.com {         # 2
        server unix:/tmp/unicorn.sock;  # 3
    }
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        return 301 https://$host$request_uri;       # 4
    }
    server {
        listen       443 ssl http2 default_server;
        listen       [::]:443 ssl http2 default_server;
        server_name  _;
        root   /home/ec2-user/deploy/current/public; # 5
        location @myapp {
	    proxy_set_header        X-Forwarded-Proto https;
	    # needed to forward user's IP address to rails
	    proxy_http_version                  1.1;
	    proxy_set_header  X-Real-IP         $remote_addr;
	    proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
	    proxy_set_header  Host              $http_host;
	    proxy_set_header  Upgrade           $http_upgrade;
	    proxy_set_header  Connection        "upgrade";
	    proxy_set_header  X-Queue-Start     't=${msec}000';
	    proxy_redirect off;
	    proxy_max_temp_file_size 0;
	    proxy_pass http://hoge.example.com;       # 6
        }
        location ~ ^/(images|assets|javascripts|stylesheets)/ {
          try_files  $uri $uri/index.html /last_assets/$uri /last_assets/$uri.html @myapp;
          expires 10y;
        }
        location / {
          try_files  $uri $uri/index.html $uri.html @myapp;
        }
	ssl_certificate "/etc/letsencrypt/live/hoge.example.com/fullchain.pem";   # 7
        ssl_certificate_key "/etc/letsencrypt/live/hoge.example.com/privkey.pem"; # 7
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP;
        ssl_prefer_server_ciphers on;
        include /etc/nginx/default.d/*.conf;
        error_page 404 /404.html;
            location = /40x.html {
        }
        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}
```

コメントにつけた番号について注意点をあげておきます。

| 番号 | 説明    | 
|:----|:--------|
| 1 | セキュリティのためです。今回の設定とは無関係です。|
| 2 | ちゃんとFQDNにしておかないとアプリでフルパスを取得するとき失敗してしまいました。|
| 3 | socket ファイルの指定。unicorn の設定と合わせる必要あり。|
| 4 | http にアクセスがあったら、https にリダイレクトする設定。|
| 5 | コンパイル後の css, js が生成されるパスを指定しまる。後述の `config/deploy.rb` に書いているデプロイ先と関係しています。つまりアプリは `/home/ec2-user/deploy/current` に配備されてることになります。|
| 6 | http:// より後ろは `#2` と合わせておいてください。|
| 7 | LetsEncrypt により生成された証明書と秘密鍵を指定します。|


## Capistrano 3 のインストール

以下は、これらは `git@github.com:<github-account>/<certain-repository>.git` にある Rails アプリ上のファイルになります。
まずは **ローカルでの作業** になります。

### Gemfile

Gemfile に以下を追加しましょう。

```rb:Gemfile
gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano-bundler'           
gem 'capistrano-rbenv'
gem 'capistrano3-unicorn'
gem 'unicorn'
```

前回の記事では、 `capistrano3-unicorn` は使わないと書きましたが今回は使っています。

次に bundle install コマンドによって capistrano 関連の gem をインストールします。
インストールが成功したら `bundle exec cap install` とコマンド実行します。

```shell-session
$ bundle install
$ bundle exec cap install
```

すると関連するフォルダやファイルが生成されます。

```
Rails.root
├── Capfile
├── config
│   ├── deploy
│   │   ├── production.rb
│   │   └── staging.rb
│   └── deploy.rb
└── lib
    └── capistrano
            └── tasks
```

### Capfile

Capfile については `# ADDED` のみ追加しています。

```rb:Capfile
require "capistrano/setup"
require "capistrano/deploy"
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

require "capistrano/rbenv"
set :rbenv_type, :user         # ADDED
set :rbenv_ruby, '2.3.3'       # ADDED

require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
require 'capistrano3/unicorn'

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

### config/deploy.rb

次に、deploy.rb です。ここは重要です。
実際にデプロイする先の情報が必要になります。とはいっても`github` のレポジトリurlやデプロイする先のディレクトリなどで、ホスト情報はまだでてきません。ホスト情報は後述する `config/deploy/production.rb` に記述します。そりゃ環境によってホストは分かれるよねということです。

```rb:config/deploy.rb
lock "3.8.0"
set :application, "<application-name>"                                      # 1
set :repo_url, "git@github.com:<github-account>/<certain-repository>.git"   # 2
set :deploy_to, "/home/ec2-user/deploy"                                     # 3
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"
set :keep_releases, 5
set :unicorn_config_path, -> { File.join(current_path, "config", "unicorn.rb") } #4
set :unicorn_rack_env, 'production'
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end
```

| 番号 | 説明 | 
|:----|:-----|
| 1 | アプリ名です。 |
| 2 | デプロイ対象のアプリです。git のURLパスです。|
| 3 | サーバ側のデプロイ先のディレクトリです。/var/www などのディレクトリを使っているサンプルが多かったのですが、パーミッションの問題もあるだろうと思い、ssh ユーザがアクセスできるように、ホームディレクトリ以下にしました。このディレクトリは先に mkdir しています。|
| 4 | 後述の unicorn.rb の場所を明示的に指定します。デフォルトは `config/unicorn/production.rb` のようです。こちらのほうがよいという人は本設定はなくてよいかも。|


デプロイするソースは master から取得する前提なので明示的に指定はしていません。たとえばウチは常に `develop` ブランチをデプロイしたいのだという場合は

```rb:config/deploy.rb
set :branch, 'develop'
```

と書けばよいかと思いますが、デプロイ時の `cap` コマンドに外部から渡すことも可能かと思います。

### config/deploy/production.rb

production に接続する情報を記入します。具体的には ssh で接続するため鍵情報も必要になります。

```rb:config/deploy/production.rb
server "hoge.example.com", user: "ec2-user", roles: %w{app db web}
set :ssh_options, {
      keys: [File.expand_path('~/.ssh/aws.pem')],
    }
set :stage, :production
set :rails_env, 'production'
set :unicorn_rack_env, 'production'
```

**ローカルの** `~/.ssh/aws.pem` に `hoge.example.com` のEC2サーバーにアクセスできる鍵を用意しておいてください。

この鍵ファイルのパスは、後から Wercker でデプロイする際も踏襲する必要があることに留意ください。

## Unicorn

AP サーバーは慣れていることもあり unicorn を使います。前の記事と同じですね。
すでに、Gemfile に `gem 'unicorn'` と書いて `bundle install` しているのでインストールは終わっています。

あと、前回は、`unicorn.cap` みたいなファイルを頑張って作ったような気がしますが、`Capfile` に

```rb:Capfile
require 'capistrano3/unicorn'
```

があれば、自分でタスクを作る必要はありません。なお、以前は `*.cap` でしたが、今回は、

```rb:Capfile
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
```

とあるように、自分で `unicorn.cap` を作っても動かないので注意してください。拡張子が変わってますよという意味です。

### config/unicorn.rb

とりあえず、こうしてます。このファイルの場所は `config/deploy.rb` の `unicorn_config_path` と一致させる必要があります。

```rb:config/unicorn.rb
worker_processes 3

app_path = "/home/ec2-user/deploy"

listen '/tmp/unicorn.sock' #1
pid "#{app_path}/shared/tmp/pids/unicorn.pid"

stderr_path File.expand_path('unicorn.err.log', File.dirname(__FILE__) + '/../log')
stdout_path File.expand_path('unicorn.log', File.dirname(__FILE__) + '/../log')
preload_app true

# use correct Gemfile on restarts
before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = "#{app_path}/current/Gemfile"
end

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  # Before forking, kill the master process that belongs to the .oldbin PID.
  # This enables 0 downtime deploys.
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end
```


| 番号 | 説明 | 
|:----|:-----|
| 1 | nginx.conf に記述する unicorn の socket ファイルのパスと合わせてください。|


さてこれで設定ファイルはひととおり編集できました。

## ローカルからデプロイ

あとは、`git push` して、以下のデプロイコマンドを打ってください。

```shell-session
$ bundle exec cap production deploy
```

これで、ちゃんとEC2のサーバーを覗いたら、 `/home/ec2-user/deploy/current` とか出来ていて、unicorn のプロセスが立ち上がっていたらバンザイ。

```shell-session
$ sudo /etc/init.d/nginx restart
```

して、外部からつながったら、さらにバンバンザイ。

## wercker からデプロイ

事前条件として

- wercker には github アカウントでサインアップしておく。
- wercker の `Environment` タブより、キーペアを作っておく（`+ Generate SSH Keys` リンクより）
    - ここで発行された公開鍵は予め手動で、EC2 サーバーの `ec2-user` の `~/.ssh/authorized_keys` に追加しておく
- `master` が push されたら deploy が走るように workflow を定義しておく

Git のリポジトリのルートに以下のような `wercker.yml` を配置して、`git push` する。

```yaml:wercker.yml
box: ruby:2.3

services:
   - id: postgres
     env:
       POSTGRES_PASSWORD: test
build:
    steps:
        # A step that executes `bundle install` command
        - bundle-install

        # A step that prepares the database.yml using the database in services
        - rails-database-yml:
            service: postgresql-docker

        # Add more steps here:
        - script:
            name: setup DB (schema load)
            code: bundle exec rake db:schema:load RAILS_ENV=test
        - script:
            name: setup DB (seed sample)
            code: bundle exec rake db:seed:sample RAILS_ENV=test
        - script:
            name: rspec
            code: bundle exec rspec

deploy:
    steps:
        - script:
            name: make .ssh directory
            code: mkdir -p "$HOME/.ssh"
        - create-file:
            name: write ssh key
            filename: $HOME/.ssh/aws.pem # ここが `config/deploy/production.rb` に定義しているパスと同じになること
            overwrite: true
            hide-from-log: true
            content: $WERCKER_SSH_KEY_PRIVATE
        - script:
            name: set permissions for ssh key
            code: chmod 0400 $HOME/.ssh/aws.pem # ここが `config/deploy/production.rb` に定義しているパスと同じになること
        - script:
            name: add ssh-agent
            code: |
                eval `ssh-agent`
                ssh-add $HOME/.ssh/aws.pem # ここが `config/deploy/production.rb` に定義しているパスと同じになること
        - bundle-install
        - cap:
            stage: $WERCKER_DEPLOYTARGET_NAME

```

たぶんこれで `master` に push されるたびにデプロイが走るようになるはず。