Serverspec を利用したテスト駆動環境構築

とある事情からNLPのローカル開発環境を整えることになり,久しぶりにChefで遊んでます.
今回はVagrant+Chef+serverspecを利用して,日本語形態素解析器MeCab (+ipadic) を利用するための環境をテスト駆動構築するまでをまとめます.

形態素解析器 MeCab について詳しく知りたい方は,以下のサイトを参照して下さい.
本家:http://mecab.googlecode.com/svn/trunk/mecab/doc/index.html

前準備

  • vagrant init→Vagrantfileを編集して,nlpという名前のインスタンスを作成.
  • vagrant ssh-config --host nlp >> ~/.ssh/config してnlpでsshできるよう設定.
  • knife solo init nlp-repoして,chef環境の初期化.
  • knife solo prepare nlpして,Vagrantのインスタンスにchef環境を用意.

大まかな流れ

  • serverspecのテストで必要な機能について記述,VM環境でコケることを確認.
  • chefでrecipeを書き,VMに適用する.
  • 再びserverspecのテストを走らせ,全てが通ることを確認する.通らない場合は,通るまでchefによる環境構築を続ける.

serverspec の導入

なにはともあれ,serverspec をローカル環境に導入しなければ始まりません.
serverspecは,以下のように簡単にセットアップすることが出来ます(実行するディレクトリはどこでも良い?よう.今回は,Vagrantfileが置かれているnlpディレクトリ上で実行した).
詳細に関しては本家サイトを参照して下さい.
本家:http://serverspec.org/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ gem install serverspec
$ serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 1

Vagrant instance y/n: n
Input target host name: nlp
 + spec/
 + spec/nlp/
 + spec/nlp/httpd_spec.rb
 + spec/spec_helper.rb

spec/default/***_spec.rb ファイルの作成/1回目のテスト実行

spec/default/httpd_spec.rb を見本として,***_spec.rb ファイルを作成していきます.
早速,以下の通り spec/default/mecab_spec.rb を作成しました(spec/default/httpd_spec.rb は削除してしまって問題ありません).

1
2
3
4
5
6
7
8
9
10
require 'spec_helper'

describe file('/usr/local/bin/mecab') do
  it { should be_file }
  it { should be_executable }
end

describe file('/usr/local/lib/mecab/dic/ipadic/') do
  it { should be_directory }
end

ここまで出来たら,以下のコマンドを実行してテストがfailすることを確認します.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ rake spec
(in /path/to/nlp)
/path/to/ruby -S rspec spec/nlp/mecab_spec.rb
FFF

Failures:

  1) File "/usr/local/bin/mecab" should be file
     Failure/Error: it { should be_file }
       sudo test -f /usr/local/bin/mecab
       expected file? to return true, got false
     # ./spec/nlp/mecab_spec.rb:4:in `block (2 levels) in <top (required)>'

  2) File "/usr/local/bin/mecab" should be executable
     Failure/Error: it { should be_executable }
       sudo stat -c %a /usr/local/bin/mecab
       stat: cannot stat `/usr/local/bin/mecab': そのようなファイルやディレクトリはありません
       expected File "/usr/local/bin/mecab" to be executable
     # ./spec/nlp/mecab_spec.rb:5:in `block (2 levels) in <top (required)>'

  3) File "/usr/local/lib/mecab/dic/ipadic/" should be directory
     Failure/Error: it { should be_directory }
       sudo test -f /usr/local/lib/mecab/dic/ipadic/
       expected file? to return true, got false
     # ./spec/nlp/mecab_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.3636 seconds
3 examples, 3 failures

Failed examples:

rspec ./spec/nlp/mecab_spec.rb:4 # File "/usr/local/bin/mecab" should be file
rspec ./spec/nlp/mecab_spec.rb:5 # File "/usr/local/bin/mecab" should be executable
rspec ./spec/nlp/mecab_spec.rb:9 # File "/usr/local/lib/mecab/dic/ipadic/" should be directory
/path/to/ruby -S rspec spec/nlp/mecab_spec.rb failed

mecab本体用のrecipeを作成&VM環境にインストール

chefのmecab用recipeを作成します.
書き方はリンクを参考:http://docs.opscode.com/resource_script.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version = node["mecab"]["version"]

remote_file "#{Chef::Config[:file_cache_path]}/mecab-#{version}.tar.gz" do
  source "http://mecab.googlecode.com/files/mecab-#{version}.tar.gz"
  checksum node['mecab']['checksum']
  mode "0644"
  not_if { ::File.exists?("/usr/local/bin/mecab") }
end

bash "build_and_install_mecab" do
  user "root"
  cwd Chef::Config[:file_cache_path]
  code <<-EOH
    tar -zxf mecab-#{version}.tar.gz
    (cd mecab-#{version} && ./configure #{node["mecab"]["configure_options"]})
    (cd mecab-#{version} && make && make check && make install)
  EOH
  not_if { ::File.exists?("/usr/local/bin/mecab") }
end

今回,attribute/default.rbは以下の通り設定しました.

1
2
3
default["mecab"]["version"] = "0.996"
default["mecab"]["checksum"] = "15baca0983a61c1a49cffd4a919463a0a39ef127"
default["mecab"]["configure_options"] = "--with-charset=utf8 --enable-utf8-only"

書き終わったら,run_listにrecipe[mecab]を加え,以下のようにVM環境にrecipeをインストールします.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ knife solo cook nlp
Running Chef on nlp...
Checking Chef version...
Uploading the kitchen...
Generating solo config...
Running Chef...
Starting Chef Client, version 11.6.0
Compiling Cookbooks...
Converging 2 resources
Recipe: mecab::default
  * remote_file[/var/chef/cache/mecab-0.996.tar.gz] action create
    - create new file /var/chef/cache/mecab-0.996.tar.gz
    - update content in file /var/chef/cache/mecab-0.996.tar.gz from none to e07332
        (new content is binary, diff output suppressed)
    - change mode from '' to '0644'

  * bash[build_and_install_mecab] action run
    - execute "bash"  "/tmp/chef-script20131127-3321-1d0z02u"

Chef Client finished, 2 resources updated

mecabは正しくインストールされたようです.

2回目のテスト実行

ここまで出来たところで,serverspecのテストをもう1度実行してみます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ rake spec
(in /path/to/nlp)
/path/to/ruby -S rspec spec/nlp/mecab_spec.rb
..F

Failures:

  1) File "/usr/local/lib/mecab/dic/ipadic/" should be directory
     Failure/Error: it { should be_directory }
       sudo test -f /usr/local/lib/mecab/dic/ipadic/
       expected file? to return true, got false
     # ./spec/nlp-default/mecab_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.25173 seconds
3 examples, 1 failure

Failed examples:

rspec ./spec/nlp/mecab_spec.rb:9 # File "/usr/local/lib/mecab/dic/ipadic/" should be directory
/path/to/ruby -S rspec spec/nlp/mecab_spec.rb failed

先ほどと結果が変わっていることが分かります.
mecab本体用のテストは無事に通ったようです.

mecab用辞書(mecab-ipadic)のrecipeを作成&VM環境にインストール

mecab用辞書をインストールするために,recipe/default.rbに以下の内容を書き加えます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version = node["ipadic"]["version"]
remote_file "#{Chef::Config[:file_cache_path]}/mecab-ipadic-#{version}.tar.gz" do
  source "https://mecab.googlecode.com/files/mecab-ipadic-#{version}.tar.gz"
  checksum node['ipadic']['checksum']
  mode "0644"
  not_if { ::File.exists?("/usr/local/lib/mecab/dic/ipadic") }
end

bash "build_and_install_ipadic" do
  user "root"
  cwd Chef::Config[:file_cache_path]
  code <<-EOH
    tar -zxf mecab-ipadic-#{version}.tar.gz
    (cd mecab-ipadic-#{version} && ./configure #{node["mecab"]["configure_options"]})
    (cd mecab-ipadic-#{version} && /usr/local/libexec/mecab/mecab-dict-index -f euc-jp -t utf-8)
    (cd mecab-ipadic-#{version} && make install)
  EOH
  not_if { ::File.exists?("/usr/local/lib/mecab/dic/ipadic") }
end

attribute は以下の通り設定しました.

1
2
3
default["ipadic"]["version"] = "2.7.0-20070801"
default["ipadic"]["checksum"] = "0d9d021853ba4bb4adfa782ea450e55bfe1a229b"
default["ipadic"]["configure_options"] = "--with-charset=utf8 --enable-utf8-only"

書き終わったら,knifeコマンドをもう一度実行します.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Running Chef on nlp...
Checking Chef version...
Uploading the kitchen...
Generating solo config...
Running Chef...
Starting Chef Client, version 11.6.0
Compiling Cookbooks...
Converging 4 resources
Recipe: mecab::default
  * remote_file[/var/chef/cache/mecab-0.996.tar.gz] action create (skipped due to not_if)
  * bash[build_and_install_mecab] action run (skipped due to not_if)
  * remote_file[/var/chef/cache/mecab-ipadic-2.7.0-20070801.tar.gz] action create
    - create new file /var/chef/cache/mecab-ipadic-2.7.0-20070801.tar.gz
    - update content in file /var/chef/cache/mecab-ipadic-2.7.0-20070801.tar.gz from none to b62f52
        (file sizes exceed 10000000 bytes, diff output suppressed)
    - change mode from '' to '0644'

  * bash[build_and_install_ipadic] action run
    - execute "bash"  "/tmp/chef-script20131127-8352-omxnss"

Chef Client finished, 2 resources updated

mecab-ipadicは正しくインストールされたようです.

3回目のテスト実行

ここまで出来たところで,serverspecのテストをもう1度実行してみます.

1
2
3
4
5
6
(in /path/to/nlp)
/path/to/ruby -S rspec spec/nlp/mecab_spec.rb
...

Finished in 0.34858 seconds
3 examples, 0 failures

ようやくMeCabの導入に成功しました!

まとめ

上記のように,CaboChaなんかも入れていこうと思ってます.
すべて導入し終わったら,Githubに公開予定です!
(気力があれば)ブログにもまとめます.

Comments