この記事は CircleCI Advent Calendar 2015 - Qiita の10日目の記事です。
前回はpokrkamiさんによる「circle.ymlの書き方」でした。
今日はCircleCIで変更があった箇所だけに限定してビルドするテクニックについて書きます。
時間のかかるビルド
今のプロジェクトではMicroservices志向でやっててDockerをフル活用しているのですが、それゆえに運用しているDockerイメージの数はそれなりの数があります。 アプリ側ではAPIコンテナやReactでSSRするコンテナ、バッチコンテナ、その他インターナルなMicroserviceなコンテナ等色々あります。
それとは別に、nginxやtd-agentといったミドルウェアのコンテナがあり、これらも用途別に複数用意されています。ミドルウェアのコンテナは1つのコンテナで全て管理しています。ディレクトリ構成のイメージは以下のような感じにします。
- イメージごとにディレクトリを切る
- イメージディレクトリの中にDockerfileを置く
- docker buildで必要なもの(COPYしたりするもの)もイメージディレクトリの中に、階層構造で配置しておく
CircleCIでこのリポジトリをCIします。CircleCIが自動で構成を検知してよしなにやってくれる構成ではないので、circle.ymlを少し書く必要があります。また、リポジトリ配下のものを拾ってビルドするのでスクリプトを書く必要がありそうです。 スクリプトを書けばまるっとDockerイメージをビルドできるようになるので便利ですが、この手法には問題点があります。
リポジトリ配下のイメージを全てビルドするので、それなりにCIの時間がかかる
これはさすがにちょっとたまらんので、少し手を加えました。
変更があったイメージだけに限定してビルド
APIやWebのDockerイメージに比べれば、ミドルウェア系のDockerイメージのビルドの頻度は圧倒的に少ないです。そこで、変更があったイメージに限定してビルドすればいいじゃないかと思いつきました。
サンプルプロジェクトがこちら。
とりあえず dockerci.sh
というスクリプトを書いたのでベタッと貼ります。
#!/usr/bin/env bash
COMMAND=$1
if [ $COMMAND != "build" -a $COMMAND != "push" ]; then
echo "$COMMAND is invalid command. (Required build|push)." 1>&2
exit 1
fi
REGISTORY="your-registry:5000"
CURRENT_BRANCH=`git rev-parse --abbrev-ref @`
# 変更があったdockerイメージを取得
if [ $CURRENT_BRANCH = "master" ]; then
# 現在がmasterであれば、直前のコミットと比較
TARGET="HEAD^ HEAD"
else
# masterブランチ以外であれば、origin/masterの最新と比較
TARGET="origin/master"
fi
git diff $TARGET --name-only | awk '{sub("docker/", "", $0); print $0}' | awk '{print substr($0, 0, index($0, "/") -1)}' > check.tmp
for dir in `ls`
do
if [ -d $dir ]; then
imagefile="$dir/image.txt"
if [ -e $imagefile ]; then
cat check.tmp | grep -e "^$dir$" > /dev/null
if [ $? -eq 0 ]; then
echo "modified $dir"
name="`cat $imagefile | head -1`:latest"
echo -e "\e[36m[BUILD]\e[mstart docker build: $name"
if [ $COMMAND = "build" ]; then
docker build -t $name $dir
if [ $? -ne 0 ]; then
echo -e "\e[31m[FAILED]\e[m docker build -t $name $dir"
exit 1
fi
docker tag -f $name $REGISTORY/$name
elif [ $COMMAND = "push" ]; then
# 実際にpushする際は次のコメントアウトを外す
#docker push $REGISTORY/$name
echo "docker push $REGISTORY/$name"
fi
else
echo -e "\e[35m[SKIP]\e[m $dir is not modified."
fi
else
echo -e "\e[33m[WARN]\e[m $imagefile is not found"
fi
fi
done
rm check.tmp
やっていることはこんな感じ。CircleCIっていうよりGitとシェルの話ですなw
master
ブランチのビルドであれば直前のコミットと比較し、変更のあったイメージディレクトリを検出master
以外のブランチでのビルドであれば、origin/master
の最新と比較し、変更のあったイメージディレクトリを検出- 変更のあったイメージだけ
docker build
、docker push
を行う
※docker push部分は実際にpushしてないのでコメントアウトして、コマンドをechoしてるだけです。pushしたければ宛先のレジストリ等を整備してコメントアウトしてみてください
あと、Dockerイメージの名前を定義した image.txt
というファイルを各イメージのディレクトリに配置しています。例えばnginx_aであれば、
stormcat/nginx_a
といった具合です。このイメージ名にタグは latest
でビルドします。 docker build -t stormcat/nginx_a:latest
ということですね。
で、circle.ymlは次のようになります。
machine:
timezone: Asia/Tokyo
services:
- docker
dependencies:
override:
- ./dockerci.sh build
test:
override:
- echo test
deployment:
push-docker:
branch: master
commands:
- ./dockerci.sh push
master
ブランチでビルドが通った場合は、docker push
を行うようにしてます。
適当に変更してビルドしてみる
nginx_aに適当な変更を入れてみて、masterにpushしてみましょう。
適当に変更 · stormcat24/middle-repo@818dc1d · GitHub
CircleCIでビルドされると、以下のようにnginx_aだけに反応して(サンプルなのでechoしてるだけ)他のイメージだけスキップしています。
ビルドの詳細はこちら を参照してみてください。
今のプロジェクトでは実際にこれを運用しています。現時点での課題としては、リポジトリには変更が無くベースイメージだけが変わった場合は検知できないってことでしょうか。
こんな感じで日々CI時間の削減に勤しんでいます。
明日11日目はheki1224さんです。