最近開発に参加したサービスで、サーバの構築はAnsibleのPlaybookで自動化されているのですが、構築後のテストは手作業で行っている環境がありました。それだけなら、正常にサービスが動作している状態をServerspecで定義すればテストできそうですが、このサービスはアップデートの際にいくつかのプロセスを停止する必要がありました。この停止後のチェックも手作業でした。
一部プロセスの停止 動作中 -----------------------> 更新可能 (Running) (Updatable) ^ | | | +--------------------------------+ 一部プロセスの再起動
このような状況で、Serverspecのテストをうまく書く方法について考えてみました。
要件
今回の事例について、以下のように要件を定義しました。ちなみに、片方だけ実現できれば良いという場合は、今回のサンプルコードを多少削れば実現できます。
- Ansibleのグループごとに、Serverspecでテストを書ける。また、一部のホストについて、そのホスト特有のテストを書くこともできる(Master-Slave構成の、Masterのみに実施したいテストなど)。
- 「動作中(Running)」、「更新可能(Updatable)」など、ホストの状態に応じた複数のテストを書ける。
最終的に構築される環境
Rakefileの書き方を工夫して、以下のいずれかのコマンドでServerspecのテストを実行できるようにします。
% bundle exec rake inventory=<inventory file> status=<status name> spec:all % bundle exec rake inventory=<inventory file> status=<status name> spec:<group name> % bundle exec rake inventory=<inventory file> status=<status name> spec:<host name>
テストの内容を記載した*_spec.rbファイルは、以下のディレクトリに配置します。ちなみに、Ansibleのインベントリファイルは、Serverspecのディレクトリの外にあっても問題ありません。
serverspec/ ├ Gemfile ├ Gemfile.lock ├ Rakefile ├ <inventory file> ├ spec/ │ ├ spec_helper.rb │ ├ <group name>/ │ │ └ <status name>/ │ │ └ *_spec.rb │ └ hosts/ │ └ <host name>/ │ └ <status name>/ │ └ *_spec.rb └ vendor/
構築手順
以下の例では、~/serverspec ディレクトリ以下に、必要なすべてのファイルを配置するものとします。bundler を使わない場合は bundle exec を省いてください。
1. ~/serverspec ディレクトリの作成
2. ~/serverspec/Gemfile を作成し、以下の内容を記載
source 'https://rubygems.org' gem 'serverspec' gem 'rake'
3. bundle installコマンドの実行
% bundle install --path vendor/bundle Fetching gem metadata from https://rubygems.org/....... Fetching version metadata from https://rubygems.org/.. Resolving dependencies... Installing rake 10.4.2 Installing diff-lcs 1.2.5 Installing multi_json 1.11.2 Installing net-ssh 2.9.2 Installing net-scp 1.2.1 Installing net-telnet 0.1.1 Installing rspec-support 3.3.0 Installing rspec-core 3.3.2 Installing rspec-expectations 3.3.1 Installing rspec-mocks 3.3.2 Installing rspec 3.3.0 Installing rspec-its 1.2.0 Installing sfl 2.2 Installing specinfra 2.43.4 Installing serverspec 2.23.1 Using bundler 1.10.4 Bundle complete! 2 Gemfile dependencies, 16 gems now installed. Bundled gems are installed into ./vendor/bundle.
4. serverspec-initコマンドで大枠を作成(サンプルが不要ならパスして良い)
% bundle exec 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: sample.example.com + spec/ + spec/sample.example.com/ + spec/sample.example.com/sample_spec.rb + spec/spec_helper.rb + Rakefile + .rspec
5. AnsibleのインベントリファイルからRSpecのタスクを自動生成できるように、Rakefileを編集
RakefileはGistにアップロードしたので、こちらをご参考ください。
Serverspec Rakefile for creating tasks from Ansible inventory file with server status
6. 上記のディレクトリ構成に従って、Ansibleのグループごと、またはホストごとの設定を *_spec.rb ファイルに記載
テストの作成方法は、普通にServerspecを使う場合と特に変わりないと思います。
7. rake -T コマンドで動作確認
インベントリファイルとしては、今のところ以下のような簡単なものだけをサポートしています。以下は Inventory — Ansible Documentation から抜粋した例です。
mail.example.com [webservers] foo.example.com bar.example.com [dbservers] one.example.com two.example.com three.example.com
この状態で rake -T コマンドを実行すると、正しくタスクを生成できているか確認できます。設定に成功した場合は、以下のように出力されるはずです。
% bundle exec rake inventory=./hosts status=running -T rake spec:bar.example.com # Run tests for host 'bar.example.com' rake spec:dbservers:one.example.com # Run tests for group 'dbservers' rake spec:dbservers:three.example.com # Run tests for group 'dbservers' rake spec:dbservers:two.example.com # Run tests for group 'dbservers' rake spec:foo.example.com # Run tests for host 'foo.example.com' rake spec:mail.example.com # Run tests for host 'mail.example.com' rake spec:one.example.com # Run tests for host 'one.example.com' rake spec:three.example.com # Run tests for host 'three.example.com' rake spec:two.example.com # Run tests for host 'two.example.com' rake spec:webservers:bar.example.com # Run tests for group 'webservers' rake spec:webservers:foo.example.com # Run tests for group 'webservers'
Rakefileの解説
AnsibleとServerspecを組み合わせて使うための既存ツール
先行事例としては、@volanjaさんが作成されている「Ansibleの設定ファイルを使ってServerspecを実行するテンプレート作成用Gem(ansible_spec)」があります。できることは、リンク先のタイトルがほぼ完全な説明になってます。
今回はサーバの状態ごとにテストを分けたかった、すでに存在するAnsible Playbookと密結合にしたくなかった、などの理由で ansible_spec は使いませんでした。
Rakeに引数を渡す方法
Rakeに引数を渡す方法については、rake でのコマンドライン引数の扱い の説明がとても参考になりました。主に以下の2つの方法があるということで、今回は後者を採用しました。
- タスク毎に引数を定義し、受け取る。
- 環境変数経由で受け取る。
記法としては、(rake spec:all[running]
みたいに)鍵括弧内に引数を記載する前者の方が簡潔なのですが、この方法は「直接実行されるタスクしか引数を受け取れない」という欠点があります。今回は、タスク名でグループを指定したら、そのグループに属するホストすべてにテストを実行する、という動作を実現したかったので、この方式は採用できませんでした。
インベントリファイルの読み込み
インベントリファイルはINIファイルに似た形式なので、INIファイル用のパーサで読み込めるかと思い、PythonやRubyでのINIファイルの参照 - Qiita を読んで inifile を試してみました。
で、結果としては、key=value
形式になっていない行(つまりkey
だけの行)を読み込もうとすると、以下のようなエラーが出て駄目でした。
irb(main):004:0> hosts = IniFile.load('./hosts') IniFile::Error: Could not parse line: "web01" from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:578:in `error' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:532:in `block in parse' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:515:in `each_line' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:515:in `parse' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:400:in `parse' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:128:in `block in read' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:128:in `open' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:128:in `read' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:80:in `initialize' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:31:in `new' from /Library/Ruby/Gems/2.0.0/gems/inifile-3.0.0/lib/inifile.rb:31:in `load' from (irb):4 from /usr/bin/irb:12:in `<main>'
結局、今回のRakefileではインベントリファイルのパース処理を自前で作成しました。ただ、これは単純な構造のインベントリファイルしかサポートしていないので、導入先の環境によっては色々修正する必要がありそうです。ここの汎用性は今後の課題ですね。
参考文献
- Ansibleの設定ファイルを使ってServerspecを実行するテンプレート作成用Gem(ansible_spec)を作りました。 - Qiita
- Ansible と Serverspec を組み合わせて使う : あかぎメモ
- rake でのコマンドライン引数の扱い - 君の瞳はまるでルビー - Ruby 関連まとめサイト
- Inventory — Ansible Documentation
- PythonやRubyでのINIファイルの参照 - Qiita
- Serverspec用のspec_helperとRakefileのサンプルをひとつ - Qiita
- Ruby - serverspecの高度なヒント - Qiita