Chefのテストスイーツを色々試してみた (1)chefspec, minitestなどによるテスト整備
エンジニアのみなさま、自動化は進めていますでしょうか。海外ドラマにハマってる@masudaKです。
すたじおなんとかさんが、以下のようなことを仰ってまして、
. @kenjiskywalker さんとか @nakashii_ さんとか@mikeda さんとか@masudaK さんとかはユーザローカルなRuby環境作るcookbookとか既に書いてそう
— Satoshi.S (@studio3104) 2013, 3月 13
rbenvのcookbookでも作ろうかなと思ったのですが、折角なので前から触りたかったツールを色々使ってみました。その紹介記事であります。
記事書こうかなと思ってるうちにVagrantがクリティカルに進化してたりして追いつけてない(近いうちに動かなくなる)部分もあるかもしれませんし、Rubyも触りしかわかってないので、間違い等あったらご指摘お願いします。
ちなみにChefあまり触ったことがない人は、chef-soloとかでまず簡単なところからやってみるのがよいと思うので、そのへんは他の記事に任せます。また、Chefサーバのインストールは以下の記事に書いてますので、そちらでも(古いので、陳腐化してるかも…)。
今回の目的はテストや試験的環境を作り、そのなかでテスト・レシピを書いて、リリースするということを念頭に置いています。
ただ、Chefと戯れ続けてると以下の様な課題も出てきて、そういうのもできるだけクリアできるように書いたつもりです。
- 本番で実際レシピ流したら、エラー吐いて、途中までしか流れてない。
- エラーも吐かずレシピ流れたけど、実は違う環境(ステージングとか)の設定が流れてて、間違ったバージョンとかのもの流してる(気づきにくい)。テストがない。
- 違うプラットフォームとかに向けてレシピ書いてるうちにデグレって、昔書いた処理がうまく機能してないことに気づく。
- 色々書いてるうちに、コードが複雑化してしまう。リファクタしたいのにそのまんま…
まぁ、実際触れると色々詰まるのですが、そんなこと言ってもしょうがないので、気楽にレシピ修正して、自動化進めましょうという話しを書いてみたいと思います。
まずは、構成
構成をまず考えましょう。といっても、最初から文字で羅列すると複雑なので、Githubにあげた構成を見て下さい。
github.com
- cookbooks/ - cookbooks/rbenv - cookbooks/rbenv/spec - cookbooks/rbenv/files/default/tests/minitest - cookbooks/Rakefile - feature/
今回重要なのは、以上となります。一つ一つ説明していきますが、実装にかけられる時間とかを考えると、省いたほうがいい部分もあったりするので、この記事のなかで、これはやってみようかなというものだけ是非やってみてください。あと、間違ってる箇所があれば、是非ご指摘をば。
では、説明していきましょう。
cookbooks/rbenv/spec
早速レシピを書きたい気持ちはありますが、最初は良くても、後々テストがなく困るということが割りとあります。パッケージAはバージョン固定で、バージョン1.3を入れるようにして書いていたのに、違うプラットフォーム・ディストリビューションだとバージョンが変わっていて、処理書き換えていたら、なぜか違うバージョンが入ってるとか…
無駄に全環境に流して確認しないといけないとか、まぁ後々困ることはよくあります。
ので、そういうのを避けるためにテストを書きましょう。ここではRspecの記述を利用しながら、レシピのテストができる、chefspecを使ってみます。2013年3月20日時点ではChef11系では動かないので、rspec実行のchefのバージョンは上げ過ぎないように気をつけましょう。
基本的な書き方はドキュメントに書いてありますので、熟読しておくのがよいでしょう。書き方はシンプルで、gitパッケージのバージョン1.7.1-3.el6_4.1が入ったかどうかの確認をしたければ以下の様に書きます。
it { expect(chef_run).to install_package_at_version "git","1.7.1-3.el6_4.1" }
んで、さらにテスト全体を書く方法を説明します。ドキュメントにあるサンプルを少しいじって説明すると、
# まずrequire require 'chefspec' # describeでテストしたいレシピ記述して、convergeで引数にレシピを指定したものをletでメモ化。要はchef_runオブジェクトをずっと使いまわせるようにする describe 'my_new_cookbook::default' do let(:chef_run) { ChefSpec::ChefRunner.new.converge 'my_new_cookbook::default' } # itを使って、どうなるかの記述をして、そのなかで/var/lib/fooディレクトリが作られていることを確認します。 it 'should do something' do expect(chef_run).to create_directory '/var/lib/foo' end end
概要はこんな感じです。シンプルなレシピであれば、このようなテストを書くのはそこまで難しくないでしょう。
あとは、 「$ rspec -fd -c spec/default_spec.rb」 でrspecを実行して、確認してみましょう。
レシピがまだないので、エラーになりますが、とりあえずそのまま必要そうな処理を書いていきます。
んで、僕の環境だと、OSの違いとかあってもちゃんとカバーできるようにテストを書こうとして、以下のようになっていきました。
describe 'my_new_cookbook::default' do let(:chef_run) { ChefSpec::ChefRunner.new.converge 'my_new_cookbook::default' } describe 'When Centos' do it 'should do something' do expect(chef_run).to create_directory '/var/lib/foo' end end describe 'When Ubuntu' do it 'should do something' do expect(chef_run).to create_directory '/var/lib/foo' end end end
これだと、対象とするプラットフォーム増えるたびに、「create_directory '/var/lib/foo'」って処理が増えていくわけですね。
レシピに10の処理が書いてあるなら、10×プラットフォームの数になるわけです。んで、抽象クラスを作って、そのなかにそれを継承したcentosクラスとかubuntuクラスを作ってみましたが、それでも処理が長いことには変わらない。。。
また、テストである性質というのもあってファイル名は変えたくないので、どうしてもrecipes/default.rbに対するテストはspec/default_spec.rbに書いておきたいわけです。
しかも、あまりに疎にして、ファイルが分離しすぎたら、具体的であるはずのテストも見難くなってしまう。と思っていたら、いいものありました。
shared_exaplesという共通処理をまとめられるものが用意されてるのですね。
なので、パッケージをインストールするレシピに対応するテストは以下の様な感じにしてみました。
shared_examples 'Package' do |package_name, package_version| context 'when install' do it { expect(chef_run).to install_package_at_version "#{package_name}","#{package_version}" } end end <中略> describe "Package git" do it_behaves_like 'Package', 'git', '1.7.1-3.el6_4.1' end
こうすると、describeで文脈が変わっても、処理は共通化させることができる。しかも引数渡せるという神ぐあいで、助けられました。
あとは、OSの違いをパラメータとして渡さないといけないので、node['platform']にsetしないといけない。
そういう場合は、以下のようにnode.automatic_attrsを使って、値を渡します。
let(:chef_run) { runner = ChefSpec::ChefRunner.new() runner.node.automatic_attrs['platform'] = 'centos' runner.converge 'rbenv::default' runner }
んで、最終的な形になったのが、以下。
github.com
書いてみて、以下の印象を受けました。
- 共通化もできるし、処理がわかりやすい。モジュールとかにすれば、もっと共通化できるのかな。
- 書き方もよくわからなかったけど、リファクタのポイントをまとめてくれる資料がわりとあって、少しずつ見やすくできた(気がする)
- ただ、gitリソースとかに対応してなくて、テストできない部分があった
- 実際にレシピ流さなくてもrspecコマンドだけでテストできるので、非常に早い。気楽にテストできる。
という感じです。Rubyよくわかってないので、今もいい書き方になってるかは分かりませんが、レシピのテストは最低限はできるし、一度形を決めれば使いまわせるので、初心者には優しいかなぁと。ただ、最低限で全てカバーできるわけでは当然ないので、そこは注意が必要です。
cookbooks/rbenv/files/default/tests/minitest
レシピのテストとしては上述したchefspecの他にminitestというものがあります。
chefと連携させたものとしてはminitest-chef-handlerになります。
minitestってtest-kitchenと連携させて、オプスタでインスタンスの作成とともにレシピを流して、その後レシピのテストを行うってのが一般的なのかなと思ってましたが、そうでもないみたいですね。
ということで、オプスタを使わず、Vagrantでやってみましょう。
をもとにやってみると、「$ bundle exec kitchen test」で簡単にCent6.4とかのテスト環境を作ってテストできました。
Vagrantが使える環境なら、test-kichenを使ってやってみるのが一番楽かと思います。ただ、Cent6.4のVMはできたのですが、Ubuntuのとあるboxでは以下の様なエラーでテストできませんでした。
vagrant-1.0.7/lib/vagrant/action/vm/customize.rb:23:in `block in call': A customization command failed: (Vagrant::Errors::VMCustomizationFailed) ["storagectl", "37f52d42-6398-4c79-b79f-83bf49571509", "--name", "SATA Controller", "--hostiocache", "on"] The following error was experienced: VBoxManage: error: Could not find a storage controller named 'SATA Controller' VBoxManage: error: Details: code VBOX_E_OBJECT_NOT_FOUND (0x80bb0001), component SessionMachine, interface IMachine, callee nsISupports VBoxManage: error: Context: "GetStorageControllerByName(Bstr(pszCtl).raw(), ctl.asOutParam())" at line 1151 of file VBoxManageStorageController.cpp VBoxManage: error: Couldn't find the controller with the name: 'SATA Controller'
こっちを直してもいいのですが、元々Vagrant環境はあったので、そっちで流せるようにしてみました。
やりかたは、自分がテストしたいcookbookがあるディレクトリに、
$ git clone https://github.com/btm/minitest-handler-cookbook.git $ rm -rf minitest-handler-cookbook/.git
してみましょう。んで、必要であればgitignoreでレポジトリ管理下にいれないようにする。
あとは、ドキュメントにあるように"minitest-handler"をrun_listに加えるのですが、僕の場合はvagrant環境があるので、Vagrantfileに以下のように記述します。
config.vm.provision :chef_solo do |chef| chef.cookbooks_path = "../masudaK-chef/cookbooks" chef.add_recipe "minitest-handler" chef.add_recipe "rbenv" end
これでminitestが「$ vagrant provision」のあとに走るようになります。
次に書き方を。
http://cookbooks/rbenv/files/default/tests/minitest/default_test.rb
を見て頂ければわかりますが、そこまでchefspecと乖離するほどの内容ではありません。
include MiniTest::Chef::Assertions include MiniTest::Chef::Context include MiniTest::Chef::Resources
という記述をして、
it 'installs git' do package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1") end
こんな感じで指定するだけです。ただ、chefspecと同じように、OSの違いとかを吸収したくなったわけです。
んで、書いてみたのがこちら。
require 'minitest/spec' describe 'rbenv' do include MiniTest::Chef::Assertions include MiniTest::Chef::Context include MiniTest::Chef::Resources case "#{node['platform']}" # node attribute would be get inside it block... when "centos" it 'installs git' do package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1") # minitest-chef-handler end it "Directory dir" do file("/home/vagrant/.rbenv/plugins").must_exist.with(:owner, "vagrant").and(:group, "vagrant").and(:mode, "0755") end when "ubuntu" it 'installs git' do package('git').must_be_installed.with(:version, "1:1.7.10.4-1ubuntu1") end it "Directory dir" do file("/home/vagrant/.rbenv/plugins").must_exist.with(:owner, "vagrant").and(:group, "vagrant").and(:mode, "0755") end end end
これ残念ながら、動きません…
コメントにも書いてありますが、nodeアトリビュートが使えるのはitブロックのなかだけなので、以下のようにしないといけないんですね。
it 'installs git' do case "nodeほにゃらら" when "centos" package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1") # minitest-chef-handler when "ubuntu" .... end
これだと、当然冗長になってしまうわけです。ので、解決策募集中ですが、ファイル分ける以外でできるんだろうか…
ひとまず、使ってみた感想としては以下。
- 書き方自体はrspec同様複雑ではない
- 同時に複数のVMでテストできるので、ディレクトリの移動とかなくて楽
- サーバに入って目で確認できるタイプ
- がっつりレシピ流してからテストするので、ごっつい
- nodeアトリビュートいじれない…
という感じでしょうか。現状chefspecのほうが僕にはあってる気がします。ただ、リソースの対応とかも増えてきたら、ちょっと考えるかもという状況です。いずれにしても、そこまで大きな書き方の差異はないですし、もっと柔軟にかけるかもなので、実は違うのかもしれません。
テスト・レシピを書く
とりあえず、テストは最低限書く環境ができました。rspecでもminitestでもどちらでもいいですが、テストを書きましょう。
項目としては、以下の様なイメージ。
- rbenvを入れるための必要最低限環境を整える。パッケージいれたり、ディレクトリ作ったり。
- rbenvをclone
- ruby-buildをclone
- .bashrcを書き換え
- attributeに渡せるものは渡す。
という感じでしょうか。本当は.bashrcと.zshrcが選べたり、インストールのディレクトリを変更できたり、まぁ色々できたほうがいいのでしょうが、ひとまずシンプルに書くことを念頭に置きます。
したがって、テストは、
- rbenvを入れるために必要なパッケージが入ってるかどうか(ex: git cloneするためにはgitパッケージが必要)
- 必要なディレクトリが作られてるかどうか(ex: ruby-buildを入れるためには$HOME/.rbenv/pluginsディレクトリが必要)
- .bashrcに必要な処理が入ってるか(ex: パスの追加とかが必要)
となるでしょう。 んで、書いたテストは前に示したように以下。
github.com
んで、それに合わせてレシピを書きます。 レシピは以下。
github.com
あとは、以下でテストします。
$ rspec -fd -c spec/default_spec.rb
出力は以下の様な感じ。
rbenv::default on a CentOS box Package git behaves like Package when install should install package at version "git" and "1.7.1-3.el6_4.1" Directory ruby-build behaves like Directory when create should create directory "/home/vagrant/.rbenv/plugins" should be owned by "vagrant" and "vagrant" should == 493 Template .bashrc behaves like Template when create should create file with content "/home/vagrant/.bashrc" and "rbenv" on a Ubuntu box Package git behaves like Package when install should install package at version "git" and "1:1.7.10.4-1ubuntu1" Directory ruby-build behaves like Directory when create should create directory "/home/vagrant/.rbenv/plugins" should be owned by "vagrant" and "vagrant" should == 493 Template .bashrc behaves like Template when create should create file with content "/home/vagrant/.bashrc" and "rbenv" Finished in 0.07047 seconds 10 examples, 0 failures
できるだけ自然言語として意味が通るように作ってみましたが、どうだろうか…
この辺はbest practiceを紹介してるここ
betterspecs.org
とかを見ながら、直していくのがよいでしょう。
一応、minitestにも対応してるので、vagrant provisionすればテストできますが、ubuntuの場合は、以下のように当然怒られてしまうので、ここは課題だったりします。
Finished tests in 0.492544s, 8.1211 tests/s, 8.1211 assertions/s. 1) Failure: test_0001_installs_git(rbenv) [/var/lib/gems/1.9.1/gems/minitest-chef-handler-0.6.8/lib/minitest-chef-handler/resources.rb:41]: The package does not have the expected version. Expected: "1.7.1-3.el6_4.1" Actual: "1:1.7.10.4-1ubuntu1"
終わりに
ひとまず、ここまではいかにテストを書いていくかを紹介してみました。
どう環境作るか分からないうちは謎なことが多かったですが、分かってくるとそこまでテストも複雑ではありませんし、いろんなOSの空きサーバ用意するのも大変なので、テスト書いてみようかなという気になってます。そういう環境がないと、なんか謎のバージョンとか入っちゃったりしてるんですよね。
ということで、レシピのテストはあるといいよというテンションで今回は終わらせたいと思います。次回の記事では、ここまで書いたレシピやテストをいかにリファクタしていくのかを紹介してみたいと思います。
次回作はこちら。
blog.masudak.net