Serverspecでテスト対象のIPとロールを指定する

前回のknife-soloの話と同様にServerspecのテスト対象のIPも変わるのでやり方調べた。Serverspec自体はserverspecコマンドみたいなのがあるのではなく、rakeタスクで管理されてるので、rakeタスクに引数を渡して、自分でspec_helper.rbでssh_optionsにセットするというのが順当な考え方であるとは思うのだけど、vagrantとかで手元のPC上でテストしてるときは毎回IPアドレス指定するのもダルいので、変更したいところだけ環境変数埋めて渡すことにした。場合によってはテスト対象が複数あって並列にしたいとか要件あれば環境変数だと困りそうなので、やりたい目的によって解が違うようにも思うけど、まぁなんでもrubyのコードに落とせば動くので好きな様にやればよいと思う。

最初一旦固定値をymlで書いて<%= ENV[] %>で環境変数埋めればよいかなぁと思って書いてみたのだけど、展開されず。

ちなみにserverspecでテスト対象をymlに外出しする方法はこの辺参照。

調べてみたところrailsのdatabase.ymlとかで環境変数が展開されるのはrailsが内部でerbをかましているからだったようで、yml自体に環境変数を解釈する機能があるわけじゃなかった。そりゃそうか。

ymlでENV読み込む方法はこの辺参照。

自分でerbをかませればよさそうってことで、それならymlである必要なくてjsonでもよくね?と思って方針転換してjsonにした。chefのノード定義がjsonなので気分的な問題と、構成管理ツールでノードの一覧とかAPIで取れる類のものはだいたいjsonを吐きそうという予感がしてjsonにしとくとあとでうれしい気がする。

まず、準備としてこんなかんじのJSONのテンプレ作ってhosts.jsonという名前で保存する。

[
  {
    "name": "commitm-dev",
    "host_name": "127.0.0.1",
    "user": "vagrant",
    "port": 2222,
    "keys":  "xxxx/private_key",
    "roles":["base", "ap", "db"]
  },
  {
    "name": "commitm-ap01",
    "host_name": "54.92.xxx.xxx",
    "user": "ec2-user",
    "port": 22,
    "keys":  "xxxx/aws-login.pem",
    "roles":["base", "ap"]
  },
  {
    "name": "commitm-ap",
    "host_name": "<%= ENV['TARGET_IP'] %>",
    "user": "ec2-user",
    "port": 22,
    "keys":  "xxxx/aws-login.pem",
    "roles":["base", "ap"]
  }
]

次にRakefileをこんなかんじで、ERBをかましてからJSON.parseする。

require 'rake'
require 'rspec/core/rake_task'
require 'json'
require 'erb'
hosts = JSON.parse(ERB.new(File.read('hosts.json')).result)

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

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

最後にspec/spec_helper.rbでsshのオプションを読み込んでセットする。ちなみにspec/spec_helper.rbから見てファイルのパスが一段ずれるのだけどパスは../hosts.jsonじゃなくてhosts.jsonで読み込めた。

require 'serverspec'
require 'net/ssh'
require 'json'
require 'erb'
hosts = JSON.parse(ERB.new(File.read('hosts.json')).result)

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 = hosts.find{ |h| h['name'] == ENV['TARGET_HOST'] }
set :host,        host['host_name']
set :ssh_options, {
  :user => host['user'],
  :port => host['port'],
  :keys => host['keys']
}

set :request_pty, true

# Disable sudo
# set :disable_sudo, true


# Set environment variables
# set :env, :LANG => 'C', :LC_MESSAGES => 'C'

# Set PATH
# set :path, '/sbin:/usr/local/sbin:$PATH'

この状態で、こんなかんじでTARGET_IPにIPアドレスセットしつつ実行すれば、ちゃんとテストできた。

$ TARGET_IP=54.64.xxx.xxx rake serverspec:commitm-ap

これでIPが動的に変わっても環境変数で吸収できるようになった。