Docker + Serverspecでインフラのテストを行う

knife-soloを使ってサーバー構築しているのだが、serverspecを使ってテストをしていなかった。
でも、前々からしようと思っていたので、今回重い腰を上げて実際にserverspecを使ってテストを行うようにした。

基本的な方針として、ローカル環境では knife-solo + docker を使用しているので、それにあわせて serverspecもknife-soloで作成したコンテナに対して、実行するようにしたい。

ちなみに環境は、

Serverspecをインストール

chefのプロジェクトのGemfile に serverspecを追加し、公式サイトに従いインストール。 まあ、この辺は色々と情報があるので、割愛。

また、特に必要と言うわけではないが、~/.ssh/configに dockerを使用するホストのIPアドレス とユーザー名を指定。IPアドレスは環境によって違うので、自分の環境に合わせる。

Host docker
  HostName        192.168.42.43
  User            moock

Serverspecをroleに分ける

ローカル環境、ステージング環境、本番環境とあるのでホスト名でディレクトリを分けるのではなく、 ロールで分けるようにした。

この辺は、公式サイトに記述されているように、 以下のようなイメージ。

 spec
  ├── base
  │   └── base_spec.rb
  ├── db
  │   └── mysql_spec.rb
  ├── solr
  │   └── solr_spec.rb
  └── web
      └── nginx_spec.rb

また、ホストとロールを関連づけるため、以下のようなYAMLファイル (properties.yml) を作成する。

docker:
  :roles:
    - base
    - db
    - solr
    - web

そして、作成したテスト実行時に読み込むため、Rakefileに以下を記述する。

require 'rake'
require 'rspec/core/rake_task'
require 'yaml'

properties = YAML.load_file('spec/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

こうすることにより、ホスト毎のテストのタスクが作成される。

$ bundle exec rake -T
rake serverspec:docker               # Run serverspec to docker
rake spec                            # Run serverspec to all hosts

Serverspecを特定のホストに対して実行する

rakeタスクでserverspecを実行すれば完成なのだが、dockerはコンテナを実行するたびに ポートが変わるので、rakeタスクを実行する際にポートを指定できるように、 spec_helper.rbに、ポートの部分を環境変数で指定するようにする。

require 'serverspec'
require 'pathname'
require 'net/ssh'

include SpecInfra::Helper::Ssh
include SpecInfra::Helper::DetectOS

RSpec.configure do |c|
    c.request_pty = true
    c.host  = ENV['TARGET_HOST']
    options = Net::SSH::Config.for(c.host)
    options[:port] = ENV['SSH_PORT'] if ENV['SSH_PORT']
    user    = options[:user] || Etc.getlogin
    c.ssh   = Net::SSH.start(c.host, user, options)
    c.os    = backend(Serverspec::Commands::Base).check_os
end

こうすることにより、

bundle exec rake serverspec:docker SSH_PORT=49153

sshのポートを指定し、serverspecを実行できるようになる。 これで、dockerとserverspecを実行し、インフラをテストできるようになった。

おまけ

以下のスクリプトを実行すれば、まっさらな状態からインストールが始まり、 テストまで行われ、その後コンテナが廃棄される。

# sshdを起動させ、コンテナを実行
container_id=`docker run -d -p 22 <docker image> /usr/sbin/sshd -D`

# dockerのexposeされているsshのポートを取得
ssh_port=`docker inspect --format='{{ if index .NetworkSettings.Ports "22/tcp" }}{{ (index (index .NetworkSettings.Ports "22/tcp") 0).HostPort }}{{  end }}' $container_id`

# サーバーのプロビジョン & テスト(dockerは .ssh/config で指定したホスト名)
bundle exec knife solo prepare docker -p $ssh_port &&
bundle exec knife solo cook docker -p $ssh_port &&
bundle exec rake serverspec:docker SSH_PORT=$ssh_port

# コンテナの削除
docker stop $container_id
docker rm `docker ps -a -q`

また、これをjenkinsなどに登録し、githubと連携させれば、 chefのコードがプッシュされたときに自動でサーバーの構築され、テストが走るはず。
ためしてないけど。

おしまい。

参考サイト

Serverspec Advanced Tips