読者です 読者をやめる 読者になる 読者になる

tjinjin's blog

いつかすごいエンジニアになることを目指して、日々学んだことを書いていきます。

Railstutorial番外編part1 〜 VMにアプリをdeployするまで

About

RailsTutorialを以前一通りやったのですが、やって終わりになってしまっていたので何かやろうと考えました。アプリを拡張するのもいいのですが、私はインフラの人間なので、実際に運用できる相当の状態にまで持って行こうと思ってやってみました。長くなりすぎたので、一旦VMへのデプロイまでをこの記事のスコープとします。

目標

  • VMでwebサーバ/dbサーバを作る
  • APPサーバとしてunicornを導入する
  • dbサーバをpostgresqlに変更
  • capistranoを使ってVMにデプロイする
  • webサーバにnginxを導入し、リバースプロキシさせる

実際やったPRはこちら。

https://github.com/krossblack/sample_app/pull/1

https://github.com/krossblack/sample_app/pull/2

殴り書きしながらやったので、要点を整理しながら記事にします。実際の順番と前後する部分がありますが、次やるときはこうしたいなという思いを込めて変更しています。

サーバを作る

VMの準備

何はともあれデプロイ先のサーバがないと始まらないので、先に作っておきます。Vagrantは1.7.4を利用しています。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  config.vm.box = "bento/centos-7.1"
  config.ssh.insert_key = false

  config.vm.define :web do |web|
    web.vm.network "private_network", ip: "192.168.34.40"
    web.cache.scope = :box if Vagrant.has_plugin? 'vagrant-cachier'
  end
  config.vm.define :db do |db|
    db.vm.network "private_network", ip: "192.168.34.41"
    db.cache.scope = :box if Vagrant.has_plugin? 'vagrant-cachier'
  end
end

vagrant1.7.x以降デフォルトの鍵が各machine毎に変更されるようになってしまい、面倒なので私はconfig.ssh.insert_key = false の設定を入れて、鍵はどのVMでも共通のものを利用するようにしています。

Railsが動く環境を変更する

本番運用を考えた際に、WEBrick/sqlite3の構成はパフォーマンスや運用の面で実用に耐えない可能性があります。そこでまずは構成を見直します。今回はよく利用されるUnicorn/Postgresqlに変更してみます。

Unicornを導入する

$ mkdir -p config/unicorn
$ touch config/unicorn/development.rb
$ cat config/unicorn/development.rb
# Railsのルートパスを求める。(RAILS_ROOT/config/unicorn.rbに配置している場合。)
rails_root = '/var/www/my_app_name'
# RAILS_ENVを求める。(RAILS_ENV毎に挙動を変更したい場合に使用。今回は使用しません。)
# rails_env = ENV['RAILS_ENV'] || "development"

# 追記に記載してます。入れた方がいいです。
ENV['BUNDLE_GEMFILE'] = rails_root + "/Gemfile"

# Unicornは複数のワーカーで起動するのでワーカー数を定義
# サーバーのメモリなどによって変更すること。
worker_processes 2

# 指定しなくても良い。
# Unicornの起動コマンドを実行するディレクトリを指定します。
# (記載しておけば他のディレクトリでこのファイルを叩けなくなる。)
working_directory "#{rails_root}/current"

# 接続タイムアウト時間
timeout 30

# Unicornのエラーログと通常ログの位置を指定。
stderr_path 'log/unicorn_stderr.log'
stdout_path 'log/unicorn_stderr.log'

# Nginxで使用する場合は以下の設定を行う。
# listen File.expand_path('../../tmp/sockets/unicorn.sock', __FILE__)
# とりあえず起動して動作確認をしたい場合は以下の設定を行う。
listen 8080
#listen "#{rails_root}/current/tmp/sockets/unicorn.sock"
# ※「backlog」や「tcp_nopush」の設定もあるけど、よくわかって無い。

# プロセスの停止などに必要なPIDファイルの保存先を指定。
pid "#{rails_root}/current/tmp/pids/unicorn.pid"

# 基本的には`true`を指定する。Unicornの再起動時にダウンタイムなしで再起動が行われる。
preload_app true
# 効果なしとの記事を見たので、コメントアウト。
# GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true

# USR2シグナルを受けると古いプロセスを止める。
# 後述するが、記述しておくとNginxと連携する時に良いことがある。
before_fork do |server, worker|
  defined?(ActiveRecord::Base) and
      ActiveRecord::Base.connection.disconnect!

  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.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
    end
  end
end

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

こちらを参考にしました。

Ruby - Unicorn設定のまとめ - Qiita

path周りの設定が参考にした記事と違ったので、変更しています。またlistenの設定はUnixドメインソケットではなくポートでアクセスするようにしています。後々nginxと接続する予定ですが、この時点でUnixドメインソケットにしてしまうと稼働確認が面倒になるので、一旦ポートにしておくべきです。

設定ファイルができたら稼働確認します。

$ bundle exec unicorn_rails -c config/unicorn/development.rb -D

実際にブラウザからアクセスして確認しましょう。 http://localhost:8080

Postgresqlを導入する

次にDBを変更します。まずは手元のMacで動作確認したいので、Postgresqlをインストールします。

$ brew install postgres
$ ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

私の環境ではすでにpostgresql設定してあったので、こちらを参考にしました。

Mac OS X YosemiteにPostgreSQLをインストール - Qiita

$ psql postgres

アプリからpostgresqlに接続するためにgemを追加します。

gem 'pg'

そして、database.ymlを修正します。

# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem 'sqlite3'
#
default: &default
  pool: 5
  adapter: postgresql
  encoding: unicode
  user: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  <<: *default
  database: sample_app_development

ハードコードするのではなくて、環境変数から読めるようにするとstage毎に設定変更がいらないので楽ですし、publicなリポジトリに公開する際に認証情報をコードに持たせなくてよいのでおすすめです。dotenvやdirenvを使うといいと聞きましたが、まだ未検証です。

この時点でうまく動くか検証しましょう。Postgresqlの場合、最初にinitdbコマンドを実行する必要があります。brewでインストールする場合は不要かもしれません。

$ service postgresql initdb 
$ be rake db:create
$ be rake db:migrate

この時点でアプリを動かしてみます。dbへのアクセスを確認したいので、sign upで新規ユーザを作るなどして動作確認します。テストを動かすでもいいかもしれません。

RailsアプリがVMにデプロイできるようにする

ミドルウェアVMにインストールする前にアプリをデプロイする準備をします。どんなミドルウェアが必要かわからなかったので、先にデプロイの仕組みづくりをした後にアプリを動かしながら、必要なミドルウェアをインストールしていきました。

デプロイにはcapistranoを利用します。Gemfileにcapistranoを追加し、初期インストールします。

$ 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

VMにデプロイするように設定ファイルを作成します。

$ cat config/deploy/development.rb
ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call

set :url, 'http://localhost/'
set :repo_url, 'https://github.com/krossblack/sample_app'
set :ssh_options, {
   user: 'app',
   keys: %w(~/.vagrant.d/insecure_private_key),
   forward_agent: false,
   auth_methods: %w(publickey)
}

role %w(web db), "192.168.34.40"

capistrano実行時にbundle installしたいので、設定を追加します。

# Gemfile
gem 'capistrano-bundler'
# Capfile
require 'capistrano/bundler'

Railsを動かすのに必要なミドルウェアを導入する

cookbookの準備

準備ができたので、cookbookを作っていきます。

$ mkdir chef && cd chef
$ bundle exec knife init .
$ bundle exec knife cookbook <cookbook> -o site-cookbooks

上記のように作成していきます。普段ならアプリとサーバ側でGitHubリポジトリを分けるのですが、気楽にやっているのでchefというディレクトリを掘ってそこで開発することにしています。

cookbookの開発

今回rbenvを使ってrubyを導入しようとしましたがデフォルトで導入されるrubyで諦めました。

VMで動かせるようになるまでに起きたトラブルまとめ

unicornが起動しない

/usr/local/share/gems/gems/bundler-1.11.2/lib/bundler/runtime.rb:80:in `rescue in block (2 levels) in require': There was an error while trying to load the gem 'uglifier'. (Bundler::GemRequireError)

このエラーはuglifierが利用しているjsのエンジンがないということらしいです。今回はtherubyracerを導入することで解決しました。

postgresqlにアクセスできない

これは2点はまってました。1点目はそもそもリモートからのアクセスを拒否する設定になっていました。デフォルトではlocalhostからの接続のみ許可するようです。

Enable remote connection · krossblack/sample_app@98f06b3 · GitHub

2点目は認証の部分です。postgresqlの認証には複数種類のパターンがあります。今回は特に認証を設けないことにしました。最終的にAWSで運用するとした際にDBはプライベートネットワークにあり、外部からのネットワークは基本的にない想定なので。いろいろ認証見たのですが、やるならssl認証かなと思いましたがどれがいいのでしょうかね。

postgresqlの認証方式

unicorncapistranoから起動しない

deploy後に特定のタスクの実行を定義することでdeploy後に起動するようになります。

Fix task · krossblack/sample_app@7334f50 · GitHub

nginxを使ってリバースプロキシさせる

ここまででVM上でアプリが動く状態になりましたが、unicornが直接リクエストをさばいている状態です。静的コンテンツなどの配信はnginxに分散させunicornはアプリの処理に集中させます。その前にunixドメインソケットでnginx -> unicornをつなぐようにします。

config/unicorn/development.rbを下記のように修正します。

listen "#{rails_root}/current/tmp/sockets/unicorn.sock"
upstream unicorn{
   server unix:/var/www/my_app_name/current/tmp/sockets/unicorn.sock;
}

server {
    listen       80;
    server_name  sample_app;

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

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

    location ~* \.(js|html|txt|ico)$ {
      root /var/www/my_app_name/current/public;
    }

    location /heatbeat {
      allow   all;
      try_files $uri $uri/index.html $uri.html @unicorn;
    }

    location @unicorn {
      satisfy any;
      allow   all;
      proxy_pass http://unicorn;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
}

上記のようにunicornのsocketの場所をnginx側で指定します。

これで細かい部分を除き、VM上でアプリを動かす所まで行き着きました。

まとめ

普段デプロイ周りはバックエンドエンジニアに任せてしまっているので、非常に苦労しました。一人でアプリを作って運用まで持っていくという経験は大切だと思うので、可能な限り本番相当の環境(AWS)にのせる所までやりたいなと思います。

参考リンク

https://github.com/krossblack/sample_app