Serverspecのテストケースをロール単位で管理する

serverspec-initでできるデフォルトのRakefileだとホスト名ごとにディレクトリ掘ってその中にテストケースのspecファイルを入れていくのだけど、サーバ複数あるとspecファイルコピペしないといけないのイマイチなので、Chefのロール単位ぐらいで管理できるとよいかなぁというかんじ。

調べてみたところserverspec作者のmizzyさんのブログにサンプルコードあったので、これを参考にやってみた。
serverspec のテストをホスト間で共有する方法 - Gosuke Miyashita

今回のディレクトリ構成は大体こんなかんじでロールごとにディレクトリ掘って、その中にcookbookごとにspecファイル入れてくイメージ。

.
├── Rakefile
├── spec
│          ├── ap
│          │        ├── mysqlclient_spec.rb
│          │        ├── nodejs_spec.rb
│          │        ├── rvm_spec.rb
│          │        └── sqlite_spec.rb
│          ├── base
│          │        ├── build-essential_spec.rb
│          │        └── git_spec.rb
│          ├── db
│          │        └── mysql_spec.rb
│          └── spec_helper.rb

Rakefileはこんなかんじで、はじめにホストの定義を書いて、それに対応するrakeタスクを機械的に生成する。ホスト定義は台数増えてきたら別ファイルに分けた方がよいかもしんない。

require 'rake'
require 'rspec/core/rake_task'

hosts = [
  {
    :name   => 'commitm-dev',
    :roles  => %w( base ap db )
  },
  {
    :name   => 'commitm-ap01',
    :roles  => %w( base ap )
  }
]

hosts = hosts.map do |host|
  {
    :name       => host[:name],
    :short_name => host[:name].split('.')[0],
    :roles      => host[:roles]
  }
end

desc "Run serverspec to all hosts"
task :serverspec => 'serverspec:all'

namespace :serverspec do
  task :all => hosts.map { |h| 'serverspec:' + h[:short_name] }
  hosts.each do |host|
    desc "Run serverspec to #{host[:name]}"
    RSpec::Core::RakeTask.new(host[:short_name].to_sym) do |t|
      ENV['TARGET_HOST'] = host[:name]
      t.pattern = 'spec/{' + host[:roles].join(',') + '}/*_spec.rb'
    end
  end
end

short_nameのところはnameがFQDNのときにタスク名がFQDNになるのめんどいという意図だろうけど、今回の例だとあんまり意味ないが一応入れてある。

ちなみに参考元のブログ記事ではspec_helperも記載されてるようなのだけど、spec_helperが旧バージョンっぽい香りがしたので、そのまま使わず。今回使ってるserverspec 2.8.2のserverspec-initで生成されたテンプレベースで、request_ptyの設定だけいじったら動いた。request_pty自体は今回のロール分割の話とは本質的に関係ないのだけど、参考までにspec_helper.rbのコードも貼っておく。

require 'serverspec'
require 'net/ssh'

set :backend, :ssh

if ENV['ASK_SUDO_PASSWORD']
  begin
    require 'highline/import'
  rescue LoadError
    fail "highline is not available. Try installing it."
  end
  set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
else
  set :sudo_password, ENV['SUDO_PASSWORD']
end

host = ENV['TARGET_HOST']

options = Net::SSH::Config.for(host)

options[:user] ||= Etc.getlogin

set :host,        options[:host_name] || host
set :ssh_options, options

set :request_pty, true

# Disable sudo
# set :disable_sudo, true


# Set environment variables
# set :env, :LANG => 'C', :LC_MESSAGES => 'C'

# Set PATH
# set :path, '/sbin:/usr/local/sbin:$PATH'

request_ptyについて、ssh先がEC2のAmazon Linuxだとデフォルトで/etc/sudoersでrequirettyが設定されててsudo制限されてることが原因のようで、テンプレのままだとsudoがこけた。Linux側の設定をいじらなくてもserverspecのspec_helper側でrequest_ptyをtrueにするとうまく動くっぽい。ちなみにこのオプションtrueにすると標準出力と標準エラー出力が1つになるので、テストで標準出力や標準エラー出力をチェックしてる場合は注意。

Rakefileの確認のためにrake -Tするとホストごとにタスクが定義されてるのが分かる。

$ rake -T
rake serverspec               # Run serverspec to all hosts
rake serverspec:commitm-ap01  # Run serverspec to commitm-ap01
rake serverspec:commitm-dev   # Run serverspec to commitm-dev

あとは実行したいタスクを指定して実行すればOK。

$ rake serverspec:commitm-dev

うん。よさげなかんじじゃないでしょうか。