オライリーのServerspec本を読んで、こんなこともできるよねということで試してみました。
Serverspecについての紹介は省略します。
Serverspecはデフォルトの場合、テスト対象のサーバ毎にフォルダが作られ、その中にテストコードを書いていきます。
その場合、サーバ間で同様のテストを実施したい時に重複して同じテストコードを書く必要があります。
それでは効率がよくないので、できればサーバ用途毎にテストコードをまとめて管理したいといったケースがあります。
そのような場合には、以下の記事にあるようにRakefileをカスタマイズすることでどのホストに対してどのテストコードを実行するかを柔軟に扱うことができるようになります。
http://mizzy.org/blog/2013/05/12/1/
http://thinkit.co.jp/story/2014/09/04/5212
これらの例は静的にどのホストがどのロールに割り当てられているかを定義しています。
これを少し発展させて、今回は外部ツールで管理されているホスト情報を自動取得し、テストを自動実行することを試してみます。
ZabbixとServerspec
今回は外部ツールとしてZabbixを対象に考えてみます。
Zabbixに登録されているホスト情報(IPアドレスやDNS名等)やホストグループの情報を自動取得してテストのターゲットとなるホストを決定し、どのロールのテストを実行すべきかを決めます。
こういった連携が実現できれば、「Zabbixで監視を行う」、何か起こった時に「Serverspecでサーバ内部の状態を確認する」という一連の処理がよりスムーズにできるようになるかもしれません。
Zabbix側の設定
Zabbixに登録されているホストのインタフェース情報とホストグループの情報を収集するように設定するので、まずZabbixにテスト対象のホストを登録します。
例えば今回は以下のようなホストが登録されているとします。
- ホスト名: test-server-01
- 所属ホストグループ: DB
- インタフェース:
- IPアドレス: 10.2.2.1
- DNS名: test-server-01
- UseIP: 0
- ホスト名: test-server-02
- 所属ホストグループ: Web
- インタフェース:
- IPアドレス: 10.2.2.2
- DNS名: なし
- UseIP: 1
- ホスト名: Zabbix server
- 所属ホストグループ: Zabbix servers
- インタフェース:
- IPアドレス: 10.2.2.10
- DNS名: なし
- UseIP: 1
Zabbixのホストリスト取得スクリプト作成
以下のような形式でホストおよびホストグループの情報を返すようにRubyのスクリプトを作成します。
Zabbix APIを使うことで、ホストの一覧や各ホストが所属しているホストグループ情報を取得することが可能です。今回はこのZabbixAPIを使ってホストリストを生成するスクリプトを作りました。
#!/bin/env ruby
require 'net/http'
require 'json'
require 'optparse'
options = ARGV.getopts("", "url:http://localhost/zabbix/api_jsonrpc.php", "username:Admin", "password:zabbix")
ZABBIX_URL = options["url"]
ZABBIX_USER = options["username"]
ZABBIX_PASSWORD = options["password"]
ZABBIX_URI = URI.parse(ZABBIX_URL)
ZABBIX_API_HEADER = {'Content-Type' =>'application/json-rpc'}
API_REQUEST_BASE = Net::HTTP::Post.new(ZABBIX_URI.request_uri, initheader = ZABBIX_API_HEADER)
api_connection = nil
# api_connection is user.login returned value.
# {"jsonrpc"=>"2.0", "result"=>"099adde3cea983cece7c0f03195791eb", "id"=>1}
def login
params = {user: ZABBIX_USER, password: ZABBIX_PASSWORD}
API_REQUEST_BASE.body = {method: "user.login", auth: nil, params: params, id: 1, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
def api_access(uri, request)
response = nil
http = Net::HTTP.new(uri.host, uri.port)
http.start do |h|
response = h.request(request)
end
return JSON.parse(response.body)
end
def get_host_list(api_connection)
params = {output: "extend", selectGroups: "true", selectInterfaces: "extend"}
API_REQUEST_BASE.body = {method: "host.get", auth: api_connection["result"], params: params, id: 2, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
def get_hostgroup_list(api_connection, hostgroup_ids)
params = {output: "extend", groupids: hostgroup_ids}
API_REQUEST_BASE.body = {method: "hostgroup.get", auth: api_connection["result"], params: params, id: 3, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
## main ##
result = []
api_connection = login()
hosts = get_host_list(api_connection)
hosts["result"].each do |h|
host = {}
h["interfaces"].each do |interface|
if interface["useip"] == "1" then
host = {:name => interface["ip"]}
else
host = {:name => interface["dns"]}
end
break
end
groups = get_hostgroup_list(api_connection, h["groups"].map!{|group| group.values}.flatten)
group_names = groups["result"].map {|group| group["name"]}
host[:roles] = group_names
result << host
end
puts JSON.dump(result)
このスクリプトを以下のように実行し、hosts.jsonファイルとして保存します。
$ ./get_zabbix_hosts.rb --username Admin --password zabbix --url http://10.2.2.10/zabbix/api_jsonrpc.php > hosts.json
hosts.jsonの中身は以下のような感じになります。
[
{
"name":"10.2.2.2",
"roles":["Web"]
},
{
"name":"10.2.2.10",
"roles":["Zabbix servers"]
},
{
"name":"test-server-01",
"roles":["DB"]
}
]
Rakefileのカスタマイズ
serverspec-initを実行するとデフォルトのRakefileが生成されます。
このファイルを以下のように変更します。
hosts.jsonを読み込んで各ホストのnameで指定された値をターゲットホストとして設定します。
そのホストに対して実行すべきテストコードは
spec/ホストグループ名/*_spec.rbに該当するテストコード全てと設定しています。
require 'rake'
require 'rspec/core/rake_task'
require 'json'
task :spec => 'spec:all'
task :default => :spec
namespace :spec do
hosts = JSON.load(File.new('hosts.json'))
task :all => hosts.map {|h| h['name'] }
task :default => :all
hosts.each do |host|
name = host['name']
desc "Run serverspec tests to #{name}"
RSpec::Core::RakeTask.new(name) do |t|
ENV['TARGET_HOST'] = name
t.pattern = "spec/{#{host['roles'].join(',')}}/*_spec.rb"
end
end
end
そのため、例えば今回の場合、spec以下に以下のようなディレクトリ構成でテストコードを配置すれば、test-server-01ではDBディレクトリ以下のテストコードのみ、test-server-02ではWebディレクトリ以下のテストコードのみが実行されるといった具合でロールベースでテストコードの管理が実現できます。
├ Rakefile
├ get_zabbix_hosts.rb
├ hosts.json
└ spec
├ DB
│ └ postgresql_spec.rb
├ Web
│ └ apache_spec.rb
├ Zabbix servers
│ └ zabbix_server_spec.rb
└ spec_helper.rb
テスト実行
$ rake spec:all (rake specだけでもOK)
$ rake spec:test-server-01
Rakefile内で全て完結させる
また、上記でget_zabbix_hosts.rbで別で切り出した処理を以下のように全てRakefileに含めて、rake spec実行時に全処理を一括実行させるといったことももちろん可能です。
require 'rake'
require 'rspec/core/rake_task'
require 'json'
require 'net/http'
ZABBIX_URL = "http://localhost:50008/zabbix/api_jsonrpc.php"
ZABBIX_USER = "Admin"
ZABBIX_PASSWORD = "zabbix"
ZABBIX_URI = URI.parse(ZABBIX_URL)
ZABBIX_API_HEADER = {'Content-Type' =>'application/json-rpc'}
API_REQUEST_BASE = Net::HTTP::Post.new(ZABBIX_URI.request_uri, initheader = ZABBIX_API_HEADER)
api_connection = nil
# api_connection is user.login returned value.
# {"jsonrpc"=>"2.0", "result"=>"099adde3cea983cece7c0f03195791eb", "id"=>1}
def login
params = {user: ZABBIX_USER, password: ZABBIX_PASSWORD}
API_REQUEST_BASE.body = {method: "user.login", auth: nil, params: params, id: 1, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
def api_access(uri, request)
response = nil
http = Net::HTTP.new(uri.host, uri.port)
http.start do |h|
response = h.request(request)
end
return JSON.parse(response.body)
end
def get_host_list(api_connection)
params = {output: "extend", selectGroups: "true", selectInterfaces: "extend"}
API_REQUEST_BASE.body = {method: "host.get", auth: api_connection["result"], params: params, id: 2, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
def get_hostgroup_list(api_connection, hostgroup_ids)
params = {output: "extend", groupids: hostgroup_ids}
API_REQUEST_BASE.body = {method: "hostgroup.get", auth: api_connection["result"], params: params, id: 3, jsonrpc: "2.0"}.to_json
return api_access(ZABBIX_URI, API_REQUEST_BASE)
end
## main ##
result = []
api_connection = login()
hosts = get_host_list(api_connection)
hosts["result"].each do |h|
host = {}
h["interfaces"].each do |interface|
if interface["useip"] == "1" then
host = {:name => interface["ip"]}
else
host = {:name => interface["dns"]}
end
break
end
groups = get_hostgroup_list(api_connection, h["groups"].map!{|group| group.values}.flatten)
group_names = groups["result"].map {|group| group["name"]}
host[:roles] = group_names
result << host
end
hosts = JSON.dump(result)
task :spec => 'spec:all'
task :default => :spec
namespace :spec do
hosts = JSON.load(File.new('hosts.json'))
p hosts
task :all => hosts.map {|h| h['name'] }
task :default => :all
hosts.each do |host|
name = host['name']
desc "Run serverspec tests to #{name}"
RSpec::Core::RakeTask.new(name) do |t|
ENV['TARGET_HOST'] = name
t.pattern = "spec/{#{host['roles'].join(',')}}/*_spec.rb"
end
end
end
Serverspec本では
今回のこのカスタマイズはServerspec本の中のサンプルをもとにやってみました。
書籍の中ではConsulで管理されているホスト一覧情報を取得して自動実行させたり、AWSのインスタンス情報を取得して自動実行させたりといった情報が掲載されています。
非常にいろんな情報が掲載されているのぜひ一読されることをおすすめします。
まとめ
基本的にはServerspecはRubyで処理を書けばどんなカスタマイズでも可能です。
今回のように様々なツールと連携させることで効果的な管理が実現できるかもしれません。
お試しください。