RailsアプリをNginx+UnicornなAWSにCapistranoでデプロイ

Special Thanks

追記@2016/04/29

最近は環境やconfファイルのsetupはitamaeでやるようになりました。めっちゃ楽ですitamae。。

goo.gl

totutotu.hatenablog.com

PostgreSQLの設定は、以下を参考にしていつも自分でやってます。

totutotu.hatenablog.com

手順

手順をまとめると、以下のようになります。

  1. 前準備
  2. nginxの設定
  3. 設定ファイルの用意
  4. 権限変更
  5. Capistrano設定
  6. Unicorn設定
  7. ssh設定
  8. デプロイ

myAppの部分は適宜読み替えてやってください。また、設定ファイルのうち編集する必要がある場所にTODOとコメントを入れてます。

ローカル

1の前準備はローカルで行ってください。

1. 前準備

公開鍵をサーバーに送信しておきます。

$ scp -i ~/.ssh/hogehoge.pem ~/.ssh/id_rsa.pub ec2-user@ec2_server:~

サーバー

ここからサーバー上での作業です。sshで接続しておいてください。

2. nginx

今回作成するアプリ用にnginxの設定ファイルを作成します。パスはetc/nginx/conf.d/myapp.confみたいな感じです。

デフォルトで作成されてるnginx.confconf.d以下の*.confを全て自動でincludeする設定になってます。

upstream unicorn_server {
  server unix:/tmp/unicorn.sock
  fail_timeout=0;
}
 
server {
  listen 80;
  client_max_body_size 4G;
  server_name example.com; # TODO
 
  keepalive_timeout 5;
 
  root /var/www/myApp/current/public; # TODO
  access_log /var/log/nginx/myApp_access.log; # TODO
  error_log /var/log/nginx/myApp_error.log; # TODO
 
  error_page 500 502 503 504 /500.html;
 
  try_files $uri/index.html $uri @unicorn;
 
  location @unicorn {
    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_pass http://unicorn_server;
  }
 
  location ~ ^/assets/ {
    root /var/www/myApp/current/public; # TODO
  }
 
  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /var/www/myApp/current/public; # TODO
  }
}

3. 設定ファイルの用意

gitでignoreして管理していないsecrets.ymlとかdatabase.ymlとかのファイルがあればここで準備しておきます。

自分は/var/www/myApp/shared以下に.envとかdatabse.ymlを突っ込んでおいて、Capistranoのタスクでデプロイのフローでcurrentにcpしてくる設定にしています。Capistranoの設定のところでやります!

4. 権限変更

ec2-userの/var/wwwに対する権限を変更。書き込みできるように。

$ sudo chown -R ec2-user /var/www

ローカル

こっから再びローカルでの作業です。

5. Capistrano

Gemfileに以下のgemを加えます。

# Gemfile
group :production, :staging do
  gem 'unicorn'
end
 
group :development do
  gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano3-unicorn'
end

$ bundle installで反映しておく。

次に、Capistranoで必要なファイル群を生成します。Capistranoはそのためのinstallコマンドを用意してくれてますー。

▶ bundle exec cap install
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を以下のように書きました。

# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'

# Include 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
#   https://github.com/capistrano/passenger
#
# require 'capistrano/rvm'
require 'capistrano/rbenv'
set :rbenv_type, :user
set :rbenv_ruby, '2.2.1' # TODO: Change Ruby version
require 'capistrano/rails'
# require 'capistrano/chruby'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
# require 'capistrano/passenger'
require 'capistrano/bundler'
require 'capistrano3/unicorn'

# set :linked_files, %w{config/secrets.yml config/database.yml}

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

config/deploy.rbは以下ように。

lock '3.4.0'

set :application, 'myapp' # TODO
set :repo_url, 'git@github.com:org/myapp.git' # TODO
set :deploy_to, '/var/www/myapp' # TODO
set :log_level, :debug

set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system public/assets}

# rbenv Ruby version
set :rbenv_ruby, '2.3.0' # TODO

# nokogiri はシステムライブラリを使うため bundle install にオプションを指定する
set :bundle_env_variables, { nokogiri_use_system_libraries: 1 }

namespace :deploy do
  desc 'restart application'
  task :restart do
    invoke 'unicorn:restart'
  end
end
after 'deploy:publishing', 'deploy:restart'

以下のようにCapistranoのタスクを作っておいて自動でshared以下の.envdatabase.ymlをcpしてくる的なことをするのも結構便利なのでオススメです。

# config/deploy.rb

# ...

namespace :deploy do
  desc "Copying database.yml"
  task :config_database do
    on roles(:all) do
      within release_path do
        db_config_file = "/var/www/myApp/shared/database.yml"
        execute :cp, "#{db_config_file} ./config/database.yml"
      end
    end
  end
  desc "Set Environment Values"
  task :set_env_values do
    on roles(:all) do
      within release_path do
        env_config = "/var/www/myApp/shared/.env"
        execute :cp, "#{env_config} ./.env"
      end
    end
  end
  desc "Restarting application"
  task :restart do
    invoke "unicorn:restart"
  end
end

before "deploy:updated", "deploy:config_database"
before "deploy:updated", "deploy:set_env_values"
after "deploy:publishing", "deploy:restart"

config/deploy/production.rbを編集。以下テンプレ。

# server-based syntax
# ======================
server 'xx.xx.xx.xx', user: 'ec2-user', roles: %w{web app db} # TODO

# role-based syntax
# ==================
role :app, %w{ec2-user@xx.xx.xx.xx} # TODO
role :web, %w{ec2-user@xx.xx.xx.xx} # TODO
role :db, %w{ec2-user@xx.xx.xx.xx} # TODO

# Configuration
# =============
set :stage, :production
set :unicorn_rack_env, "production"
set :branch, 'deployment/production' # TODO
set :rails_env, 'production'
set :migration_role, 'db'

# Custom SSH Options
# ==================
set :ssh_options, {
  keys: [File.expand_path('~/.ssh/sample.pem')], # TODO
  forward_agent: true,
  auth_methods: %w(publickey)
}

6. Unicorn設定

config/unicorn/production.rbを以下のようにします。

# production.rb
root = "/var/www/myApp/current" # TODO
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/tmp/unicorn.sock"
worker_processes 3
timeout 30

preload_app true

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = "#{root}/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 = "#{root}/tmp/pids/unicorn.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|
  # the following is *required* for Rails + "preload_app true",
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end

他にもstagingなどの環境ごとの設定ファイルを必要に応じてconfig/unicorn以下に作成してください。

7. ssh設定

ssh keyを生成してssh-agentに登録。

$ ssh-keygen -t rsa -C myapp-production
$ ssh-add ~/.ssh/myapp-production

ssh-agentとは、いろんなサーバーsshするときにパスフレーズを省略することができるOpenSSHの標準機能で、

ssh-agent が起動すると、 UNIX ドメインソケット(UNIX でプロセス間通信に使われる、ファイルシステムベースの通信経路)を作成し、その名前を環境変数 "SSH_AUTH_SOCK" に設定します。そして、 ssh コマンドなどが起動したときは、このソケット経由で通信を行い、秘密鍵が必要な認証の処理を ssh-agent に代行させます。シェルの環境変数は子プロセスからは変更できないため、代わりに新しいシェルを起動しているわけです。 http://webos-goodies.jp/archives/50672669.html

という仕組みになってるらしい。

GitHubにDeploy Keyを登録

GitHubで、先ほど生成した公開鍵をDeploy Keyとして登録します。

登録したら、初回だけsshでの接続確認が必要っぽいので通しておきます。

$ ssh -T git@github.com

既存のssh鍵の記憶が邪魔になるときは、以下で記憶を削除できます。

$ ssh-add -D

8. デプロイ

ということでデプロイ。

$ bundle exec cap production deploy

Unicornは以下のようにして起動できます。

$ bundle exec unicorn -D -c /var/www/myApp/current/config/unicorn/production.rb -E production

直面した問題

デプロイ作業中のエラーログです。上での設定ファイルのテンプレなどにはここでの修正も反映してまっす。

rbenvのパス問題

リモートで使うRubyのversionが指定できてなかった。capistrano/rbenvの設定方法を確認して指定する。

# config/deploy.rb
# rbenv Ruby version
set :rbenv_ruby, '2.2.2'

それでも見つからないと思ったら、どうやらrbenvのパスが違うみたい。 multistage 環境で capistrano-rbenv を使うときは rbenv_path の扱いに注意!とかも読んだのですが結局わからず。

パスがどうしても変わらないので...シンボリックリンクを貼るという荒技。完全によくない解決方法なのはわかってますが解決できませんでした...

$ ln -s /usr/local/rbenv/* ~/.rbenv

pg

bundle stdout: An error occurred while installing pg (0.18.2), and Bundler cannot continue

ひとまず$ gem isntall pg試してみる

'mkmf.rb can't find header files for ruby'

的なエラーが出てくる。ぐぐる

The first link you’ve posted is exactly your case: there is no ruby development environment installed. Development env is needed to compile ruby extensions, which are mostly written in C. http://stackoverflow.com/questions/20559255/error-while-installing-json-gem-mkmf-rb-cant-find-header-files-for-ruby

らしい。ということでRubyのdevelopment環境で走らせるためのライブラリをインストール。

$ sudo yum install ruby-devel

ちょっと進んだ、次のエラーは...

No pg_config... trying anyway. If building fails, please try again with...

pgのconfigファイルがないよと。

Install PostgreSQL on AWS EC2(Amazon Linux AMI 2013.03.1)を参考にPostgreSQLのインストール手順を見直してみる。

pgに関しては、後半になってこんなエラーも出た。

PG::ConnectionBad: could not connect to server: No such file or directory

Is the server running locally and accepting connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?

うん、PostgreSQLが走ってなかったみたいね。

PostgreSQLでは必ず最初に初期化処理することー。

$ sudo /sbin/service postgresql initdb

そして起動、停止、再起動は以下のようにすればok。

## 起動・停止・再起動
$ sudo /sbin/service postgresql start
Starting postgresql service:                               [  OK  ]
$ sudo /sbin/service postgresql stop
Stopping postgresql service:                               [  OK  ]
$ sudo /sbin/service postgresql restart
Stopping postgresql service:                               [  OK  ]
Starting postgresql service:                               [  OK  ]

ユーザー・DBの作成は以下のようにすればmigrationまでうまくいけました。

http://totutotu.hatenablog.com/entry/2015/08/30/PostgreSQL%E3%81%AF%E3%81%98%E3%82%81%E3%81%AE%E3%81%84%E3%81%A3%E3%81%BD

JSのRuntime

ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime.

指定してるブランチで反映するの忘れてた。Gemfileでtherubyracerを使うよう宣言しときます。

と思ったらまだエラー。nodejsをインストールしないといけなかった。

ところが普通にyumでインストールしようとするも見つからず。How to yum install Node.JS on Amazon Linuxを参考にこんな感じのコマンドでインストールします。

$ sudo yum install nodejs npm --enablerepo=epel

The option --enablerepo=epel causes yum to search for the packages in the EPEL repository.

ということでEPELのパッケージも検索してくれます。EPELは確かもっといろんなパッケージを置いてる場所だったよね。この文章が詳しくてよい。

EPEL (Extra Packages for Enterprise Linux) is open source and free community based repository project from Fedora team which provides 100% high quality add-on software packages for Linux distribution including RHEL (Red Hat Enterprise Linux), CentOS, and Scientific Linux. Epel project is not a part of RHEL/Cent OS but it is designed for major Linux distributions by providing lots of open source packages like networking, sys admin, programming, monitoring and so on. Most of the epel packages are maintained by Fedora repo.

Via http://www.tecmint.com/how-to-enable-epel-repository-for-rhel-centos-6-5/

Bundler

SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@54.65.96.201: bundle...bundle stdout: bundler: command not found: unicorn Install missing gem executables with bundle install

Bundlerは入ってるっぽい。

$ bundle -v
Bundler version 1.10.6

サーバーに接続して$ bundle install|grep unicornとかやってもどうやらインストールでいてない。ちゃんとGemfileみてみたら、そうだ、こっちはGitHubからfetchしてるから内容はリモートのHEADになるんだった。

あまりの凡ミスに悲しくなるが、一応つまずく人もいるかな(未来の自分ではないことを願う...笑)と思い書き留めておく。

secret_key

app error: Missing secret_token and secret_key_base for 'production' environment, set these values in config/secrets.yml (RuntimeError)

デプロイ後反映されないので、Railsapp/unicorn.rbを確認すると、secret_tokentとsecret_key_baseがないよと。

そうだった。環境変数から読み込む設定にしてたんだった。設定する。 

以下に従って環境変数を定義すれば大丈夫になりました。

http://stackoverflow.com/questions/23180650/how-to-solve-error-missing-secret-key-base-for-production-environment-on-h

ttyが必要

以下で解決しました。

sudo: sorry, you must have a tty to run sudo - Shell Tips!

Capistranoでsudo実行時にttyエラー