無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

AnsibleのGroupごと&状態ごとにServerspecのテストを書く

最近開発に参加したサービスで、サーバの構築はAnsibleのPlaybookで自動化されているのですが、構築後のテストは手作業で行っている環境がありました。それだけなら、正常にサービスが動作している状態をServerspecで定義すればテストできそうですが、このサービスはアップデートの際にいくつかのプロセスを停止する必要がありました。この停止後のチェックも手作業でした。

              一部プロセスの停止
    動作中 -----------------------> 更新可能
  (Running)                       (Updatable)
      ^                                |
      |                                |
      +--------------------------------+
              一部プロセスの再起動

このような状況で、Serverspecのテストをうまく書く方法について考えてみました。

要件

今回の事例について、以下のように要件を定義しました。ちなみに、片方だけ実現できれば良いという場合は、今回のサンプルコードを多少削れば実現できます。

  1. Ansibleのグループごとに、Serverspecでテストを書ける。また、一部のホストについて、そのホスト特有のテストを書くこともできる(Master-Slave構成の、Masterのみに実施したいテストなど)。
  2. 「動作中(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を組み合わせて使うための既存ツール

先行事例としては、@さんが作成されている「Ansibleの設定ファイルを使ってServerspecを実行するテンプレート作成用Gem(ansible_spec)」があります。できることは、リンク先のタイトルがほぼ完全な説明になってます。

今回はサーバの状態ごとにテストを分けたかった、すでに存在するAnsible Playbookと密結合にしたくなかった、などの理由で ansible_spec は使いませんでした。

Rakeに引数を渡す方法

Rakeに引数を渡す方法については、rake でのコマンドライン引数の扱い の説明がとても参考になりました。主に以下の2つの方法があるということで、今回は後者を採用しました。

  1. タスク毎に引数を定義し、受け取る。
  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ではインベントリファイルのパース処理を自前で作成しました。ただ、これは単純な構造のインベントリファイルしかサポートしていないので、導入先の環境によっては色々修正する必要がありそうです。ここの汎用性は今後の課題ですね。

参考文献