Serverspec と Infrataster でサーバのテストをする
サーバの構築・運用の効率化の為に Test-Driven Infrastructure をする手法として、
Serverspec が登場して 1 年近く経ちました。
そして最近、Infrastructure Behavior Testing Framework として、
Infrataster が登場しました。
今日は、上記で紹介した 2 つを組み合わせて使用し、
実際にどのようにサーバのテストを行うかについて書きます。
書くこと・書かないこと
- 書くこと
- Serverspec と Infrataster を両方使った Test-Driven Infrastructure の一手法に関して
今日書くのは、Serverspec と Infrataster を組み合わせることで、
Serverspec がカバーしている領域と Infrataster がカバーしている領域の両方をテストする一手法に関してです。
- 書かないこと
- Test-Driven Infrastructure についてのベストプラクティス
- TDD や BDD と言ったそもそものテスト手法について
これらは、個々人やプロジェクト単位で、ベストプラクティス・手法が異なると思っています。
その為、ここに書いてある事が必ずもベストではありません。(もちろん、マッチする人も居るかもしれません)
また、そもそもの言葉の定義や、Test-Driven Infrastructure のあり方について等は書きません。
前提
インストール
まずは、テストを行うプロジェクト用のディレクトリを用意し、
Serverspec と Infrataster をインストールします。
また、Infrataster で MySQL のテストを行うため、
infrataster-plugin-mysql も同時にインストールします。3
mkdir servertest
cd servertest
bundle init
echo 'gem "rake"' >> Gemfile
echo 'gem "serverspec"' >> Gemfile
echo 'gem "infrataster"' >> Gemfile
echo 'gem "infrataster-plugin-mysql"' >> Gemfile
bundle install --path vendor/bundle
設定
ホスト固有の設定値等を用いるために、
Serverspec のテストの実行を、
advanced_tips の “How to use host specific properties” に沿ったものにします。
Rakefile を以下のように書きます。
- Rakefile
$EDITOR Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
properties = YAML.load_file('properties.yml')
desc "Run serverspec to all hosts"
task :spec => 'serverspec:all'
namespace :serverspec do
task :all => properties.keys.map {|key| 'serverspec:' + key.split('.')[0] }
properties.keys.each do |key|
desc "Run serverspec to #{key}"
RSpec::Core::RakeTask.new(key.split('.')[0].to_sym) do |t|
ENV['TARGET_HOST'] = key
t.pattern = 'spec/{' + properties[key][:roles].join(',') + '}/*_spec.rb'
end
end
end
spec_helper.rb
次に spec というディレクトリを作成しておき、
spec 配下に spec_helper.rb を生成します。
mkdir spec
$EDITOR spec/spec_helper.rb
ここで、Serverspec の設定と Infrataster のサーバ定義を同時に行います。
require 'serverspec'
require 'pathname'
require 'net/ssh'
require 'yaml'
require 'infrataster/rspec'
require 'infrataster-plugin-mysql'
include Serverspec::Helper::Ssh
include Serverspec::Helper::DetectOS
include Serverspec::Helper::Properties
properties = YAML.load_file('properties.yml')
properties.keys.each do |host|
Infrataster::Server.define(
properties[host][:name],
host,
ssh: {host_name: host, user: properties[host][:user], keys: ['~/.ssh/id_rsa']},
from: properties[host][:from],
mysql: {user: properties[host][:mysql_user], password: properties[host][:mysql_password]}
)
end
RSpec.configure do |c|
c.host = ENV['TARGET_HOST']
set_property properties[c.host]
options = Net::SSH::Config.for(c.host)
user = options[:user] || Etc.getlogin
c.ssh = Net::SSH.start(c.host, user, options)
c.os = backend.check_os
end
- properties.yml
ホスト毎の定義や設定値を書く yaml ですが、
ここでは以下のような例にします。
$EDITOR properties.yml
rrreeeyyy.com:
:roles:
- base
:name: :proxy
:user: :rrreeeyyy
web-01.rrreeeyyy.com:
:roles:
- base
- web
:name: :web
:user: :rrreeeyyy
db-01.rrreeeyyy.com:
:roles:
- base
- db
:name: :db
:user: :rrreeeyyy
:from: :web
:mysql_user: 'username'
:mysql_password: 'password'
こうすることで、Serverspec では以下のホストに対し任意のテストを実行します。4
- rrreeeyyy.com
- web-01.rrreeeyyy.com
- db-01.rrreeeyyy.com
また、Infrataster 側では、以下のホストが定義されたことになります。
- :proxy
- :web
- :db
テストを書く
先ほど yaml ファイルで定義した role 毎にディレクトリを作成します。
その配下に置かれた *_spec.rb というファイルは、テスト実行時に全て実行されます。
spec 配下のディレクトリ構成を以下のようにします。
.
├── base
│ └── base_spec.rb
├── db
│ └── db_spec.rb
├── spec_helper.rb
└── web
└── web_spec.rb
それぞれの spec ファイルについて見ていきます。
- base_spec.rb
全てのホストの role に base がついているので、
この spec ファイルに書いてあるテストは、定義した全てのホストで実行されます。
そのため、22 番ポートが Listen しているかをテストしています。
require 'spec_helper'
describe port(22) do
it { should be_listening }
end
他にも、ntp や sysctl の設定などで、全ホストで共通するものを書いていくと良いと思います。
もちろん、ディレクトリ内にある *_spec.rb ファイルは全て実行されるため、
ntp_spec.rb, sshd_spec.rb, sysctl_spec.rb 等に分けても問題ありません。
むしろ、テストが肥大化してきたらファイルを分割するべきかと思います。
にも、ntp や sysctl の設定などで、全ホストで共通するものを書いていくと良いと思います。
もちろん、ディレクトリ内にある *_spec.rb ファイルは全て実行されるため、
ntp_spec.rb, sshd_spec.rb, sysctl_spec.rb 等に分けても問題ありません。
むしろ、テストが肥大化してきたらファイルを分割するべきかと思います。
- web_spec.rb
web_spec.rb は web ディレクトリ配下にあるため、
role に web がついている、web-01.rrreeeyyy.com サーバでのみ実行されます。
80 番ポートが Listen していることに加えて、
rrreeeyyy.com へアクセスし、レスポンスに ‘rrreeeyyy - Powered by’ が含まれていること、
レスポンスヘッダの content-type が text/html であることをテストしています。
require 'spec_helper'
describe port(80) do
it { should be_listening }
end
describe server(property[:name]) do
describe http('http://' + ENV['TARGET_HOST'].gsub('web-01.','')) do
it "responds content including 'rrreeeyyy - Powered by'" do
expect(response.body).to include('rrreeeyyy - Powered by')
end
it "responds as 'text/html'" do
expect(response.headers['content-type']).to match(%r{^text/html})
end
end
end
なお、Infrataster の http は Ruby HTTP クライアントライブラリである Faraday を使用しています。
後述の :from を用いると、特定のホストからアクセスした時にどのように表示されるか、などもテスト可能です。
また、Web アプリケーションのテストフレームワークである Capybara を使用することも可能なので、
複雑な Web アプリケーションのルーティング等もテスト可能だと思われます。
- db_spec.rb
db_spec.rb は db ディレクトリ配下にあるため、
role に db がついている、db-01.rrreeeyyy.com サーバでのみ実行されます。
require 'spec_helper'
describe port(3306) do
it { should be_listening }
end
describe server(:db) do
describe mysql_query('SHOW STATUS') do
it 'returns positive uptime' do
row = results.find {|r| r['Variable_name'] == 'Uptime' }
expect(row['Value'].to_i).to be > 0
end
end
end
Infrataster は :from が付いていると、定義されたサーバからの振る舞いをテストします。
今回の場合、db サーバには :from :web が付いている為、
web-01 サーバから db-01 サーバへ MySQL で接続できるかをテストします。
仕組みとしては、db-01 サーバの 3306 番ポートを、
web-01 サーバを経由してローカルへ SSH ポートフォワードします。5
その後、Ruby の mysql2 ライブラリを用いて、クエリを発行します。
なお、3306 番ポートが LISTEN しているかどうかテストする部分に関しては、
Serverspec の管轄内になるので、内部的には db-01 サーバに SSH して、
netstat の結果を取得してテストしています。
何がいいか、どんな風にテストを書いていくかの例
- 何がいいか
Serverspec と Infrataster はテスト対象のレイヤーが少々異なっています。
Infrataster は 次の記事のように、nginx のルーティングをテストしたり、
MySQL のクエリを発行し、その結果をテストするなど、
かなりアプリケーションに近いレイヤーでのテストを行います。
その一方で、サーバ内にインストール済のパッケージや、設定ファイルの詳細をテストするのはやや困難です。6
Serverspec では、サーバ内の設定や、導入済みのパッケージなど、
Infrataster よりやや低いレイヤーにフォーカスしてテストを行うのが得意なように見えます。
その一方で、MySQL のクエリを発行した結果をテストするのはやや困難です。
この 2 つを組み合わせて使用することにより、サーバのより広いレイヤーに対してテストを行うことが可能になります。
- どんな風にテストを書いていくかの例
あくまで一例ですが、頭の整理的にこんな使い方も出来ます。
- Web アプリをデプロイする対象のサーバ構築をテストしたい
- デプロイする Web アプリは ‘Hello World’ と画面に出力する
まずこれを書く
describe http('http://app') do
it "responds content including 'Hello World'" do
expect(response.body).to include('Hello World')
end
end
当然失敗するわけです。
- そうだ、Web アプリがレスポンスを返すためには 80 番ポートを Listen する必要があるなあ
describe port(80) do
it { should be_listening }
end
describe http('http://app') do
it "responds content including 'Hello World'" do
expect(response.body).to include('Hello World')
end
end
上にテストを書きます。
- そうだ、80 番ポートを Listen するためには httpd が入っている必要があるなあ
describe package('httpd') do
it { should be_installed }
end
describe port(80) do
it { should be_listening }
end
describe http('http://app') do
it "responds content including 'Hello World'" do
expect(response.body).to include('Hello World')
end
end
更に上にテストを書きます。
ここで初めて、ansible や chef の playbook や cookbook を書き始めます。7
そしてテストをすると、一番上のテストは通るわけです、じゃあ次は 80 番ポートの Listen ,
じゃあ次は index.html の設置 … 等とコードベースでテストをしながらサーバを構築していく。
… なんて方法も、ありじゃないでしょうか?
まとめ
- サーバ構築のテストツールである Serverspec と Infrataster を一緒に使うテスト手法について説明
- 広いレイヤーでテストが出来る
- Nginx のルーティングや、MySQL のクエリ実行結果 (Infrataster)
- 特定のホストから見た、他のテストの振る舞い (Infrataster)
- サーバにインストールされているプロダクトの設定ファイルの詳細 (Serverspec)
- サーバの iptables の設定値 (Serverspec)
- 広いレイヤーでテストが出来る
- Serverspec と Infrataster を使ったテスト駆動インフラ構築の一例
- 目的からトップダウンでテストを書いて、ボトムアップで構築していく方法
- あくまで一例なので、合う合わないは当然ある
Footnotes
-
本記事では 2.1.2 で検証しましたが、1.9 以降なら恐らく正常動作するでしょう。 ↩
-
gem install bundler で入ります。 ↩
-
Ruby の mysql ライブラリのインストール時に、mysql-devel のようなライブラリを必要とします。 ↩
-
もちろん、この時点ではまだテストを書いていないので、何も実行されません。 ↩
-
ポート番号は、Infrataster::Server.define の mysql に port オプションを与えれば変更可能です。 ↩
-
ssh.exec を用いれば可能に見えます。それは serverspec の command で mysql クエリを発行すれば infrataster のテストが出来るのと同じように。 ↩
-
あるいは、涙を流しながら手で yum install httpd を実行します。 ↩