VagrantとChefでRailsアプリのStaging環境をつくる
Railsアプリケーションの検証用環境のお話です。
普段Railsアプリケーションを開発しているローカル環境とアプリケーションが本番稼動する環境との間では、OS、ソフトウェアの種類、設定ファイルの設定値等にかなり差があります。そこで、本番環境相当の環境(以後、ステージング環境と呼びます)をローカル端末の仮想マシン上に作成し開発中のアプリケーションを動かすことができれば、本番環境でしか発生しないバグの対処や開発環境では動かす予定のない機能の検証が簡単にできるので非常に便利です。
環境構築は自動化し、環境を壊してもコマンド一つですぐにまた再構築できるようにします。
今回作成する環境の概要図です。
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の設定(仮想マシンの初期化、ポートフォーワーディング、共有ディレクトリ、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)
Chef-Soloは構築するサーバ(今回は仮想マシンのUbuntu)にインストールしサーバ上で動きます。
対象のサーバにログインして構築をするのは大変なので自分のマシン(MacOSX)からリモートで構築をできるようにします。それを実現するのがKnife-Soloというツールです。正確にはローカルマシンでサーバ構築用の各設定ファイルを作成してリモートのサーバに転送しchef soloコマンドを実行します。
名称がChef-Solo、Knife-Soloでどっちがどっちだか分からなくなってくるので以下に簡単な絵を書いておきます。
図の対象ノードは今回の場合、ローカル端末に作成した仮想マシン上にあるubuntuサーバになります。
ここで簡単にChefの用語を説明します。
Chefではどのように構築するか記述したファイルをrecipe, サーバ構築の各設定ファイル(recipeを含む)をまとめたものをcookbookと呼びます。そしてcookbookをまとめた入れ物をrepositoryと呼びます。
ややこしいのですが以下のような関係になります。
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
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を構築することに変わりは無いですので、ローカル端末の仮想マシン上に環境を構築するぐらいであればあまり心配は無いと個人的に思っています。