RailsアプリをAWS EC2でデプロイするレシピ

# はじめに
Railsで個人アプリを作成後、AWSでデプロイする際に、やり方をいつも忘れてしまうので、ここに書き記しておきたい。
## 前提
- Rails 5.2以上を想定

# 1. EC2インスタンス作成・キーペア作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/46c16068-bbdd-1a8c-8045-27720066962e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/b30010b4-b098-3c61-6f9f-06284b4575c8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/6f07aa8f-d541-abe6-20d9-c9043bf8ffc3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/e827443c-f1ff-b194-288b-744fdc02406d.png)
キーペアは新規作成でも、既存のキーペアでもOK。
キーペアはスペースを含んではいけない。

# 2. Elastic IPとの紐付け
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/2835acb3-6c9d-51f1-0913-a1f335a3bfde.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/cd3ce577-da9d-0d21-07d2-e43974b21475.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/11cfe742-5ecd-d676-bb3b-be8923d9eaf4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/84a15ada-64e9-dc26-2f4f-13c742667914.png)

# 3. ポートを開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/120c7c56-83bc-df1c-ab36-99589840d8b5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/64398d9f-e8db-a217-c47e-b737f148feeb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/05e7ac45-9a78-1376-7197-340aa870bd5a.png)

タイプを「HTTP」、プロトコルを「TCP」、ポート範囲を「80」、送信元を「カスタム / 0.0.0.0/0, ::/0」に設定します。

HTTP接続できるように下記も解放

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/3b2ad140-df4c-dda0-d96c-50380f3ea7ab.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/6f58f2e1-dbc6-43e3-44b1-c99914886faf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/3622b871-3d79-f142-bdc3-ef840fb81cd8.png)



# 4. EC2-userでログイン
```console:ローカル
$ cd ~

$ mkdir ~/.ssh
# .sshというディレクトリを作成
# File existsとエラーが表示されたとしても、.sshディレクトリは存在しているということなので、そのまま進みましょう。

$ mv Downloads/ダウンロードした鍵の名前.pem .ssh/
# mvコマンドで、ダウンロードしたpemファイルを、ダウンロードディレクトリから、.sshディレクトリに移動します。

$ cd .ssh/

$ ls
# pemファイルが存在するか確認しましょう

$ chmod 600 ダウンロードした鍵の名前.pem

$ ssh -i ダウンロードした鍵の名前.pem ec2-user@作成したEC2インスタンスと紐付けたElastic IP
#（例えばElastic IPが123.456.789であれば、ssh -i ダウンロードした鍵の名前.pem ec2-user@123.456.789 というコマンドになります）
#（ダウンロードした鍵を用いて、ec2-userとしてログイン）
```
# 5. 必要なパッケージ・Rubyのインストール
```terminal:サーバー
[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y update
[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install git make gcc-c++ patch libyaml-devel libffi-devel libicu-devel zlib-devel readline-devel libxml2-devel libxslt-devel ImageMagick ImageMagick-devel openssl-devel libcurl libcurl-devel curl
[ec2-user@ip-172-31-25-189 ~]$ sudo curl -sL https://rpm.nodesource.com/setup_6.x | sudo bash -
[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install nodejs
[ec2-user@ip-172-31-25-189 ~]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 
#パスを通す
[ec2-user@ip-172-31-25-189 ~]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile 
#rbenvを呼び出すための記述
[ec2-user@ip-172-31-25-189 ~]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
#.bash_profileの読み込み
[ec2-user@ip-172-31-25-189 ~]$ source .bash_profile
#ruby-buildのインストール
[ec2-user@ip-172-31-25-189 ~]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
#rehashを行う
[ec2-user@ip-172-31-25-189 ~]$ rbenv rehash  
[ec2-user@ip-172-31-25-189 ~]$ rbenv install 2.5.1
[ec2-user@ip-172-31-25-189 ~]$ rbenv global 2.5.1
[ec2-user@ip-172-31-25-189 ~]$ rbenv rehash  #rehashを行う
[ec2-user@ip-172-31-25-189 ~]$ ruby -v # バージョンを確認

# MySQLのインストール
[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install mysql56-server mysql56-devel mysql56
[ec2-user@ip-172-31-25-189 ~]$ sudo service mysqld start
[ec2-user@ip-172-31-25-189 ~]$ sudo /usr/libexec/mysql56/mysqladmin -u root password 'ここを設定したいパスワード'

# MySQLに設定したPWで入れるか確認
[ec2-user@ip-172-31-25-189 ~]$ mysql -u root -p
```
# 6. GithubにSSH keyを登録
```terminal:EC2サーバ
[ec2-user@ip-172-31-23-189 ~]$ ssh-keygen -t rsa -b 4096
```
途中で passphrase など３段階ほど入力を求められることがありますが、全て何も入力せずにEnterキーで進んでください。

```terminal:EC2サーバ
[ec2-user@ip-172-31-23-189 ~]$ cat ~/.ssh/id_rsa.pub
```
catで表示させた公開鍵を、Githubにアクセスして登録していきます
https://github.com/settings/keys

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/d53199ca-70e7-4d8a-1913-af3e1b5f9ebc.png)

Githubに鍵を登録できたら、SSH接続できるか以下のコマンドで確認

```terminal:EC2サーバ
[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com
```

# 7. Capistrano準備

```ruby:Gemfile
group :development, :test do
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
end
```
```terminal:ローカル
$ bundle install
$ bundle exec cap install
```
```ruby:Capfile編集
require "capistrano/setup"
require "capistrano/deploy"
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'

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

```
```ruby:config/deploy/production.rb
server '<用意したElastic IP>', user: 'ec2-user', roles: %w{app db web}
```
```ruby:config/deploy.rb
# config valid only for current version of Capistrano
# capistranoのバージョンを記載。固定のバージョンを利用し続け、バージョン変更によるトラブルを防止する
lock '<Capistranoのバージョン>'

# Capistranoのログの表示に利用する
set :application, '<自身のアプリケーション名>'

# どのリポジトリからアプリをpullするかを指定する
set :repo_url,  'git@github.com:<Githubのユーザー名>/<レポジトリ名>.git'

# バージョンが変わっても共通で参照するディレクトリを指定
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, '<このアプリで使用しているrubyのバージョン>' 

# どの公開鍵を利用してデプロイするか
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['<ローカルPCのEC2インスタンスのSSH鍵(pem)へのパス（例：~/.ssh/key_pem.pem）>'] 

# プロセス番号を記載したファイルの場所
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }

# Unicornの設定ファイルの場所
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

set :linked_files, %w{ config/master.key }

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:stop'
    invoke 'unicorn:start'
  end

  desc 'upload master.key'
  task :upload do
    on roles(:app) do |host|
      if test "[ ! -d #{shared_path}/config ]"
        execute "mkdir -p #{shared_path}/config"
      end
      upload!('config/master.key', "#{shared_path}/config/master.key")
    end
  end
  before :starting, 'deploy:upload'
  after :finishing, 'deploy:cleanup'
end
```

```ruby:config/database.yml
production:
  <<: *default
  database: application_name_production
  username: root
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: /var/lib/mysql/mysql.sock
```


# 8.アプリケーションサーバー（unicorn）設定
```ruby:Gemfile
group :production do
  gem 'unicorn', '5.4.1'
end
```
```terminal:ローカル
bundle install
```
```ruby:config/unicorn.rb
app_path = File.expand_path('../../../', __FILE__)

worker_processes 1
# currentを指定
working_directory "#{app_path}/current"

# それぞれ、sharedの中を参照するよう変更
listen "#{app_path}/shared/tmp/sockets/unicorn.sock"
pid "#{app_path}/shared/tmp/pids/unicorn.pid"
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"

timeout 60

#以下は応用的な設定なので説明は割愛

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_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 => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end
```

```ruby:config/environments/production.rb（修正後）
# config.assets.js_compressor = :uglifier
```

# 9. Githubからコードをクローン
まずはGitHub Desktopからコミットしてプッシュしましょう。この時必ず、masterブランチで行うようにしてください。

```terminal:ec2サーバ
#mkdirコマンドで新たにディレクトリを作成
[ec2-user@ip-172-31-23-189 ~]$ sudo mkdir /var/www/
#作成したwwwディレクトリの権限をec2-userに変更
[ec2-user@ip-172-31-23-189 ~]$ sudo chown ec2-user /var/www/
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630397/4e50b555-f560-0357-cbea-36606a34f1bc.png)

```terminal:ec2サーバ
[ec2-user@ip-172-31-23-189 ~]$ cd /var/www/
[ec2-user@ip-172-31-23-189 www]$ git clone https://github.com/<ユーザー名>/<リポジトリ名>.git
```


# 10. Nginx設定
```terminal:ec2サーバ
# インストール
[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install nginx

# 設定ファイル起動
[ec2-user@ip-172-31-25-189 ~]$ sudo vim /etc/nginx/conf.d/rails.conf
```

```vim:rails.conf
upstream app_server {
  # sharedの中を参照するよう変更
  server unix:/var/www/<アプリケーション名>/shared/tmp/sockets/unicorn.sock;
}

server {
  listen 80;
  server_name <Elastic IPを記入>;

# クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

  # currentの中を参照するよう変更
  root /var/www/<アプリケーション名>/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    # currentの中を参照するよう変更
    root   /var/www/<アプリケーション名>/current/public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}
```
```terminal:ec2サーバ
# nginxの権限を変更
[ec2-user@ip-172-31-25-189 ~]$ cd /var/lib
[ec2-user@ip-172-31-25-189 lib]$ sudo chmod -R 775 nginx  
[ec2-user@ip-172-31-25-189 lib]$ cd ~
[ec2-user@ip-172-31-25-189 ~]$ sudo service nginx restart
# nginx再起動
[ec2-user@ip-172-31-25-189 ~]$ sudo service nginx reload
[ec2-user@ip-172-31-25-189 ~]$ sudo service nginx restart
```

# 11. EC2の能力拡張/bundler install
```terminal:ec2サーバ
[ec2-user@ip-172-31-25-189 ~]$ cd
[ec2-user@ip-172-31-25-189 ~]$ sudo dd if=/dev/zero of=/swapfile1 bs=1M count=512
[ec2-user@ip-172-31-25-189 ~]$ sudo chmod 600 /swapfile1
[ec2-user@ip-172-31-25-189 ~]$ sudo mkswap /swapfile1
[ec2-user@ip-172-31-25-189 ~]$ sudo swapon /swapfile1
[ec2-user@ip-172-31-25-189 ~]$ sudo sh -c 'echo "/swapfile1  none        swap    sw              0   0" >> /etc/fstab'

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ gem install bundler -v x.x.x
# ローカルで確認したbundlerのバージョンを導入する
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ bundle install
# 上記コマンドは、数分以上かかる場合もあります。
```

# 12. 環境変数の設定

```terminal:ec2サーバ
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rake secret
69619d9a75b78f2e1c87ec5e07541b42f23efeb6a54e97da3723de06fe74af29d5718adff77d2b04b2805d3a1e143fa61baacfbf4ca2c6fcc608cff8d5a28e8d
# 文字列をコピー
```
```terminal:ec2サーバ
[ec2-user@ip-172-31-23-189 ~]$ sudo vim /etc/environment
```
```vim:/etc/environment
DATABASE_PASSWORD='MySQLのrootユーザーのパスワード'
SECRET_KEY_BASE='先程コピーしたsecret_key_base'
```
書き込みができたら esc(エスケープキー)を押下後、:wqと入力して内容を保存します。保存できたら環境変数を適用するために一旦ログアウトします。

```terminal:ローカル
$ ssh -i [ダウンロードした鍵の名前].pem ec2-user@[作成したEC2インスタンスと紐付けたElastic IP]
（ダウンロードした鍵を用いて、ec2-userとしてログイン）
```
```terminal:ec2サーバ
[ec2-user@ip-172-31-23-189 ~]$ env | grep SECRET_KEY_BASE
SECRET_KEY_BASE='secret_key_base'

[ec2-user@ip-172-31-23-189 ~]$ env | grep DATABASE_PASSWORD
DATABASE_PASSWORD='MySQLのrootユーザーのパスワード'
```

```terminal:ec2サーバ
# 本番環境データベース作成
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ cd /var/www/アプリ名/releases
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ ll 
# 日付を表す数字の名前のフォルダが表示される 例：20200218063515
cd 一番下の数字 #=> 例 cd 20200218063515
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rails db:create RAILS_ENV=production
# => Created database '<データベース名>'
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rails db:migrate RAILS_ENV=production
```
# 13. 自動デプロイ
ローカルでの変更を全てmasterにpush

```terminal:ローカル
# アプリケーションのディレクトリで実行する
$ bundle exec cap production deploy
```

エラーログの確認方法
サーバー側で
`/var/www/<レポジトリ名>/current/log/unicorn.stderr.log`をlessまたはcatコマンドで確認しエラーが出ていないか確認する（下に行くほど最新のログです。時刻表記がUTCであることに注意してください）