この記事は 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さんです。