CircleCI で Docker Container を Serverspec でテストする

Serverspec の Docker Backend を使った Docker コンテナのテストを CircleCI 上で実行する際、多少手こずったので、その試行錯誤によってできた、サンプルプロジェクトを公開しました。

前回の記事で紹介した事例は Rails を採用していたので、コンテナ側にも Ruby がインストールされており、コンテナ側にマウントするだけで Serverspec を実行できました。

docker run \
  -e DATABASE_URL="${DATABASE_URL}" \
  -e REDIS_URL="${REDIS_URL}" \
  -v "$(pwd)/docker/serverspec"\:/mnt/serverspec \
  --name "serverspec-${HASH}" \
  --link dev-mysql:mysql \
  --link dev-redis:redis \
  -w /mnt/serverspec -t $TARGET \
  sh -c 'echo "DATABASE_URL=${DATABASE_URL}" >> /var/www/app/.env &&
  echo "REDIS_URL=${REDIS_URL}" >> /var/www/app/.env &&
  service supervisor start &&
  bundle install --path=vendor/bundle &&
  sleep 10 &&
  bundle exec rake spec'

今回、新たに Ruby を使わないアプリケーションのコンテナを作る必要があり、コンテナ側には Ruby をインストールせず、ホスト側の Ruby から直接 Severspec を実行すべく、Specinfra の Docker Backend を採用しテストをしようと試みました。

参照: Specinfra::Backend::Docker

Serverspec は Docker の Exec コマンドを使用してコンテナ内の処理を行う仕組みになっており、CircleCI が Docker のドライバとして採用している LXC は、そ の Exec コマンドに対応していません。

Unsupported: Exec is not supported by the lxc driver

そのため、CircleCI のドキュメント に記載されている通り、lxc-attach コマンドを使って直接コンテナ側と IO のやり取りをする必要がありました。

sudo lxc-attach -n "$(docker inspect --format '{{.Id}}' $MY_CONTAINER_NAME)" -- bash -c $MY_COMMAND

現時点では Docker Backend ないしは、その依存ライブラリで lxc-attach に処理を行わせる機構がないため、 モンキーパッチを spec_helper.rb に実装しました。

require 'serverspec'
require 'docker'
require 'open3'
set :backend, :docker
set :os, family: 'ubuntu', arch: 'x86_64'
if ENV['DOCKER_IMAGE']
  set :docker_image, ENV['DOCKER_IMAGE']
elsif ENV['DOCKER_CONTAINER']
  set :docker_container, ENV['DOCKER_CONTAINER']
end
# TODO https://github.com/swipely/docker-api/issues/202
Excon.defaults[:ssl_verify_peer] = false
# https://circleci.com/docs/docker#docker-exec
if ENV['CIRCLECI']
  module Docker
    class Container
      def exec(command, opts = {}, &block)
        command[2] = command[2].inspect # ['/bin/sh', '-c', 'YOUR COMMAND']
        cmd = %Q{sudo lxc-attach -n #{self.id} -- #{command.join(' ')}}
        stdin, stdout, stderr, wait_thread = Open3.popen3 cmd
        [stdout.read, [stderr.read], wait_thread.value.exitstatus]
      end
      def remove(options={})
        # do not delete container
      end
      alias_method :delete, :remove
      alias_method :kill, :remove
    end
  end
end

以下は circle.yml の抜粋です。test.pre であらかじめ serverspec という名前のコンテナを立ち上げておき、環境変数でコンテナ名を指定して rspec を実行します。

test:
  pre:
    - docker run --name serverspec -p 8080:8080 -d $DOCKER_REPO && sleep 5
  override:
    - bundle exec rspec:
        pwd: docker/serverspec
        environment:
          DOCKER_CONTAINER: serverspec

その他試したこと

上記の方法でテストを成功させる前に、以下の試行を行いました。

Specinfra のバージョンを古いもので固定する

参照:

以下のエラーでコケました。

undefined method `gsub' for nil:NilClass

Specinfra LXC Backend を採用する

lxc-extra gem のビルドでコケたので、諦めました。

comments powered by Disqus