Rails/Rbenv/Apache+Passenger/Mysql/jenkins on Vagrantなテスト環境を揃える


# 概要

## やりたいこと

railsのテスト環境欲しいなー☆
って思ってvagrantで頑張ってたけどだいぶ時間がかかったので備忘録でまとめておく。

やりたいのは有りもののレシピを使って自動で環境するってこと。
なのでberkshelfを使って出来合いのcookbookを活用することにした。
（でも結果的にはちょっと手をいれて自作してます。）

加えてrailsアプリを突っ込んでjenkins上でテストやシンタックスチェックなどを自動で回せる用にもしてみることにした。

passenger＋rbenv＋railsにするところがなかなか調べてもわかんなかったので自分でやることに。

意見や不具合などあれば教えて下さい！


## 苦労したこと、上手くいってないこと

- passenger_apache2のcookbookを使おうとしたがrbenvのrubyを認識してくれなかったので使うのやめて作りなおした
- jenkinsのインストールでタイムアウトエラーが発生することがある（再度vagrant provisionすればうまくいく）
- railsアプリをsynced_folderにフォルダごと直置きしてるのがあんまり綺麗じゃない

特に最後の項目は、githubから自動で取ってくるなりしたほうがいいかなと思ったりしますが手を付けてません。




## てっとり早く試したいなら

[このリポジトリ](https://github.com/muran001/apache2-rails-box)をgit cloneしてきて以下実行

```
# git cloneした一つ上の階層のappフォルダにrailsアプリを置く
# そしてそのアプリ名をattributes/default.rbで設定しておく
# vagrantやberkshelfは事前にインストールしておく

$ berks vendor
$ vagrant up

# もしプロビジョニングだけやりたいなら
$ vagrant provision
```

最初からやるとそれなりに時間がかかりますのでご注意


# かいつまんで説明

## berkshelfでopscodeのレシピ取得

Berksfileに以下を追加

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

metadata

cookbook "apt"
cookbook "build-essential"
cookbook "apache2"
cookbook "git"
cookbook "vim"
cookbook "mysql"
cookbook "database"
cookbook "rbenv",git: "https://github.com/RiotGames/rbenv-cookbook.git"
cookbook "ruby_build"
cookbook "timezone"
cookbook "jenkins"
```

あとはberks vendorで勝手にcookbookを取ってこれます。


## apacheのレシピをちょっとだけ作る

apache2のcookbookを使います。
サーバー名やドキュメントルートなどを変えたいのでrecipes/apache2.rbとして記述

```recipes/apache2.rb
web_app 'rails' do
  server_name "#{node['rails']['server_name']}"
  server_aliases ["www.#{node['rails']['server_name']}"]
  docroot "/home/vagrant/app/#{node['rails']['app_name']}/public"
end
```

ここで書いてる変数はattributes/default.rbに記述しておきます。


## mysqlのレシピをちょっとだけ作る

mysqlとユーザー追加とかが可能なdatabaseのcookbookを使います。
Mysqlはデータベースユーザーなんかを作ったり設定ファイルを置いたりします

管理者とアプリユーザーって形で分けてますが、テスト環境なんで管理者だけでもいい気がします。

```recipes/mysql.rb
include_recipe 'mysql::server'

service 'mysql' do
  supports status: true, restart: true, reload: true
  action [ :enable, :start ]
end

template '/etc/mysql/conf.d/rails_db.cnf' do
  source 'rails_db.cnf.erb'
  owner  'root'
  group  'root'
  mode   '0644'
  if ::File.exists?("/etc/mysql/conf.d/rails_db.cnf")
    notifies :restart, 'service[mysql]', :immediately
  end
end

include_recipe 'database::mysql'

mysql_connection_info = {
  host: "localhost",
  username: "root",
  password: node['mysql']['server_root_password']
}

mysql_database node['rails']['db_name'] do
  connection mysql_connection_info
  action :create
end

mysql_database_user node['rails']['db_admin'] do
  connection mysql_connection_info
  password node['rails']['db_admin_pass']
  database_name node['rails']['db_name']
  privileges [:all]
  action [:create, :grant]
end

mysql_database_user node['rails']['db_app_user'] do
  connection mysql_connection_info
  password node['rails']['db_app_user_pass']
  database_name node['rails']['db_name']
  privileges [:select, :update, :insert, :delete, :create, :index, :drop]
  action [:create, :grant]
end
```

## railsのレシピを頑張って作りこむ

流れとしては、以下のとおり

- rbenvを入れる
- 必要なパッケージを入れる
- 必要なgemを入れる
- passenger周りの設定をする
- rails入れる
- (所定場所の)railsアプリを動作可能にする

railsアプリはvagrantfile指定のsynced_folder内に置くようにしています。

ちょっと若干ムダもありそうですが一応。


```recipes/rails.rb
include_recipe "rbenv::default"
include_recipe "rbenv::ruby_build"
include_recipe "rbenv::rbenv_vars"
include_recipe "apache2"
include_recipe "build-essential"

rbenv_ruby "#{node['rails']['ruby_version']}" do
  ruby_version "#{node['rails']['ruby_version']}"
  global true
end 

rbenv_gem "bundler" do
  action :install
end

if platform?("centos","redhat")
  package "httpd-devel"
  if node['platform_version'].to_f < 6.0
    package 'curl-devel'
  else
    package 'libcurl-devel'
    package 'openssl-devel'
    package 'zlib-devel'
  end
else
  %w{ apache2-prefork-dev libapr1-dev libcurl4-gnutls-dev }.each do |pkg|
    package pkg do
      action :upgrade
    end
  end
end

rbenv_gem "passenger" do
  action :install
end

rbenv_gem "rbenv-rehash" do
  action :install
end

rbenv_gem "rails_best_practices" do
  action :install
end

rbenv_execute "ruby_bin" do
  command 'rbenv which ruby > /tmp/ruby_bin'
end

rbenv_execute "passenger_root" do
  command 'passenger-config --root > /tmp/passenger_root'
end

ruby_block "set passenger attributes" do
  block do
    node.default[:passenger][:ruby_bin]    = %x(cat /tmp/ruby_bin).chomp
    node.default[:passenger][:root_path]   = %x(cat /tmp/passenger_root).chomp
    node.default[:passenger][:module_path] = "#{node[:passenger][:root_path]}/buildout/apache2/mod_passenger.so"
  end
end

if platform_family?('debian')
  template "#{node['apache']['dir']}/mods-available/passenger.load" do
    cookbook 'passenger_apache2'
    source 'passenger.load.erb'
    owner 'root'
    group 'root'
    mode 0644
  end
end

template "#{node['apache']['dir']}/mods-available/passenger.conf" do
  cookbook 'passenger_apache2'
  source 'passenger.conf.erb'
  owner 'root'
  group 'root'
  mode 0644
end

rbenv_execute "passenger module" do
  command 'passenger-install-apache2-module --auto'
  creates node[:passenger][:module_path]
end

apache_module 'passenger' do
  module_path node['passenger']['module_path']
end

rbenv_gem "pry" do
  action :install
end

rbenv_gem "rails" do
  action :install
end

%w(libsqlite3-dev).each do |pkg|
  package pkg do
    action :install
  end
end

rbenv_execute 'bundle install' do
  cwd "/home/vagrant/app/#{node['rails']['app_name']}"
  command "bundle install"
end
rbenv_execute 'rake db:reset' do
  cwd "/home/vagrant/app/#{node['rails']['app_name']}"
  command "rake db:reset RAILS_ENV=test"
end
rbenv_execute 'rake db:migrate' do
  cwd "/home/vagrant/app/#{node['rails']['app_name']}"
  command "rake db:migrate RAILS_ENV=test"
end
```

マイグレーションまでやっちゃってるのでもうvagrantにログインして何かしなくてもrailsアプリは動作します。


## jenkinsのレシピを作る

jenkinsではパッケージインストールだけでなくて、プラグインのインストールとジョブの登録までやります

ジョブはテンプレートファイル化しています。（Github参照）

ちなみにjenkinsのジョブは、一度WebUIでジョブを登録しておいてそれがxmlファイルとして保存されているのでそれをそのまま持ってきて拡張子だけerbに変えて使ってます。

jenkinsのジョブでrspec実行するのはもちろんrails_best_practicesなんかを使えるようにしておけばいろいろ捗ります。

```recipes/jenkins.rb
include_recipe 'jenkins::master'

jenkins_plugin 'git'
jenkins_plugin 'rbenv'
jenkins_plugin 'greenballs'
jenkins_plugin 'plot'

jenkins_dir = '/home/vagrant/jenkins'

directory jenkins_dir do
  action :create
end

rails_job_config = File.join(Chef::Config[:file_cache_path], "#{node['rails']['job_name']}.xml")

template rails_job_config do
  source 'jenkins_config.xml.erb'
end

jenkins_job node['rails']['job_name'] do
  config rails_job_config
end
```


## Vagrantfileを編集する

ここでsynced_folder設定してそこにrailsアプリをおいてます


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

VAGRANTFILE_API_VERSION = "2"
Vagrant.require_version ">= 1.5.0"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  config.vm.hostname = "apache2-rails-box"
  config.omnibus.chef_version = :latest
  config.vm.box = "precise64"
  config.vm.box_url = "http://files.vagrantup.com/precise64.box"
  config.vm.network :private_network, ip: "192.168.33.10"
  config.berkshelf.enabled = true
  config.vm.synced_folder '../app', '/home/vagrant/app', nfs: true

  config.vm.provision :chef_solo do |chef|
    chef.log_level = :debug
    chef.json = {
      mysql: {
        server_root_password: 'rootpass',
        server_debian_password: 'debpass',
        server_repl_password: 'replpass'
      },
      tz: "Asia/Tokyo",
      apache: {
        doc_root: '/home/vagrant/app'
      }
    }

    chef.run_list = [
        "recipe[apt]",
        "recipe[build-essential]",
        "recipe[vim::default]",
        "recipe[git::default]",
        "recipe[apache2-rails-box::apache2]",
        "recipe[apache2-rails-box::mysql]",
        "recipe[apache2-rails-box::rails]",
        "recipe[timezone::default]",
        "recipe[jenkins::master]",
        "recipe[apache2-rails-box::jenkins]"
    ]
  end
end
```


## その他必要なこと

説明しませんが、attributeとtemplateを用意します。（Github参照）


# 今後の課題

- OSはCentOS版、 Webサーバーはnginx版も試してみたいところ
- test-kitchen + serverspecで作成した環境のテストをやる
- jenkinsインストールのタイムアウトエラー問題をなんとかする
- vagrant up所要時間の短縮（できるのか？）