ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Capistrano3 プラグインの e2e test を CI する - Docker を添えて -

経緯

  • Capistrano3 割と使ってて、特に asonas/capistrano3-puppet · GitHub はギョームでバッチリ使ってる
  • でも、ギョームで使ってるのに自動テストがないので気持ちが悪い。Capoistrano(3)のプラグインってほとんどテスト書かれてないのでは...
  • 何も参考に出来ないので、どう書けば良いのか...
  • Docker使えばsshロギンしてチェックアウトして実際動作で確認できるのでは

Cap 3 からはタスクがタダの Rake Task になったので、Rake Taskのテストを書くのと同じようにテストできるかもしれないが、大量の stubbing が必要になる感じがしたので今回は e2e な感じのテストを雑に書いた。

以下が流れ。


SSH + Puppet ができるコンテナを作る Dockerfile を用意する

FROM rastasheep/ubuntu-sshd:14.04
MAINTAINER Uchio KONDO <udzura@udzura.jp>

ENV HOME /root

RUN apt-get -y install puppet
RUN apt-get -y install git

ADD dot.gitconfig /root/.gitconfig
ADD puppet_repo /var/repo/puppet
RUN git init /var/repo/puppet
RUN cd /var/repo/puppet && git add . && git commit -m 'sample commit'

RUN mkdir /root/.ssh
ADD id_dsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys

RUN mkdir /var/puppet

最終的にこんな感じになった。

ここの sshd は素直に動いたので使った。秘密/公開鍵は専用のものをリポジトリに入れておもむろにADDできるようにしておく。

あと、テスト用の簡単なpuppetプロジェクトをADDしたのちコンテナ内でgitリポジトリにした。これは、Capistranoのデプロイ対象を同じコンテナ内のリポジトリにすることで高速にしようとしている。あとテスト用のpuppetプロジェクトも一緒に管理できるので便利と思う。

RSpec のビフォーフィルターで当該コンテナを立ちあげる。

  config.before :all do
    Dir.chdir './spec/misc' do
      run 'bundle install --path vendor/bundle'
    end
    run 'docker build -t cap3puppet/base spec/misc'
    run 'docker run -d -P --hostname \'cap3puppet.dev\' --name test_sshd cap3puppet/base'
  end

  config.after :all do
    run 'docker stop test_sshd'
    run 'docker rm test_sshd'
  end

素直でよい。

run の定義は下記のような感じ。Open3.capture3という最高のメソッドをラップしている。便利のため、runやrun_and_captureに任意の環境変数を渡せるようにする。

require 'open3'
module CommandRunner
  def __env
    @__env ||= {
      'PATH' => '/bin"/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin',
    }
  end

  def run(cmd, options={})
    sout, *_ = run_and_capture cmd, options
    return sout
  end

  def run_and_capture(cmd, options={})
    ext_env = options[:ext_env] || {}
    return Open3.capture3 __env.merge(ext_env), cmd
  end
end

RSpec.configure do |config|
  config.include CommandRunner
end

後述するWerckerのbase boxがUbuntu saucyっぽくて、素直にやるとRubyが1.9系に... options={} が無念っぽい。

RSpecの中からcapして出力を検査する

sshdが動作するdocker containerが自分のものになったので、そこに対して cap deploy(今回は cap production provision というコマンドでエイリアスしている)を実行してテストにする。

spec/misc ディレクトリの中に完全に動く Capistrano3 project を配置して、その中で実行するのがポイント。そこでの設定例は https://github.com/asonas/capistrano3-puppet/compare/8025d43...070e781#diff-939ca037997ba3d51fcaf6e9b2b07f8bR1 とか https://github.com/asonas/capistrano3-puppet/compare/8025d43...070e781#diff-7b8dd0f8c713ba0d737de06490b4bf89R1 のノリで。

RSpecのexampleは以下のように。

describe 'cap basic tasks' do
  describe '$ cap -T' do
    it 'should contain provison task' do
      Dir.chdir './spec/misc' do
        stdout, stderr, exitinfo = run_and_capture('bundle exec cap -T')
        expect(stdout).to match(/^cap provision/)
        expect(stderr).to be_empty
        expect(exitinfo.exitstatus).to be 0
      end
    end
  end

  describe '$ cap production provision' do
    let(:hostname) { have_boot2docker? ? run('boot2docker ip 2>/dev/null').chomp : 'localhost' }
    let(:port)     { run('docker port test_sshd 22').split(':')[1].chomp rescue "22" }
    let(:ext_env) do
      {
        'HOSTNAME' => hostname,
        'PORT'     => port
      }
    end

    it 'should run a provisioning successfully' do
      Dir.chdir './spec/misc' do
        stdout, stderr, exitinfo = run_and_capture("bundle exec cap production provision", ext_env: ext_env)
        expect(stdout).to include('Running /usr/bin/env puppet apply --modulepath=modules manifests/site.pp')
        expect(stdout).to include('Notice: Scope(Class[Sample]): hello, world!')
        expect(exitinfo.exitstatus).to be 0
      end
    end
  end
end

今回は、capコマンドが吐き出すログが意図したようになっているか?と、コマンド自体の正常終了、という検査だけにとどめているが、capを実行した結果ファイルシステムがどうなっているか、等のテストを書いてもいいと思う。Capistrano3に同梱されている capistrano/sshkit · GitHub とか使えそう。Serverspecと組み合わせるとかもできると思うけどやり過ぎかな...。

boot2dockerを利用する際のハック

コードを見れば分かる通り随所にboot2dockerを用いた場合での分岐が見られる。普通に同じホストでdockerを動かすのと違って、以下の2点に気を使う必要がある。

  • DOCKER_HOSTをちゃんと設定しないとdockerコマンドが動かない
  • CapのSSHのログイン先がlocalhostでなくboot2docker ip 2>/dev/nullの結果になる

詳しくは当該diffで「have_boot2docker?」をC-fすれば分かると思う。

ここまでで、あとはRakefileを適当に書けば、手元で rake spec が動くところまでできている。

Wercker に仕掛けよう

あとはWerckerに仕掛けるだけである。

Werckerは、Dockerを用いたCIに対応している。

wercker-labs/docker というbase boxがあるんでそれにルビーを入れればrspecが走る。でも、パッケージのbundlerを使ったら1.9.3になってしまう...。update-alternativesとか使えばいいんかな?

box: wercker-labs/docker
build:
    steps:
        - install-packages:
            packages: build-essential ruby2.0 ruby2.0-dev bundler
        - script:
            name: docker version
            code: |
                docker -v
        - script:
            name: bundle install
            code: |
                bundle install --path vendor/bundle
        - script:
            name: spec runner
            code: |
                bundle exec rake spec

あとは

WerckerのWeb UIからプロジェクトを登録し、プルリクエストを作成すれば便利最高が発生する。

所感

今回Capistrano3だったけど、これ、サーバにアレしてボンする系のミドルウェアなら、結合テストの自動化一般に使えるんじゃないかなーと思っている。Apacheの拡張とか、Apacheが走るコンテナ内部でコンパイルしてリクエストを当てて確認するとかできて良さそう。ここで ryotarai/infrataster · GitHub がめっちゃ活躍しそうである。

あと、Werckerは普通にDockerが使えて良い。他のCIサービスでも対応が進んでほしい。