📝

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は次のような構成。

hosts/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。

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を補足すれば、途中で失敗しても止まらずに全ホスト分のテストを続けられる。

Rakefile(修正版)
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