Serverspecで複数サーバをまとめてテストしたい ─ 途中で失敗してもテストを止めない回避策
はじめに
Serverspecの公式ドキュメントでは、
hosts.ymlをループして複数ホスト分のテストタスクを動的に生成する例が紹介されている。
(参考:https://serverspec.org/advanced_tips.html)
実際にこの構成を試してみると、1台目のテストで失敗した時点でテスト全体が停止(2台目のテストが実行されない)してしまった。
この記事では、その原因と、すべてのホストを最後までテストする方法をまとめる。
検証環境
2台のサーバを用意。
それぞれcommonおよびapacheのロールを割り当てている。
├── server1 (192.168.10.2)
│ └── role: common
└── server2 (192.168.10.3)
├── role: common
└── role: apache
hosts.ymlは次のような構成。
server1:
host: 192.168.10.2
user: root
ssh_options:
# 鍵パスは例示(実際の環境に合わせて変更)
keys: [~/.ssh/id_rsa]
roles:
- common
server2:
host: 192.168.10.3
user: root
ssh_options:
# 鍵パスは例示(実際の環境に合わせて変更)
keys: [~/.ssh/id_rsa]
roles:
- common
- apache
1. まずはサンプル通りに書いてみる
Serverspec公式のadvanced_tipsでは、
hosts.ymlをもとにホストごとにRSpecタスクを自動生成するサンプルが掲載されている。
以下はそれを参考にしたRakefile。
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
host_config = YAML.load_file('hosts/hosts.yml')
namespace :spec do
desc 'Run all Serverspec tests'
task :all => host_config.keys.map { |h| "spec:#{h}" }
host_config.each do |name, info|
desc "Run Serverspec tests for #{name}"
RSpec::Core::RakeTask.new(name.to_sym) do |t|
ENV['TARGET_HOST'] = info['host']
ENV['TARGET_USER'] = info['user']
ENV['SSH_KEY'] = File.expand_path(info.dig('ssh_options', 'keys', 0))
roles = info['roles'] || []
role_paths = roles.join(',')
t.pattern = "spec/{#{role_paths}}/**/*_spec.rb"
t.rspec_opts = ENV['RSPEC_OPTS'] || '--color --format documentation'
end
end
end
desc 'Run all Serverspec tests for all hosts'
task :spec => 'spec:all'
2. 1台目のテストが通った場合
まずは1台目(server1)が問題なく通過したケース。

この場合、続けて2台目(server2)のテストも自動で実行される。
ここまでは問題なし。
3. 1台目のテストで失敗した場合
一方で、1台目が失敗すると挙動が変わる。
2台目のテストは実行されず、Rakeタスク全体が停止してしまう。

理由は、RSpec::Core::RakeTaskが内部でexit(1)を呼んでいるため。
Rakeタスク全体が例外終了し、次のホストに進まない。
4. テストを最後まで実行できるように修正
ここでSystemExitを補足すれば、途中で失敗しても止まらずに全ホスト分のテストを続けられる。
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
host_config = YAML.load_file('hosts/hosts.yml')
namespace :spec do
desc 'Run Serverspec tests for all hosts (continue on failure)'
task :all do
host_config.each do |name, _|
begin
Rake::Task["spec:#{name}"].invoke
rescue SystemExit => e
puts "[WARN] #{name} failed (exit #{e.status}), continuing..."
ensure
Rake::Task["spec:#{name}"].reenable
end
end
end
host_config.each do |name, info|
RSpec::Core::RakeTask.new(name.to_sym) do |t|
ENV['TARGET_HOST'] = info['host']
ENV['TARGET_USER'] = info['user']
ENV['SSH_KEY'] = File.expand_path(info.dig('ssh_options', 'keys', 0))
roles = info['roles'] || []
role_paths = roles.join(',')
t.pattern = "spec/{#{role_paths}}/**/*_spec.rb"
t.rspec_opts = '--color'
end
end
end
desc 'Run all Serverspec tests'
task :spec => 'spec:all'
5. 修正版の実行結果
修正版では、1台目が失敗しても最後までテストが流れる。

画像のように、server1が失敗してもserver2のテストが続行されている。
参考:
まとめ
Serverspecの公式構成は基本的に1ホスト単位での実行を前提としている。
複数ホストを一括で回したい場合は、SystemExitを補足してループ実行することで対処可能だ。
ただし、レポート出力を考えると、ホスト単位でテスト実行する方が管理しやすい場面も多い気がする。
Discussion