CircleCIで変更があった箇所だけに限定してビルドするテクニック

この記事は CircleCI Advent Calendar 2015 - Qiita の10日目の記事です。

前回はpokrkamiさんによる「circle.ymlの書き方」でした。

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 builddocker 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さんです。