tk_ch’s blog

インフラエンジニアのブログ

Serverspecを複数のサーバに対して実行する

以下の記事で、Serverspecの実行コンテナを作成した。 tk-ch.hatenablog.com

今回、複数のサーバに対してServerspecでテストを実行したい。
しかし、初期状態のServerspecの設定だと、以下の問題がある。

  • specディレクトリ配下に、1サーバ毎にディレクトリを用意し、各ディレクトリ内に実行するテストを記載したspecファイルを配置しておく必要がある。
    同じ内容のテストを複数のサーバに実行する場合、同じファイルが格納されたディレクトリが複数存在することになり、作成や維持が面倒。

→Serverspecの設定、ディレクトリ構成を変更してこれら問題を解消してみる。
ちなみに本記事で使ったプレイブックはここにある。

環境

  • DockerホストのOS:RockyLinux8.6
  • 管理対象サーバ(Serverspecの実行先)のOS:RockyLinux8.6
  • Serverspec:2.42.2

実施内容

以下を参考に、ディレクトリ構成とファイルの中身を変更した。

変更点は以下の通り。

  • specファイルのディレクトリをサーバ単位からロール(共通、DBサーバ、WEBサーバなど)単位に変更。
  • 実行対象のサーバとロールについては「hosts.yml」というファイルに記載して、Rakefileをこのファイルを読み込んで処理するように変更。
  • サーバやロールごとに値が異なるテスト内容については、変数に置き換えることでspecファイルに記載するテストコードは共通化する。
    変数はpropertyという名前の変数を使って参照する。
  • サーバごとに異なる変数は「hosts.yml」、ロールごとに異なる変数は「properties.yml」というファイルに記載する。
    spec_helper.rbに、これらファイルを読み込んで変数として使えるように設定する処理を追加。

変更前のディレクトリ構成と

serverspec-initコマンドで作成したままのディレクトリ構成。
以下のようになっている。

# tree -a --charset=c /root/serverspec
/root/serverspec
|-- .rspec
|-- Rakefile
`-- spec
    |-- 192.168.10.123
    |   `-- sample_spec.rb
    `-- spec_helper.rb

2 directories, 4 files

変更後のディレクトリ構成とファイル

以下のように変更した。

# tree -a --charset=c /root/serverspec
/root/serverspec
|-- .rspec
|-- Rakefile
|-- hosts.yml
|-- properties.yml
`-- spec
    |-- base
    |   |-- dns_spec.rb
    |   `-- sshd_spec.rb
    |-- db
    |   `-- postgres_spec.rb
    `-- spec_helper.rb

3 directories, 8 files

各ファイルの中身は以下の通り。 Rakefile

require 'rake'
require 'rspec/core/rake_task'
require 'yaml' # YAMLファイルを処理するために必要

# 実行対象が記載されたYAMLファイルを読み込む
hosts = YAML.load_file('hosts.yml')

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

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

spec_helper.rb

require 'serverspec'
require 'net/ssh'
require 'highline/import'
require 'yaml'  # YAMLファイルを処理するために必要

# サーバごとの変数が記載されたYAMLファイルを読み込む
host_properties = YAML.load_file('hosts.yml')
# ロールごとの変数が記載されたYAMLファイルを読み込む
common_properties = YAML.load_file('properties.yml')

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']

# 対象サーバの変数と、実行されるロールの変数をマージして、Property情報に格納
properties = host_properties[host]
properties[:roles].each do |r|
  properties = common_properties[r].merge(host_properties[host]) if common_properties[r]
end
set_property properties

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

options[:user] ||= Etc.getlogin

if ENV['ASK_LOGIN_PASSWORD']
  options[:password] = ask("\nEnter login password: ") { |q| q.echo = false }
else
  options[:password] = ENV['LOGIN_PASSWORD']
end

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

hosts.yml

192.168.10.123:
  :roles:
    - base
192.168.10.124:
  :roles:
    - base
    - db
  :db_user: root

properties.yml

base:
  :dns_server: 10.0.0.2

sshd_spec.rb

require 'spec_helper'

describe service('sshd'), :if => os[:family] == 'redhat' do
  it { should be_running }
end

dns_spec.rb

require 'spec_helper'

describe file('/etc/resolv.conf') do
  its(:content) { should match /^nameserver #{property[:dns_server]}/ }
end

postgres_spec.rb

require 'spec_helper'

describe user("#{property[:db_user]}") do
  it { should exist }
end

実行してみる

まず、定義されたタスクを表示する。
全サーバを対象でも、サーバ単位でも実行できることが分かる。

# rake -T
rake spec               # Run serverspec to all hosts (=spec:all)
rake spec:192.168.10.123  # Run serverspec to 192.168.10.123
rake spec:192.168.10.124  # Run serverspec to 192.168.10.124
rake spec:all           # Run serverspec to all hosts (=spec)

全サーバを対象にServerspecを実行する。

# LOGIN_PASSWORD=実行対象サーバへSSHする際の認証パスワード

# rake spec LOGIN_PASSWORD=$LOGIN_PASSWORD
#####################################################
  Target host : 192.168.10.123
  Role        : base
#####################################################
/usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern spec/\{base\}/\*_spec.rb
/usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user

File "/etc/resolv.conf"
  content
    is expected to match /^nameserver 10.0.0.2/

Service "sshd"
  is expected to be running

Finished in 0.16813 seconds (files took 2.38 seconds to load)
2 examples, 0 failures

#####################################################
  Target host : 192.168.10.124
  Role        : base,db
#####################################################
/usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern spec/\{base,db\}/\*_spec.rb
/usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user

File "/etc/resolv.conf"
  content
    is expected to match /^nameserver 10.0.0.2/

Service "sshd"
  is expected to be running

User "root"
  is expected to exist

Finished in 0.28116 seconds (files took 2.42 seconds to load)
3 examples, 0 failures

→以下のことが確認できる。

  • host.ymlに定義した通り、192.168.10.123にはbaseロール、192.168.10.124にはbaseロールとdbロールのテストが実行されている。
  • hosts.ymlとproperties.ymlに定義した変数(DNSサーバのIPと、DB接続用ユーザ)がテスト実行の際に展開、使用されている。

まとめ

  • 複数サーバを対象に、管理しやすい形でServerspecを実行できた。
  • RakeFileやspec_helper.rbを工夫することで、柔軟にServerspecを使用することができる。
    Serverspecというより、Rubyの知識が必要。
  • Serverspecの対象サーバとロールをAnsibleのファイルから読み込めるansible_specというツールがあるらしい。
    次はこれを試してみたい。

参考文献