かまたま日記3

プログラミングメイン、たまに日常

CicleCIでDockerイメージを再利用する in 2019

CicleCIでDockerイメージを再利用する - かまたま日記3

こちらの記事の最新版です。 現在CircleCIはバージョン2で、Docker Layer Cachingという機能がありますが、残念ながら追加のフィーが必要です。というわけで、会社とかで使っててフィーを払える方はそちらを使うとして、個人のOSS活動などで払うのが厳しい方用に普通にCircleCI 2.0のファイルのキャッシュ機能を使った方法を解説します*1

前回に比べて以下の点が変わっています

  • GoのようにMulti stage buildを使って多段ビルドを行う前提 (最初のビルドのステージはas buider でbuilderという名前がついてます)
  • Docker image を完全に再利用はしないで中間イメージを再利用する (docker build は毎回やる)
  • docker save 後のtarファイルを更にgzに圧縮してリストアの時間短縮を図る

注意として、自分の環境では、数百メガのキャッシュをload cacheとsave cacheするので2~3分かかるのでキャッシュを使うことによって、それ以上短縮出来なければ、総時間は変わらないか悪くなる可能性もあります。

1. キャッシュ用のディレクトリとキャッシュキーを決める

ここではディレクトリは /home/circleci/docker-cache、キャッシュキーは毎回変えたいので {{ .Branch }}-{{ .Revision }} を使います

- restore_cache:
    keys:
      - v1-docker-{{ .Branch }}-{{ .Revision }}
      - v1-docker-{{ .Branch }}-
      - v1-docker-

- save_cache:
    paths:
      - "/home/circleci/docker-cache"
    key: v1-docker-{{ .Branch }}-{{ .Revision }}

2. イメージをビルドする

今回は builder のイメージを取っておきたいので、builderの部分は別にビルドしてbuilerタグを付けています。

IMAGE_NAME=my_app
CACHE_FROM="--cache-from ${IMAGE_NAME}:builder --cache-from ${IMAGE_NAME}"
docker build --pull ${CACHE_FROM} --target builder -t ${IMAGE_NAME}:builder .
docker build --pull ${CACHE_FROM} -t ${IMAGE_NAME} .

3. イメージを保存する

docker save で保存するイメージを docker history コマンドで取得します。 builder を取ってるのがミソです。

mkdir -p /home/circleci/docker-cache
image_ids=$(docker history -q my_app:builder | grep -v '<missing>')
docker save ${image_ids} | gzip > ${DOCKER_CACHE_FILE}

4. イメージを展開する

Gzip化しているのでgunzipコマンドで解答して docker load に渡します

if [ -f /home/circleci/docker-cache/image.tar.gz ]; then
  gunzip -c /home/circleci/docker-cache/image.tar.gz | docker load
fi

全体

キャッシュファイルのパスを変数に入れて、以下のような感じになります。関係ない部分*2は略してます。

version: 2
jobs:
  build:
    environment:
      DOCKER_CACHE_FILE: /home/circleci/docker-cache/image.tar.gz
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-docker-{{ .Branch }}-{{ .Revision }}
            - v1-docker-{{ .Branch }}-
            - v1-docker-
      - run: |
          name: Load Docker cache
          command: |
            if [ -f ${DOCKER_CACHE_FILE} ]; then
              gunzip -c ${DOCKER_CACHE_FILE} | docker load
            fi
      - run: |
          name: Docker build
          command: |
            IMAGE_NAME=my_app
            CACHE_FROM="--cache-from ${IMAGE_NAME}:builder --cache-from ${IMAGE_NAME}"
            docker build --pull ${CACHE_FROM} --target builder -t ${IMAGE_NAME}:builder .
            docker build --pull ${CACHE_FROM} -t ${IMAGE_NAME} .
      - run:
          name: Save Docker cache
          command: |
            mkdir -p $(dirname ${DOCKER_CACHE_FILE})
            image_ids=$(docker history -q my_app:${t} | grep -v '<missing>')
            docker save ${image_ids} | gzip > ${DOCKER_CACHE_FILE}
      - save_cache:
          paths:
            - ${DOCKER_CACHE_FILE}
          key: v1-docker-{{ .Branch }}-{{ .Revision }}

*1:基本となる考え方は別に他のCIでも使えると思います

*2:たとえばdockerのインストールとかsetup_remote_dockerやテストの実行、デプロイなど