TDD of Infrastructure as Code on CircleCI 2.0

timakin
11 min readJun 14, 2017

--

概要

Docker + Ansible + ServerSpec + CircleCI 2.0でTDDなCIビルドを行う話です。

今僕が在籍している企業では、プロビジョニング周りはAWS OpsWorksで管理されており、Chefのカスタマイズレシピが適用されて、ポンっと環境が構築されます。

個人的にはChefとItamaeの運用はしたことがありますが、OpsWorksを除けばAnsibleの方がStar数も多く活発に使われていそうなのと、どうせならDockerとServerSpecを使ってポータブルなテスト環境を作ってみよう、それをCircleCI2.0で効率的に回そう、と思い立って、やってみました。

ディレクトリ構成

.
├── Dockerfile
├── README.md
├── ansible
│ ├── ansible.cfg
│ ├── ci.yml
│ ├── circleci_test.yml
│ ├── group_vars
│ │ └── package
│ ├── inventory_localhost
│ ├── roles
│ │ ├── mysql
│ │ └── nginx
│ ├── site.yml
│ └── spec
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── Rakefile
│ ├── properties.yml
│ └── spec
├── circle.yml
└── vendor
└── bundle
└── ruby

ルートディレクトリ直下のDockerfileはansibleの設定を反映する先のコンテナを用意するためのものです。

ansibleディレクトリ以下には、ansibleのplaybookと、serverspecのテスト内容があります。ちょっと名前がかぶっていますが、specディレクトリの中のspecの中身がテストコードで、テストコード以外はdockerコンテナ内部でのビルド用にGemfile等を置いています。

補足ですが、今回はCircleCIのビルド中に立てたDockerコンテナ内部でserverspecを回すので、inventory_localhostに書いてある、ansible-playbookの適用先のhost名はlocalhostになっています。

localhost ansible_connection=local
[circleci_test]
localhost

Dockerfile

FROM centos:centos6

MAINTAINER timakin

RUN rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
RUN yum update -y
RUN yum install -y \
build-essential \
curl \
ansible \
sudo \
passwd \
openssh-server \
openssh-clients \
git \
zlib1g-dev \
libssl-dev \
libreadline-dev \
libyaml-dev \
libxml2-dev \
libxslt-dev \
openssl-devel \
readline-devel \
zlib-devel \
gcc \
gcc-c++

# Install rbenv and ruby-build
RUN git clone https://github.com/sstephenson/rbenv.git /home/root/.rbenv
RUN git clone https://github.com/sstephenson/ruby-build.git /home/root/.rbenv/plugins/ruby-build
RUN /home/root/.rbenv/plugins/ruby-build/install.sh
ENV PATH /home/root/.rbenv/bin:$PATH
RUN echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh # or /etc/profile
RUN echo 'eval "$(rbenv init -)"' >> .bashrc

# Install multiple versions of ruby
ENV CONFIGURE_OPTS --disable-install-doc
RUN xargs -L 1 rbenv install 2.3.4

# Install Bundler for each version of ruby
RUN echo 'gem: --no-rdoc --no-ri' >> /.gemrc
RUN rbenv global 2.3.4
RUN rbenv exec gem install bundler
RUN rbenv exec gem i rbenv-rehash

CMD ["ansible", "--version"]

こんな感じで、rootユーザーにgemインストール用のrbenvをインストールしてあります。今回はcentosで試しました。

circle.yml

肝心のcircleciの設定です。

version: 2
jobs:
build:
docker:
- image: timakin/golang-and-ruby-image
working_directory: ~/ansible_build
environment:
DOCKER_CLIENT_VERSION: 17.03.0-ce
steps:
- checkout
- setup_remote_docker
- restore_cache:
name: Restore a docker client
key: docker-client-{{ .Branch }}-{{ .Environment.DOCKER_CLIENT_VERSION }}
paths:
- /docker_client_cache/docker-$DOCKER_CLIENT_VERSION.tgz
- run:
name: Install Docker client cache
command: |
if [[ ! -e /docker_client_cache/docker-$DOCKER_CLIENT_VERSION.tgz ]]; then
set -x
mkdir /docker_client_cache -m 755
curl -L -o /docker_client_cache/docker-$DOCKER_CLIENT_VERSION.tgz https://get.docker.com/builds/Linux/x86_64/docker-$DOCKER_CLIENT_VERSION.tgz
fi
tar -xz -C /tmp -f /docker_client_cache/docker-$DOCKER_CLIENT_VERSION.tgz
mv /tmp/docker/* /usr/bin
- save_cache:
name: Save a docker client cache
key: docker-client-{{ .Branch }}-{{ .Environment.DOCKER_CLIENT_VERSION }}
paths:
- /docker_client_cache/docker-$DOCKER_CLIENT_VERSION.tgz
- restore_cache:
name: Restore a docker image tarball cache
key: docker-image-{{ .Branch }}-{{ checksum "Dockerfile" }}
paths:
- /image_cache/ansible_test_image.tar
- run:
name: Build Docker image
command: |
if [[ -e /image_cache/ansible_test_image.tar ]]; then
cat /image_cache/ansible_test_image.tar | docker import - timakin/ansible_test_image
docker load --input /image_cache/ansible_test_image.tar
else
docker build -t timakin/ansible_test_image .
if [[ ! -e /image_cache ]]; then
mkdir /image_cache -m 755
fi
docker save -o /image_cache/ansible_test_image.tar timakin/ansible_test_image
fi
- save_cache:
name: Save a docker image tarball cache
key: docker-image-{{ .Branch }}-{{ checksum "Dockerfile" }}
paths:
- /image_cache/ansible_test_image.tar
- run:
name: Attach provisioning settings to the container for ansible-test
command: |
ANSIBLE_TEST_CONTAINER_ID=$(docker run -dit timakin/ansible_test_image /bin/bash)
docker cp ansible/ $ANSIBLE_TEST_CONTAINER_ID:/ansible
docker exec $ANSIBLE_TEST_CONTAINER_ID /bin/sh -c 'ansible-playbook /ansible/ci.yml -s -i /ansible/inventory_localhost -c local && cd /ansible/spec && /home/root/.rbenv/bin/rbenv exec bundle install && /home/root/.rbenv/bin/rbenv exec bundle exec rake spec'

長いので要点だけ書くと、

- docker clientを使えるように、circleci公式のgolangのimageをベースにしたimageでビルドを行なっていること
- dockerのビルドにキャッシュを利用していること
- docker clientのインストール
- playbook適用先のdocker imageのビルド結果
- ansibleディレクトリ以下をcopyしたコンテナの中で、playbookの適用からbundle install、serverspecでのテストまでを行なっている事

がポイントです。

setup_remote_dockerという手順をかませることで、docker clientを用意した別環境を、primaryなdockerビルドの中から呼び出すことができます。

また、CircleCI 2.0では例のごとくキャッシュを利用するわけですが、dockerを利用する場面では、そのimageを毎回pull, buildするのではなく、tarに圧縮して置いたものをrestoreすることで、処理をSkipすることができます。

ただ、setup_remote_dockerの後にそこで使うimageのlayerは、以下のようにオプションを利用して有料でキャッシュすることができるようです。

まとめ

インフラ周りのテスト自動化を行なっている方は少なくないと思いますが、 CircleCI2.0で上記のようにビルドをしている例がなかったので、今回記述しました。

他のcircleciのv1.0のサンプルを参照しても、volumeしたディレクトリの中のファイルはコンテナ内部で存在しなかったりして、まあそういうもんだよな、docker cpコマンドでドカンと入れるようにすればいいよな、と気づくまでに時間がかかるなど、かなりこれでも体力を消耗しました。

ただ、一度環境ができてしまえば、ローカルでコンテナを都度作ってテストするよりもはるかに効率的ですし、1分弱で終わるので、 ぜひ検証サイクルを高速に回したい方は上記の設定をご利用ください。

--

--