Serverspecで実施するテストを適当に書いていたら、Rakefileとspec_helperが度重なる継ぎ足し建築で大きくなったので晒す。
この記事に出てくるファイルのリポジトリはこちら。 OpsRockin/serverspec_helper_example
テスト対象のはなし
相手はだいたいこんな感じ。
- ほぼ同じ構成で、staging, productionほか複数の環境がある
- 環境はプロビジョニングツールの管理下である
- ツールではミドルウェア構成に対応する形のroleとして定義が作られている
- 各ホストは大分類名(webとか)がついていて、roleの組み合わせで構築される
構築側のことはあまり気にせず、『仕様を探ってspecだけ書いてちょー』という条件の下でやってます。
Rake & spec_helperで吸収したかったこと
- 大分類が同じホストが環境別に複数あったり無かったり
- 共通のroleもある
- たまにホスト特有の値がある
- 任意に対象を絞りたい
- 環境の全ホスト
- 大分類別に全ホスト
- 特定のホスト
- たまにローカルのDockerで試したい。
ということで、roleに相当するspecを作って流せるようにしていった。
O'Reilly Japan - Serverspec から、 3.7 specファイルを複数のホストで共有 と 3.8 ホスト固有情報の利用 の応用・合わせ技ですね。
関連ファイルの構成
そのまま紹介とはいかないので、説明用に軽めにアレンジしてます。
2階層までのツリー。(※)はファイルで、印がないのはディレクトリ。
├── Gemfile(※)
├── Gemfile.lock(※)
├── Rakefile(※)
├── common_spec # 他プロジェクトとも共有するspec、gitのサブモジュールだったりする。
│ ├── common
│ ├── group
│ ├── mackerel-agent
│ └── user
├── ferture_spec # プロジェクト特有のspec、別に分けなくてもいい。
│ ├── fluentd
│ ├── nagios
│ ├── nginx
│ ├── postgresql
│ └── user
├── host_vars # ホスト特有の値をロードしたいときなどに使うなど。
│ ├── 10.48.1.31.yml(※)
│ └── 10.48.2.31.yml(※)
├── hosts_production(※) # production環境のホストとrole一覧
├── hosts_staging(※) # staging環境のホストとrole一覧
└── spec # helper他。これもサブモジュールなどでもいい。
└── spec_helper.rb(※)
Rakefile
早速なんかデカイ。多分余計な部分もある。
要は対象をyamlから抽出して、タスクを生成しています。
require 'rake'
require 'yaml'
require 'rspec/core/rake_task'
## 環境変数 SPEC_ENV で環境名を指定。RackやRailsのパクリ。
spec_env = ENV['SPEC_ENV']
if spec_env
path_candidate = File.expand_path("../hosts_#{spec_env}", __FILE__)
if File.exists?(path_candidate)
hosts_defined = path_candidate
else
raise RuntimeError, "\n======\nERROR: No hosts defined for #{spec_env}.\n======"
end
else
## SPEC_ENV が省略されたらとりあえずstagingとしてあつかう。
spec_env = 'staging'
hosts_defined = File.expand_path("../hosts_staging", __FILE__)
end
## 環境名に対応する定義ファイルを読む
properties = YAML.load_file(hosts_defined)
task :spec => 'spec:all'
task :default => :spec
namespace :spec do
## 定義ファイルから spec:大分類:ホスト名を全部作成する。spec:all 用
all_tasks = properties.each_pair.map { |key, values|
## 共通設定はホスト扱いしない。
next if key == 'shared_settings'
values[:hosts].map {|host| 'spec:' + key + ':' + host }
}.flatten.compact
## 全部実行するタスク (spec:all)
desc "all target for #{spec_env}"
task :all => all_tasks
## ホスト定義をまわす、大分類はmaster_rollって名前で扱う
properties.each_pair do |master_roll, entries|
## 共通設定は大分類扱いしない(spec_helperで使う)
next if master_roll == 'shared_settings'
## 大分類に割り当てられているroleを抽出する
role_pattern = entries[:roles].join(',')
namespace master_roll.to_sym do
hosts = entries[:hosts]
## 大分類別に全ホスト実行するタスク (spec:大分類:all)
desc "all target of #{master_roll} for #{role_pattern}"
task :all => hosts.map {|h| 'spec:' + master_roll + ':' + h }
## 大分類別に個別ホスト実行するタスクを定義する (spec:大分類:ホスト名)
hosts.each do |host|
desc "Run serverspec tests to #{master_roll}: #{host} for #{role_pattern}"
RSpec::Core::RakeTask.new(host.to_sym) do |t|
## どれかがこけても途中でやめない。
t.fail_on_error = false
ENV['TARGET_HOST'] = host
ENV['SPEC_ENV'] = spec_env
## specとcommon_specとferture_specをざっくり取って、定義ファイル上のロールに対応するspecを読み込ませる。
t.pattern = "{spec,common_spec,ferture_spec}/{#{role_pattern}}/**/*_spec.rb"
end
end
end
end
end
spec:大分類:all
のあたりがややこしいです。あると便利なので少々強引に。
ちなみに定義ファイル側のroleに対応するspecのディレクトリは無くてもスルーされます。ある分だけテスト実施。
yamlに大分類と所属ホストを書いた
じゃあ大分類がweb
, db
, press
の3つあるとします。
hosts_stagingとhosts_productionというファイルをそれぞれ書いてみるとこんな感じ。
大分類と所属ホスト、ロードするspec(roleにおおまかに対応)という構成です。 SSHの環境別接続設定に継ぎ足し感あります。
---
shared_settings:
:ssh_opts:
:user: operator
:keys: /Users/sawanoboriyu/.ssh/my_staging_key
:port: 22
:paranoid: false
web:
:hosts:
- 192.168.1.11
- 192.168.1.12
:roles:
- common
- group
- user/system
- user/web
- fluentd
- mackerel-agent
- nagios/nrpe
- nginx
db:
:hosts:
- 192.168.1.31
:roles:
- common
- group
- user/system
- user/db
- fluentd
- mackerel-agent
- nagios/nrpe
press:
:hosts:
- 192.168.1.21
:roles:
- common
- group
- user/system
- user/press
- mackerel-agent
- nagios/nrpe
- nagios/server
productionはsshの設定が違ったり、ちょっとホストが多かったり。
---
shared_settings:
:ssh_opts:
:user: opera_singer
:keys: /Users/sawanoboriyu/.ssh/my_production_key
:port: 9022
:paranoid: false
web:
:hosts:
- 10.48.1.11
- 10.48.1.12
- 10.48.2.11
- 10.48.2.12
:roles:
- common
- group
- user/system
- user/web
- fluentd
- mackerel-agent
- nagios/nrpe
- nginx
db:
:hosts:
- 10.48.1.31
- 10.48.2.31
:roles:
- common
- group
- user/system
- user/db
- fluentd
- mackerel-agent
- nagios/nrpe
press:
:hosts:
- 10.48.1.21
- 10.48.2.21
:roles:
- common
- group
- user/system
- user/press
- mackerel-agent
- nagios/nrpe
- nagios/server
こういう定義一覧をプロビジョニングツールと連動するかは時と場合によります。
両方修正が面倒な場合や、1つの修正ミスがプロビジョニングとテストの両方の結果に影響するのを避けたいか。ケースによって変えましょう。
今回のはテスト側にあるていど独立性を確保した感じですね。
rakeで対象を確認する
前項のyaml達をRakeに通すとどうなるか、タスク一覧を表示してみます。
SPEC_ENVを省略した場合はstatingが対象です。
全ホストを対象にする spec:all
, webの全ホストを対象のspec:web:all
や任意ホストのspec:web:192.168.1.11
をそれぞれ実行できるようになりました。
$ ./bin/rake -vT
rake spec:all # all target for staging
rake spec:db:192.168.1.31 # Run serverspec tests to db: 192.168.1.31 for common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe
rake spec:db:all # all target of db for common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe
rake spec:press:192.168.1.21 # Run serverspec tests to press: 192.168.1.21 for common,group,user/system,user/press,mackerel-agent,nagios/nrpe,nagios/server
rake spec:press:all # all target of press for common,group,user/system,user/press,mackerel-agent,nagios/nrpe,nagios/server
rake spec:web:192.168.1.11 # Run serverspec tests to web: 192.168.1.11 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:192.168.1.12 # Run serverspec tests to web: 192.168.1.12 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:all # all target of web for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
SPEC_ENV=production
で一覧を出すと、hosts_production
からタスクを生成します。ロードするspecも表示するようになってます。
$ SPEC_ENV=production ./bin/rake -vT
rake spec:all # all target for production
rake spec:db:10.48.1.31 # Run serverspec tests to db: 10.48.1.31 for common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe
rake spec:db:10.48.2.31 # Run serverspec tests to db: 10.48.2.31 for common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe
rake spec:db:all # all target of db for common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe
rake spec:press:10.48.1.21 # Run serverspec tests to press: 10.48.1.21 for common,group,user/system,user/press,mackerel-agent,nagios/nrpe,nagios/server
rake spec:press:10.48.2.21 # Run serverspec tests to press: 10.48.2.21 for common,group,user/system,user/press,mackerel-agent,nagios/nrpe,nagios/server
rake spec:press:all # all target of press for common,group,user/system,user/press,mackerel-agent,nagios/nrpe,nagios/server
rake spec:web:10.48.1.11 # Run serverspec tests to web: 10.48.1.11 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:10.48.1.12 # Run serverspec tests to web: 10.48.1.12 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:10.48.2.11 # Run serverspec tests to web: 10.48.2.11 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:10.48.2.12 # Run serverspec tests to web: 10.48.2.12 for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
rake spec:web:all # all target of web for common,group,user/system,user/web,fluentd,mackerel-agent,nagios/nrpe,nginx
spec_helper
spec_helperも肥大。
バックエンドのdocker or ssh(および環境別オプション)を選択可能にしたり、ホスト特有の値をロードという機能を継ぎ足したりしていったのでこんな感じ。
require 'serverspec'
require "docker"
require 'net/ssh'
require 'yaml'
case ENV['SPEC_BACKEND']
## 環境変数 SPEC_BACKEND がdocker|DOCKERだったらSSHじゃなくてDockerバックエンドを使う。
when "DOCKER", 'docker'
set :backend, :docker
set :docker_url, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
## Dockerでためす場合、DOCKER_IMAGEを指定する。
set :docker_image, ENV['DOCKER_IMAGE']
set :docker_container_create_options, {'Cmd' => ['/bin/sh']}
Excon.defaults[:ssl_verify_peer] = false
else
## デフォルトのバックエンドはSSH
set :backend, :ssh
set :request_pty, true
## このへんはRakeと一緒、定義ファイルを決定
spec_env = ENV['SPEC_ENV']
if spec_env
path_candidate = File.expand_path("../../hosts_#{spec_env}", __FILE__)
puts path_candidate
if File.exists?(path_candidate)
hosts_defined = path_candidate
else
raise RuntimeError, "\n======\nERROR: No hosts defined for #{spec_env}.\n======"
end
else
hosts_defined = File.expand_path("../../hosts_staging", __FILE__)
end
## spec_helperでもRakefile同様にホスト定義を読み込む
properties = YAML.load_file(hosts_defined)
host = ENV['TARGET_HOST']
mainrole = properties.select {|k,v| v[:hosts].include?(host) if v[:hosts] }.keys.first
## ホスト固有の値を書いたファイルがあればつかう。
host_vars = YAML.load_file(
File.expand_path("../../host_vars/#{host}.yml", __FILE__)
) if File.exists?(File.expand_path("../../host_vars/#{host}.yml", __FILE__))
spec_property = properties[mainrole]
spec_property[:host_vars] = host_vars ||= {}
## 環境変数DEBUGがあったらset_propertyに渡される値を表示する
puts spec_property.to_yaml if ENV['DEBUG']
set_property spec_property
## specの中で大分類を使うかもしれないと思ってとりあえず環境変数に突っ込んである。
ENV['SPEC_MAINROLE'] = mainrole
## 環境別SSH接続設定をマージしていく
options = Net::SSH::Config.for(host).merge(properties['shared_settings'][:ssh_opts])
### 大分類の下にもssh_optsがあったらそっちを優先で上書き
options.merge!(properties[mainrole][:ssh_opts]) if properties[mainrole][:ssh_opts]
options[:user] ||= 'root'
options[:keys] ||= File.expand_path("#{ENV['HOME']}/.ssh/my_staging_key" ,__FILE__)
set :host, options[:host_name] || host
set :ssh_options, options
# Disable sudo
# set :disable_sudo, true
RSpec.configure do |config|
config.color = true
config.tty = true
end
# Set environment variables
set :env, :LANG => 'C', :LC_MESSAGES => 'C'
end
いくらか冗長だったり、initの分が残ってたりです。やってることは単純。
とりあえずDockerで
大分類db
相当のイメージをビルドしたら、該当するspec:db:all
なりでspecを実行してきてもらったり。
テスト内容はダミーです。
$ SPEC_BACKEND=docker DOCKER_IMAGE=db ./bin/rake spec:db:all
(省略) rspec --pattern \{spec,common_spec,ferture_spec\}/\{common,group,user/system,user/db,fluentd,mackerel-agent,nagios/nrpe\}/\*\*/\*_spec.rb
Command "id"
exit_status
should eq 0
File "/etc"
should be directory
Finished in 4.18 seconds (files took 0.54121 seconds to load)
2 examples, 0 failures
実際使っているspec内ではいくつかの箇所でSPEC_BACKENDがDockerなら実施しないテストなどを分岐しました。
おわりに
Serverspec(ここはRSpecとしてもいいけど)は最終的に個々のタスクを別々にこなすようになっているので、好きなようにタスクを生成して乱雑にわたしてもきっちり動いてくれますね。