ちょっと話題の記事

VagrantとChefでRailsアプリのStaging環境をつくる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Railsアプリケーションの検証用環境のお話です。
普段Railsアプリケーションを開発しているローカル環境とアプリケーションが本番稼動する環境との間では、OS、ソフトウェアの種類、設定ファイルの設定値等にかなり差があります。そこで、本番環境相当の環境(以後、ステージング環境と呼びます)をローカル端末の仮想マシン上に作成し開発中のアプリケーションを動かすことができれば、本番環境でしか発生しないバグの対処や開発環境では動かす予定のない機能の検証が簡単にできるので非常に便利です。
環境構築は自動化し、環境を壊してもコマンド一つですぐにまた再構築できるようにします。

今回作成する環境の概要図です。

Vagrant_Staging

Vagrant

ローカルマシン上に仮想環境をコマンドラインで作成、操作するツールで、仮想化ソフトウェアのフロントエンドになります。開発環境や本番環境で使いたいOS、ソフトウェアがインストールされた環境を簡単に作成することができ、手作業することなくコマンド一つで環境の作成、削除、再作成ができます。

Chef

サーバ、インフラ構築の自動化ツールです。より大規模な環境に利用するChef Server/Client、サーバ一台のみを対象とするChef Soloがあります。今回はChef Soloを使います。

Capistrano

アプリケーションのデプロイツールです。上記の概要図には書いていませんがステージング環境構築後のアプリケーションのデプロイで使用します。今回はGithubのプライベートリポジトリからチェックアウトします。Capistrano自体はソフトウェアのインストールなどもコマンドをサーバ上で叩くことによりある程度できますが、今回はデプロイのみに利用します。

構築するサーバにインストールするソフトウェア

サーバの区分 名称
web server nginx
application server unicorn
database mysql

それではステージング環境構築のためのソフトウェアをインストール、設定します。

仮想化ソフトウェアのインストール

Vagrantは仮想マシンを操作するツールなので、最初に仮想環境を作成する必要があります。今回は仮想化ソフトウェアにVirtualBoxを使います。以下のサイトからインストーラをダウンロードしインストールしてください。
VirtualBoxの公式サイト

Vagrant

vagrant

Vagrantもインストーラからインストールします。
以下のサイトからインストーラをダウンロードしインストールしてください。
Vagrantの公式サイト
Vagrant日本語ドキュメント

Vagrantの設定(仮想マシンの初期化、ポートフォーワーディング、共有ディレクトリ、SSH)

1 boxイメージの追加

仮想マシンをゼロから構築することもできますが時間がかかります。Vagrantはboxファイルという仮想マシンのテンプレートのようなもの(ベースイメージ)からマシンを構築することができます。今回はpassengerで有名なPhusion社が作成したboxを使います。 今回の環境構築用のディレクトリを作成し以下のコマンドでboxを追加してください。

mkdir vagrant_sample
cd vagrant_sample
vagrant box add rails-box https://oss-binaries.phusionpassenger.com/vagrant/boxes/latest/ubuntu-14.04-amd64-vbox.box

2 初期化

仮想マシンを初期化するには以下のコマンドを打ちます。

vagrant init rails-box

初期化が終わると、ディレクトリにVagrantfileが出来ています。これがVagrantの設定ファイルでRubyで書かれています。

3 ポートフォーワーディング

Vagrantfileの以下の行を有効にしてポートフォーワーディングを有効にすると、自分のマシンのポート8080から仮想マシンのポート80へフォワードしてくれます。

config.vm.network "forwarded_port", guest: 80, host: 8080

4 仮想マシンに固定IPアドレスを設定する

Vagrantfileの以下の行を有効にすると仮想マシンに固定のIPアドレスを割り振ることが出来ます。

config.vm.network "private_network", ip: "192.168.33.10"

5 共有ディレクトリの設定

ホストマシンとゲストマシン(今回はMacOSXとUbuntu)で共有ディレクトリを設定することでファイルの受け渡しが簡単に出来ます。 Vagrantfileの以下の行を有効にしてください。

config.vm.synced_folder "./share", "/vagrant_data"

(上記の場合、現在の作業ディレクトに「share」ディレクトリを作成してください)

6 SSHの設定

vagrant sshコマンドで仮想マシンに接続できますが、デプロイに必要なsshコマンドでも接続できるようにします。

vagrant ssh-config --host vagrant.local >> ~/.ssh/config

仮想マシンの起動

ここまでの設定が出来たら「vagrant up」コマンドで仮想マシンを起動できます(停止するには「vagrant halt」)。一度確認してみてください。

vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
    default: Adapter 2: hostonly
==> default: Forwarding ports...

接続には「vagrant ssh」コマンドを使います。

vagrant ssh
Welcome to Ubuntu 14.04 LTS (GNU/Linux 3.13.0-24-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
Last login: Mon Jul 27 23:13:27 2015 from 10.0.2.2
vagrant@ubuntu-14:~$

Chef(Chef-Solo)

pic-chef-logo

Chef-Soloは構築するサーバ(今回は仮想マシンのUbuntu)にインストールしサーバ上で動きます。
対象のサーバにログインして構築をするのは大変なので自分のマシン(MacOSX)からリモートで構築をできるようにします。それを実現するのがKnife-Soloというツールです。正確にはローカルマシンでサーバ構築用の各設定ファイルを作成してリモートのサーバに転送しchef soloコマンドを実行します。
名称がChef-Solo、Knife-Soloでどっちがどっちだか分からなくなってくるので以下に簡単な絵を書いておきます。

knife-chef
図の対象ノードは今回の場合、ローカル端末に作成した仮想マシン上にあるubuntuサーバになります。

ここで簡単にChefの用語を説明します。
Chefではどのように構築するか記述したファイルをrecipe, サーバ構築の各設定ファイル(recipeを含む)をまとめたものをcookbookと呼びます。そしてcookbookをまとめた入れ物をrepositoryと呼びます。
ややこしいのですが以下のような関係になります。

recipe_cookbook_repo

Knife-Soloのインストール

gem、もしくはbundlerどちらでもよいのでインストールします。bundlerの場合は以下のようにVagrantfileのあるディレクトリでGemfileを作成してください。 一緒にberks(外部のcookbookを管理するツール、rubyのbundlerのようなもの)もインストールするように記述しておきます。

source "https://rubygems.org"

gem "chef", "11.12.4"
gem "berkshelf", "3.1.2"
gem "knife-solo"

bundle installコマンドでカレントディレクトリにインストールしてください。Vagrantfileのあるディレクトリで実施します。

bundle install --path vendor/bundle

リポジトリの初期化

リポジトリの初期化を以下のコマンドで行います。Vagrantfileのあるディレクトリで実施します。

knife solo init .

初期化が終わると、リポジトリにはcookbookの作成に必要なファイル、ディレクトリが配置されます

cookbookの作成、外部コミュニティ製cookbookのインストール

1 cookbookの作成

リポジトリにはcookbooks、site-cookbooksというディレクトリが出来ています。自分が作成したcookbookはsite-cookbooksディレクトリに、berksコマンドで追加した外部コミュニティのcookbookはcookbooksディレクトリに配置します。cookbooksディレクトリ以下のファイルは基本的に自分で編集しません。

以下のコマンドで今回のサーバ構築用のcookbookを作成します

bundle exec knife cookbook create -o site-cookbooks staging_cookbook

今回、自分で記述する設定は主にこのstaging_cookbook配下のファイルを編集します。

2 外部コミュニティのcookbookのインストール

サーバ構築の設定を全て自分で行うのは大変なので外部コミュニティが作成しているcookbookで今回利用できるものをインストールします。 まずカレントディレクトリにBerksfileというファイルを作成し以下の記述を追加してください。
インストールするcookbookを指定しています。

source "https://api.berkshelf.com"

cookbook 'git'
cookbook 'build-essential'
cookbook 'redis'
cookbook 'nodejs'
cookbook 'xml'
cookbook 'ruby_build'
cookbook 'rbenv', :git => 'https://github.com/chef-rbenv/chef-rbenv.git'
cookbook 'nginx'
cookbook 'imagemagick'
cookbook 'mysql', "~> 5.3.6"

berksコマンドでcookbookをインストールします。cookbooksディレクトリにそれぞれ配置されます。 cookbooksディレクトリを削除してから実施してください。

bundle exec berks vendor cookbooks

インストールするソフトウェアのrecipe、その他ファイルの設定

1 Nginxの設定

nginxをサーバにインストールするようにレシピを書いていきます。自分が作成したcookbookにファイルを作成します(ファイル名は自由です)。
site-cookbooks/staging-cookbook/recipes/nginx_recipe.rb

directory '/var/www/staging_rails_app' do
  owner 'deploy_user'
  action :create
  recursive true
end

template '/etc/nginx/sites-available/default' do
  action :create
  source "default.conf.erb"
  notifies :reload, 'service[nginx]'
end

上記で指定したテンプレートファイル(default.conf.erb)を作成します。内容はunicornと連携するようにしています。
site-cookbooks/staging-cookbook/templates/default/default.conf.erb

upstream unicorn {
  server unix:/tmp/unicorn.sock;
}

server {
  listen 80;
  server_name <%= node.staging_rails_app.server_name %>;
  access_log /var/log/nginx/<%= node.staging_rails_app.server_name %>.access.log;

  root /var/www/staging_rails_app/current/public;


  location / {
    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_redirect off;

    if (!-f $request_filename) {
         proxy_pass http://unicorn;
         break;
     }
  }
}

2 MySqlの設定

続いてMySqlのインストール、設定用のrecipeを作成します。
site-cookbooks/staging_cookbook/recipe/mysql_recipe.rb

%w(mysql-client mysql-server).each do |pkg|
  package pkg do
    action :install
  end
end

template "create database sql" do
  path "/home/#{node['mysql']['user']}/create_databases.sql"
  source "create_databases.sql.erb"
  owner node['mysql']['user']
  group node['mysql']['group']
  mode "0644"
end

template "create users sql" do
  path "/home/#{node['mysql']['user']}/create_users.sql"
  source "create_users.sql.erb"
  owner node['mysql']['user']
  group node['mysql']['group']
  mode "0644"
end

execute "exec create databases sql" do
  command "mysql -u root --password='#{node['mysql']['server_root_password']}' < /home/#{node['mysql']['user']}/create_databases.sql"
  not_if "mysql -u root -p#{node['mysql']['server_root_password']} -D #{node['mysql']['staging_db_name']}"
  user node['mysql']['user']
  group node['mysql']['group']
  environment 'HOME' => "/home/#{node['mysql']['user']}"
end

execute "exec create users sql" do
  command "mysql -u root --password='#{node['mysql']['server_root_password']}' < /home/#{node['mysql']['user']}/create_users.sql"
  not_if "mysql -u #{node['mysql']['staging_user_name']} -p#{node['mysql']['staging_password']}"
  user node['mysql']['user']
  group node['mysql']['group']
  environment 'HOME' => "/home/#{node['mysql']['user']}"
end

少し長いのですが、以下の内容が書いてあります。

  • 最初のpackageブロックでmysqlをインストール
  • templateブロックでDababase作成sqlとユーザー作成sqlをサーバの所定のディレクトリにコピーする
  • executeブロックで上記のコピーしたsqlファイルを実行
  • 使用している['mysql']['server_root_password']の値はインストールしたmysqlのcookbookのattributesディレクトリにあるdefault.rbファイルに設定されています。  

続いてrecipeで使用しているtempateファイルを作成します。内容は短いのですが分かりやすくするためにファイルを分けています。
site-cookbooks/staging_cookbook/templates/default/create_databases.sql.erb

CREATE DATABASE <%= node.mysql.staging_db_name %> DEFAULT CHARACTER SET utf8;

site-cookbooks/staging_cookbook/templates/default/create_users.sql.erb

GRANT ALL PRIVILEGES ON <%= node.mysql.staging_db_name %>.* TO <%= node.mysql.staging_user_name %>@localhost IDENTIFIED BY '<%= node.mysql.staging_password %>' WITH GRANT OPTION;

最後にrecipe, templateから参照されている変数値(node['mysql']['user']や<%= node.mysql.staging_db_name %>)を設定します。
site-cookbooks/staging_cookbook/attributes/default.rb

node.default["staging_cookbook"]["server_name"] = "vagrant.local"
node.default["mysql"]["staging_db_name"] = "db_staging"
node.default["mysql"]["staging_user_name"] = "user_staging"
node.default["mysql"]["staging_password"] = "pass_staging"
node.default["mysql"]["user"] = "vagrant"
node.default["mysql"]["group"] = "vagrant"

3 デプロイユーザーの作成

仮想マシンのサーバ上でデプロイをするユーザを作成します。
site-cookbooks/staging_cookbook/recipes/deploy_user_recipe.rb

user 'deploy_user' do
  action :create
  supports :manage_home => true
  home "/home/deploy_user"
  shell "/bin/bash"
end

4 デプロイに必要な秘密鍵、公開鍵の設定

Capistranoでdeployするので、Githubからリポジトリのチェックアウトをするために秘密鍵/公開鍵の設定が必要になります。 また、仮想環境上のサーバにSSHでログインするための秘密鍵/公開鍵の設定も必要です。 以下のコマンドで作成してください。

ssh-keygen -t rsa -f ~/.ssh/staging_rails_app.pem
ssh-keygen -t rsa -f ~/.ssh/login.pem
秘密鍵の配置

作成したstaging_rails_app.pem「site-cookbooks/staging-cookbook/files/default」ディレクトリにid_rsaという名称で、login.pem.pub「site-cookbooks/staging-cookbook/files/default」ディレクトリにauthorized_keysという名称でそれぞれコピーしてください。 そしてcookbookに配置したこのファイルをサーバの指定したディレクトリに配置してくれるようにrecipeを書いていきます。
site-cookbooks/staging-cookbook/recipes/auth_recipe.rb

include_recipe "staging_cookbook::deploy_user"

directory "/home/deploy_user/.ssh" do
  action :create
  owner "deploy_user"
  mode "0700"
end

cookbook_file "/home/deploy_user/.ssh/id_rsa" do
  action :create
  owner "deploy_user"
  mode "0600"
end

cookbook_file "/home/deploy_user/.ssh/authorized_keys" do
  action :create
  owner "deploy_user"
  mode "0600"
end

この設定をすると、秘密鍵がプロジェクトに含まれることになります。Githubのパブリックプロジェクトに置かないように注意してください。

公開鍵をGithubに登録する

先ほど作成した公開鍵(~/.ssh/staging_rails_app.pem.pub)の内容をGithubに登録してください。 登録はユーザーページの「Setting -> SSH keys」から行ってください。

5 Vagrantfileにcookbookのレシピを指定

サーバ構築に利用するcookbookのレシピをVagrantfileに指定します。自分が作成したrecipeとberksコマンドでインストールした外部コミュニティのcookbookをそれぞれ指定します。
これによってvagrantの起動時に指定したソフトウェアが仮想マシンのサーバにインストールされます(chef-soloも同時にサーバにインストールされます)。

  config.vm.provision :chef_solo do |chef|
    chef.cookbooks_path = ["./cookbooks", "./site-cookbooks"]
    chef.add_recipe 'build-essential'
    chef.add_recipe 'git'
    chef.add_recipe 'rbenv'
    chef.add_recipe 'nodejs'
    chef.add_recipe 'xml'
    chef.add_recipe 'ruby_build'
    chef.add_recipe 'rbenv::system'
    chef.add_recipe 'nginx'
    chef.add_recipe 'imagemagick'
    chef.add_recipe 'mysql::client'
    chef.add_recipe 'mysql::server'
    
    chef.add_recipe 'staging_cookbook::nginx_recipe'
    chef.add_recipe 'staging_cookbook::mysql_recipe'
    chef.add_recipe 'staging_cookbook::deploy_user_recipe'
    chef.add_recipe 'staging_cookbook::auth_recipe'

    chef.json = {
      "rbenv" => {
        "global" => "2.2.2",
        "rubies" => "2.2.2",
        "gems" => {
          "2.2.2" => [
            { 'name' => 'bundler' }
          ]
        }
      }
    }
  end

6 仮想マシンのプロビジョニング

ここまでの設定でプロビジョニングの設定は一通り出来ました。以下のコマンドでステージング環境を作成します。
vagrantですでにVMを起動させている場合は以下のコマンド

vagrant reload 
vagrant provision

まだVMを起動していない場合は以下のコマンド

vagrant up --provision

Capistrano

CapistranoLogo

Ruby製のツールなのでGemをインストールして、デプロイ時に必要な処理を記述していきます。

Capistranoのインストール

デプロイ対象のRailsアプリケーションのGemfileに以下の記述を追加します。

group :development do
  gem 'capistrano', '~> 3.4.0'
  gem 'capistrano-rails'
  gem 'capistrano-bundler'
  gem 'capistrano3-unicorn'
end

unicorn、mysqlのインストールが出来ていない場合は以下の記述も追加します。

group :staging, :production do
  # Use Unicorn as the app server
  gem 'unicorn', '~> 4.9.0'
  gem 'mysql2', '~> 0.3.19'
end

bundlerでインストールします。

bundle exec install --path vendor/bundle

以下コマンドでcapistranoの設定ファイルを作成します

bundle exec capistrano install

Capistranoの設定

1 Capfileの設定

初期状態だとコメントアウトされている以下の設定を有効にしてください。

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

2 デプロイファイルの設定

configディレクトリのdeploy.rbに以下の設定を追加します(元から設定されている行もここにのせておきます)。

lock '3.4.0'

set :application, 'staging_rails_app' # ここにアプリケーション名を設定
set :repo_url, 'git@github.com:test_team/staging_rails_app.git' # ここにリポジトリのURLを設定

# デプロイ対象ブランチがmasterでない場合、以下の行を追加する
set :branch, "develop" # ここにデプロイ対象のブランチ名を設定

set :deploy_to, '/var/www/staging_rails_app' # デプロイ先ディレクトリ。Chefで作成する

set :scm, :git

set :default_env, {
      rbenv_root: "/usr/local/rbenv",
      path: "/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH"
    }

set :keep_releases, 5

# 以下unicorn関係の設定
set :linked_dirs, (fetch(:linked_dirs) + ['tmp/pids'])

# unicorn
set :unicorn_rack_env, "none"
set :unicorn_config_path, 'config/unicorn.rb'

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

3 環境別設定ファイルの設定

「config/deploy/staging.rb」を作成し以下の設定を追加します。ファイルはproduction.rbをコピーして作成してください。

role :app, %w{deploy_user@vagrant.local}
role :web, %w{deploy_user@vagrant.local}
role :db,  %w{deploy_user@vagrant.local}

set :rails_env, :staging

DB、 Unicorn、アプリケーション設定ファイルの設定

1 database.ymlの設定

Chefで作成するDatabaseの名称、ユーザー、パスワードは次の設定でした。

環境 Database名 User名 Password
staging db_staging user_staging pass_staging

この設定に合わせてアプリケーションのdatabase.ymlを変更します。

default: &default
  pool: 5
  timeout: 5000
  
staging:
  <<: *default
  adapter: mysql2
  socket: /var/run/mysqld/mysqld.sock  
  encoding: utf8
  reconnect: false
  database: db_staging
  username: user_staging
  password: pass_staging

2 unicorn.rbの変更

configディレクトリに「unicorn.rb」というファイルを作成し以下の内容を入力します。

listen "/tmp/unicorn.sock"
pid "tmp/pids/unicorn.pid"

worker_processes 2

timeout 15

preload_app true

ROOT = File.dirname(File.dirname(__FILE__))
stdout_path "#{ROOT}/log/unicorn-stdout.log"
stderr_path "#{ROOT}/log/unicorn-stderr.log"

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

  old_pid = "#{server.config[:pid]}.oldbin"
  unless old_pid == server.pid
    begin
      Process.kill :QUIT, 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

3 staging環境用設定ファイルの作成

「config/environments」ディレクトリの下に「staging.rb」というファイルを作成します。内容はdevelopment.rbの内容をそのまま設定し、環境毎に異なる設定のみ値を変更してください(session_store、action_mailerあたりの設置値が変わってくると思います。)

4 secret.ymlの設定

stagingというキーで値を設定してください。最初からdevelopment、test、productionのキーは設定されています。今回はdevelopmentの値をstagingで使用します。

deploy

お疲れ様でした。長かったですがここまでの設定でアプリケーションをdeployする準備が出来ました。
以下のコマンドで仮想マシン上のステージング環境にアプリケーションをdeployしてください。

bundle exec cap staging deploy

成功していれば「localhost:8080//アプリケーションのURL」で画面が表示されるはずです。

まとめ

一度Vagrant、Chefでステージング環境を構築すれば、いろいろ設定をいじり環境が壊れてしまったり、不要になって環境を削除した後でもまたコマンド一つですぐに環境を再現できます。試行錯誤を繰り返す、またはChefのレシピを試すプログラマにとって強い助けになると思います。Chefの設定ファイルの表現力も生のShellや設定ファイルを書き保守し続けることに比べると非常に簡単で魅力的です。

一方Chef-Soloには少し不安要素もあります。Chef-Soloは今後、Chef−Zeroというソフトウェアに置き換わり、Chef−Solo自体はいずれChefから削除される予定です(すぐにではないようです)。詳しくは以下のページを参照してください。

Chef-SoloからChef-Zeroへの移行についての説明

とはいえ、Knif-Soloでローカル端末にcookbook等を作成し対象nodeを構築することに変わりは無いですので、ローカル端末の仮想マシン上に環境を構築するぐらいであればあまり心配は無いと個人的に思っています。