Ruby
Rails
nginx
Capistrano
puma

インフラ初心者がNginx+PumaのRails5アプリケーションをCapistrano3でデプロイした話

More than 1 year has passed since last update.

この記事はフロムスクラッチ Advent Calendar 2016の14日目の記事です。


プロローグ

上司(@idobee

 「明日からインフラチームで新しいアプリケーションのデプロイ周り整備してね。」

部下(@nrk_baby

 「はい、承知しました。」

こんなノリで現在新たに開発しているアプリケーションのデプロイ周りを整備することになりました。

ちなみに、筆者(@nrk_baby)自身はインフラ関連を専門的にやったこともなければNginxとApacheの違いも説明できませんし、PumaとUnicornの違いもよくわかっていない状態でした。


やったこと

Capistrano3を使ってRails5アプリケーションをAWSのEC2インスタンスにデプロイし、ブラウザからアクセスしてアプリケーションが動作しているところまで確認しました。

色々とググりながら進んだものの、PumaとRails5とCapistrano3の組み合わせの事例は結構少なく、「Puma Rails5 capistrano3」でググると、なんと約1500件しかヒットしません。

これ大丈夫か・・・?オーソドックスにUnicornで挑むべきだったんじゃないか・・・?と不安を抱えながらも進んで行きました。


構成


  • Ruby 2.3.1

  • Ruby on Rails 5.0.0.1

  • Puma 3.0

  • Capistrano 3.6.1 (12/10に3.7.0がリリースされたようですね)

デプロイ対象のサーバーはAWSのEC2インスタンスで、Nginxは既にインストール済みでした。


gemのインストール

以下のgemを今回の取り組みの中でインストールしました。


Gemfile

# Use Puma as the app server

gem 'puma', '~> 3.0'

# Use Capistrano for deployment
group :development do
gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano3-puma'
gem 'capistrano-rbenv'
gem 'capistrano-rbenv-vars'
gem 'capistrano-bundler'
end


capistrano-rbenv-varsはあまり使われていない印象でしたが、今回ハマった部分の解決にはピッタリでした。


Capistranoの設定ファイルの記述

Capfile, config/deploy.rb, config/deploy/production.rbを触っていくのですが、まず初めにこれらのファイルを格納するcapistranoディレクトリを作成しました。

今回は開発側の人間と別々に動いていたという事情もあり、このように別ディレクトリとすることで開発側の人間が混乱しないように気をつけました。

Capistranoには初期設定のコマンドがありますが、テンプレートファイルが作成されるだけですのであまり必要ないかと思います。

具体的には以下のようにファイルを追加していきました。

まず、Capfileを見ます。


myapp/capistrano/Capfile

require "capistrano/setup"

require "capistrano/deploy"

# add requires
require 'capistrano/bundler'
require 'capistrano/puma'
require 'capistrano/rbenv'
require 'capistrano/rbenv_vars'
require 'capistrano/rails/assets'
#require 'capistrano/rails/migrations'

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


今回はまだ画面側だけのモックに近い状態のアプリケーションでしたので、require 'capistrano/rails/migrations'はコメントアウトしています。

それ以外は特筆すべき点はないかと思います。

次に、config/deploy.rbを見ます。


myapp/capistrano/config/deploy.rb

# config valid only for current version of Capistrano

lock '3.6.1'

set :application, 'myapp'
set :repo_url, 'git@github.com:your-repository/myapp.git'

# Default branch is :master
set :branch, ENV['BRANCH'] || 'master'

# deployするときのUser名(サーバ上にこの名前のuserが存在しAccessできることが必要)
set :user, 'deploy'

set :puma_threds, [4, 16]
set :puma_workers, 0
set :pty, true
set :rbenv_ruby, '2.3.1'

# 必要に応じて、gitignoreしているファイルにLinkを貼る
set :linked_files, %w{.rbenv-vars}
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}

set :use_sudo, false
set :stage, :production
set :deploy_via, :remote_cache

# deploy先サーバにおく場所
set :deploy_to, "/var/www/#{fetch(:application)}"

# bundle
set :bundle_path, -> { shared_path.join('vendor/bundle') }

# Set Gemfile
# set :bundle_gemfile, "/var/www/myapp/current/Gemfile"

set :puma_bind, "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log, "#{release_path}/log/puma.access.log"
set :ssh_options, { forward_agent: true, user: fetch(:user), keys: %w(~/.ssh/id_rsa.pub), port: 22 }
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true # Change to false when not using ActiveRecord

set :keep_releases, 2

namespace :puma do
desc 'Create Directories for Puma Pids and Socket'
task :make_dirs do
on roles(:app) do
execute "mkdir #{shared_path}/tmp/sockets -p"
execute "mkdir #{shared_path}/tmp/pids -p"
end
end

before :start, :make_dirs
end

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

before :starting, :confirm
after :finishing, :compile_assets
after :finishing, :cleanup

end


流石にこのファイルは色々と弄りました。

ポイントとしては、.rbenv-varslinked_filesに指定したこと、Gemfileに関する設定をコメントアウトしたことの2点です。

その他は一般的な設定かと思います。

set :linked_files, %w{.rbenv-vars}

.rbenv-varsの活用については後述します。

# Set Gemfile

# set :bundle_gemfile, "/var/www/myapp/current/Gemfile"

→原因はよくわからないのですが、こちらをコメントアウトすることでデプロイが正常に終了するようになりました。

最後に、config/deploy/production.rbです。


myapp/capistrano/config/deploy/production.rb

server 'Private IPを直接指定', user: 'deploy', roles: %w{app db web}

set :ssh_options, {
keys: %w(/home/deploy/.ssh/id_rsa),
forward_agent: true,
auth_methods: %w(publickey)
}


こちらは試験的な運用だったので、Private IPを直接指定してデプロイするようにしています。

ssh_optionsも特殊な設定はしていないかと思います。


デプロイ対象サーバーの設定


nginxの設定

以下の3ファイルを編集しました。


/etc/nginx/nginx.conf

http {

 略
include /etc/nginx/sites-enabled/*;
 略
}

→includeとやらを書けばいいのはわかりましたが、http{}の中に書く必要があるみたいです(初心者過ぎてわからなかった)。


/etc/nginx/sites-available/myapp.conf

upstream myapp {

server unix:///var/www/myapp/current/tmp/sockets/myapp-puma.sock;
}

server {
listen 80 default_server;
server_name myapp;
server_tokens off;
root /var/www/myapp/current/public;

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

location ~ ^/assets/ {
root /var/www/myapp/current/public;

gzip_static on;

# Per RFC2616 - 1 year maximum expiry
expires 1y;
add_header Cache-Control public;

# Some browsers still send conditional-GET requests if there's a
# Last-Modified header or an ETag header even if they haven't
# reached the expiry date sent in the Expires header.
add_header Last-Modified "";
add_header ETag "";
break;
}

location / {
gzip_static on;

# serve static files from defined root folder;.
# @gitlab is a named location for the upstream fallback, see below
try_files $uri $uri/index.html $uri.html @myapp;
}

location ~* \.(eot|ttf|woff)$ {
add_header Access-Control-Allow-Origin *;
}

# if a file, which is not found in the root folder is requested,
# then the proxy pass the request to the upsteam
location @myapp {
proxy_read_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
proxy_connect_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
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://myapp;
}
}


→こちらも特殊な設定はないかな・・・


/etc/nginx/sites-enabled

lrwxrwxrwx 1 root root   47 Dec  5 11:34 myapp.conf -> /etc/nginx/sites-available/myapp.conf


→こちらはシンボリックリンクとなります。


.rbenv-varsの設定


/var/www/myapp/shared/.rbenv-vars

SECRET_KEY_BASE=XXXXXXXXXXXXXXXXXXXXXX



デプロイの実行

myapp配下にて、

cd capistrano && bundle exec cap production deploy

でデプロイが実行されます。

このままの状態ではremoteのmasterブランチがデプロイされますが、以下のようにすることで任意のブランチをデプロイできます。

cd capistrano && bundle exec cap production deploy BRANCH={ブランチ名}

試験的なデプロイではブランチ指定でデプロイできる方が便利だと思います。

これで、ブラウザからアプリケーションにアクセスすることができました。

目的は達成したので、ここで終わりとします。


ハマったところ

いくつかハマった点はあったのですが、基本的にエラーログを見ながら対処していくので間に合いました。

ドハマリした点としては、Railsアプリケーション(Puma)側でサーバーの環境変数を読み込めなかったことです。

なんどもlinuxのユーザーを確認したり、Pumaの起動ファイルに環境変数を出力する記述を仕込んでみたりしましたが、どうも読めていないようだったので、capistrano-rbenv-varsというgemを使いました。

このgemを使うことでサーバーに置いた/var/www/myapp/shared/.rbenv-varsに直接環境変数を記述すると、Puma側で環境変数を読むことが可能になりました。

.rbenv-vars自体はgitignoreしたまま運用することが可能なので、一旦はこの方法でもいいんじゃないかと思います。

Puma側で環境変数が読めない事象にぶち当たった方で他の解決策をご存知の方は是非やり方を教えてください。


学んだこと


  • 意外と簡単にデプロイはできる

  • Nginxについてなど必然的にインプットするので、サーバーとかに詳しくなる

  • Rails5+Pumaのように新しめの技術を使うのはやっぱり難しいけど、楽しい

簡単にまとめると上記のようになりますが、非常に楽しく取り組むことができました。


今後の方針

現状とりあえずデプロイができたという状態で、課題は山積みです。

さっと思いつくだけで


  • デプロイの自動化

  • 必要なデプロイタスクの整備

  • AWSサービスの活用(S3から設定ファイルを持ってくるとかできるらしい)

  • 新サービスのALBを使用する予定なので、その相性を検証していく

などなど...

引き続き頑張ります。


エピローグ

デプロイ対象のアプリケーションの特性上本番環境で動かすにはマルチスレッドではなく、シングルスレッドのアプリケーションサーバーが必要だということが判明しました。

半日くらいヘコんだ後、アプリケーションサーバーをUnicornにするように修正しました。