Docker の Remote API + serverspec で CI

昨日 http://d.hatena.ne.jp/naoya/20130620/1371729625 で書いたように Docker を使えば、欲しい VM を "任意の状態" で簡単にかつ" "瞬時に" コピーして作り出すことができる。

  • 「任意の状態」というのは、例えば「OS は CentOS で、Ruby と Chef が入っている」みたいな VM のこと
  • 「瞬時に」というのは本当に瞬時。VM の起動時間を待ったり、Ruby や Chef を入れる時間を待つ必要はない

serverspec でテストをする場合、真っ新な VM を用意してそれにプロビジョニングを行って、その後に破棄するみたいなことを良くする。このとき「真っ新なVM」を立ち上げるのに、Vagrant などが使えるが、Vagrant だとテストの度に VM を一から作り直す・・・つまり vagrant up しなければいけない。ここで結構な待ち時間 & リソース消費がある。

Docker を使うと、Docker が LXC と AUFS を組み合わせていることにより、この待ち時間なしで任意の状態の VM を、ひな形のコピーで簡単に手に入れることができる、というわけであります。

Docker + serverspec でテストを一気通関で

というわけで sshd enabled な VM を Docker で作って serverspec でその VM の中身をテストする、ということをしたい。昨日のエントリではその sshd enabled な VM を作るまでができた。Dockerfile を作ってやって、ビルドするだけ。

ところで自分は Docker を Vagrant で立ち上げた VirtualBoxVM 内で使っている。serverspec は Vagrant VM にとってのホストOS である OSX 側で書いて実行したい。つまり

となって、OSX からテストの対象にしたい Docker VM との間に Vagrant VM が挟まることになって素の状態では直、OSX 側から直接 Docker VM を作ったり破棄したり、あるいは ssh を通したりといったことができない。

逆に、それができれば serverspec でテストの初期化で VM をポコっと作ってやって、テストを流して、終わったら VM を破棄するという一連の流れが全部自動化できる。

ssh を通す

OSX から直接 Docker VM への ssh を通すのは簡単で、Docker の提供する port forwarding の機能を使えば良い。これを使うと、Docker から見たホストOSのポートから Docker の任意のポートへの port forwarding ができる。その辺の設定は昨日のエントリの Dockefile の中に既に入っている。

Docker の Remote API を使う

Docker には "Remote API" という前述のようなユースケースに利用できる HTTP な API が用意されている。

この API は Docker を動かしているホストOS、つまり今回の例では Vagrant VM 上の OS で動いていて、デフォルトでは 4243 ポートにバインドしている。このポートに HTTP リクエストを飛ばしてやると API で Docker コンテナの操作ができる。

これを使えばなんとかなりそうだ。

Remote API を外から叩けるようにする

ただ、この Remote API はデフォルトではセキュリティの為かホストOS のループバックアドレスにしかバインドしていない。/etc/init/docker.conf を書き換えて

script
    /usr/bin/docker -d -H=tcp://0.0.0.0:4243/
end script

と、起動時のオプションでバインドアドレスを指定してやって sudo restart docker する。

次に OSXVagrant VM の通信をどうするか。実はホストOS の 4243 がゲストOS の 4243 に port forwarding するような設定が、Vagrantfile には入っているのだけどどうもそこが上手く動かなかったので、Vagrant 側で hostonly ネットワークを作って通信するようにした。

Vagrant::Config.run do |config|
   # Setup virtual machine box. This VM configuration code is always executed.
   config.vm.box = BOX_NAME
   config.vm.box_url = BOX_URI
   config.vm.forward_port 4243, 4243
   config.vm.network :hostonly, '192.168.50.5' # これ

これで OSX から http://192.168.50.5:4243/ にリクエストすることで、OSX 側から Docker の操作ができる。

docker-client

API は直接叩くのは面倒なので、github で公開されている ruby のラッパライブラリを使う。

これを使うと

require 'docker'

docker = Docker::API.new(base_url: 'http://192.168.50.5:4243')
containers = docker.containers
result = containers.create(nil, 'naoya/sshd')
container_id = result['Id']

containers.start(container_id)
p containers.show(container_id)

こんな感じで Docker を ruby スクリプトの中からプログラマブルに操作できるようになる。

serverspec でテスト

これで準備はできた。

serverspec が rake spec 時に実行する spec/spec_helper.rb を書き換えて、テストの実行前後で Docker で VM を作ったり削除したりするように変更する。

OSX から見た場合、ssh の転送ポートは毎回ランダムに変更になるので API からコンテナの情報を取得して、そのポート番号は動的に取得することにした。

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

require 'docker'
require 'awesome_print'

include Serverspec::Helper::Ssh
include Serverspec::Helper::DetectOS

RSpec.configure do |c|
  if ENV['ASK_SUDO_PASSWORD']
    require 'highline/import'
    c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
  else
    c.sudo_password = ENV['SUDO_PASSWORD']
  end

  ## Initialize docker API
  docker = Docker::API.new(base_url: 'http://192.168.50.5:4243')
  containers = docker.containers
  container_id = nil

  c.before :all do
    ## Start VM and retrieve port number of sshd
    res = containers.create(nil, 'naoya/sshd')
    container_id = res['Id']
    containers.start(container_id)
    sleep 1
    container = containers.show(container_id)
    port = container["NetworkSettings"]["PortMapping"]["22"].to_i

    block = self.class.metadata[:example_group_block]
    if RUBY_VERSION.start_with?('1.8')
      file = block.to_s.match(/.*@(.*):[0-9]+>/)[1]
    else
      file = block.source_location.first
    end
    host  = File.basename(Pathname.new(file).dirname)
    # if c.host != host
      c.ssh.close if c.ssh
      c.host  = host
      options = Net::SSH::Config.for(c.host)
      user = options[:user] || Etc.getlogin

      options[:port] = port

      c.ssh   = Net::SSH.start(c.host, user, options)
    # end
  end

  c.after(:all) do
    ## Stop and remove VM
    containers.stop(container_id)
    containers.remove(container_id)
  end
end

rake spec すると Docker API 経由で sshd enabled な VM が立ち上がって、serverspec がそこに ssh してテストを実行して結果を返してくれる。待ち時間はほとんどない、ワンダフル。

今のところ若干バッドノウハウがあって、docker.start した直後にテストを開始すると ssh port がまだ準備できていないことがあってテストが落ちるので sleep 1 を入れている。ちゃんとやるには、TCP polling が何かでチェックするなどしたほうが良さそう。あとは、ポート番号が毎回変わるので Net::SSHインスタンスを毎回作るようにちょっと変更してたりとか。

実運用させようとするともう少しリファインが必要だけど、今回の目的はとりあえず OSX 側から Docker を操作してテストと統合する、というところなのでひとまずよしとする。これを Jenkins で動かすなどして、CI しようと思えばできることは分かった。

Docker はまだまだ API その他仕様変更が激しいので、シンプルな CI の実戦投入には先日紹介した Vagrant を使う方法 http://d.hatena.ne.jp/naoya/20130520/1369054828 の方が安定していて良いとは思うが、Docker の特徴を活かして、例えば Travis CI のように perlruby が実行できる環境を用意して、その上で動かすアプリケーションは後から動的に注入するみたいな、そういうテストプラットフォームを作りたいときにこの Retmote API での連携が役に立つと思う。