読者です 読者をやめる 読者になる 読者になる

Lambdar

主に技術やソフトウェアについて

CircleCI 2.0 Beta における Docker イメージのビルド

先日,CircleCI 2.0 がオープンベータになった

CircleCI 1.0 ではベースコンテナとして LXC,ファイルシステムとして Btrfs を採用しているため,バージョン 1.11 以降の Docker を利用することができなかった. CircleCI 2.0 ではそのような制限は存在しない.Docker をネイティブサポートしているからだ.

ユーザは任意の Docker イメージを自由に組み合わせて望みの CI コンテナ環境を作ることができる. プロジェクト毎の設定で「Ubuntu 12.04 (Precise)」か「Ubuntu 14.04 (Trusty) 」の二択から選んでいたことを考えると,レゴブロックを買い与えられた子供のように喜ぶべきだろう.

しかし,CircleCI 1.0 時代においても我々はアプリケーションを Docker イメージとしてビルドし,プッシュし,デプロイしていたはずである. CircleCI 2.0 では Docker コンテナの中で Docker を使って Docker イメージをビルドするだって?そんなのやったことないよ!


幸い,CircleCI 2.0 はこの Docker-in-Docker 問題 についても解答を与えてくれている. 本稿では,CircleCI 2.0 における Docker イメージのビルドについて,イメージレイヤーのキャッシュも含めてその方法を簡単に述べる.

TL;DR

プロジェクトルートに .circleci ディレクトリを作成し,そこに以下のような config.yml を置けば良い.

version: 2
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker:17.05.0-ce-git
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache \
              py-pip=9.0.0-r1
            pip install \
              docker-compose==1.12.0 \
              awscli==1.11.76
      - restore_cache:
          keys:
            - v1-{{ .Branch }}
          paths:
            - /caches/app.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/app.tar | true
      - run:
          name: Build application Docker image
          command: |
            docker build --cache-from=app -t app .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/app.tar app
      - save_cache:
          key: v1-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/app.tar
      - run:
          name: Run tests
          command: |
            docker-compose -f ./docker-compose.test.yml up
      - deploy:
          name: Push application Docker image
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              login="$(aws ecr get-login)"
              ${login}
              docker tag app "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
              docker push "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
            fi

細部はプロジェクトによって当然異なる.このファイルが有効なサンプルプロジェクトを GitHub 上に用意した.

github.com

このサンプルプロジェクトには Express を使った「Hello World!」だけを返す Node.js アプリケーションと,Jestsupertest を使ったささやかなテストが含まれている. また,Amazon EC2 Container Registry (以下 ECR と略す)に Docker イメージをプッシュするために,ECR リポジトリの作成を行う単純な Terraform スクリプトもある. ECR を選んだ理由は,.circleci/config.yml を解説用にほんの少しばかり複雑にするためである.

以降では,この .circleci/config.yml の内容について説明を加える.

.circleci/config.yml

全体の構造

version: 2
jobs:
  build:
    working_directory: ...
    docker: ...
    steps: ...

詳細は公式ドキュメントに譲るが,特筆すべき点を以下に挙げておく.

  • CircleCI 1.0 では固定されていた実行セクションを,ジョブという単位でユーザが自由に定義できるようになった(build という名前のジョブは CircleCI が始めに実行する唯一のジョブである)
  • 同様に,各ジョブのステップをユーザが自由に定義できるようになった(リポジトリのチェックアウトやキャッシュ関係処理を含む)

つまり,CI コンテナ環境だけでなくステップについても,我々はより大きな自由を手に入れたことになる.

docker executor

docker:
  - image: docker:17.05.0-ce-git

この部分では,冒頭で述べた CI 環境を定義している.ここで定義した環境上でステップが実行される.

我々が欲しいのは,Docker がインストールされた Docker イメージである. さらに,ソースコードをチェックアウトするために Git もインストールされていると良い. それを満たすイメージが docker:17.05.0-ce-git である.このイメージは公式のものであり,-git が接尾辞としてついているイメージには Git がインストールされている.

このように,最新バージョンの Docker クライアントを使うことができる.

checkout

- checkout

最初のステップである checkout は,名前が示す通りソースコードをチェックアウトする特別なステップである. ソースコードは working_directory で指定したディレクトリに展開される.

setup_remote_docker

- setup_remote_docker

この呪文を唱えると, Docker-in-Docker 問題が回避できる. 実際には,CI コンテナ(プライマリコンテナ)から孤立した環境を立ち上げて,そのリモートホストの Docker Engine を利用している

必要なライブラリのインストール

- run:
    name: Install dependencies
    command: |
      apk add --no-cache \
        py-pip=9.0.0-r1
      pip install \
        docker-compose==1.12.0 \
        awscli==1.11.76

Python および Pip をインストールしてから,Docker Compose と AWS CLI をインストールしている. Docker Compose はテストの実行に用いる.AWS CLI は,Docker イメージを ECR リポジトリにプッシュする際に用いる.

実際のプロジェクトでは,これらの依存ライブラリも含めたイメージをあらかじめ作成しておくと良い.

Docker イメージのビルドとキャッシュ

- restore_cache:
    keys:
      - v1-{{ .Branch }}
    paths:
      - /caches/app.tar
- run:
    name: Load Docker image layer cache
    command: |
      set +o pipefail
      docker load -i /caches/app.tar | true
- run:
    name: Build application Docker image
    command: |
      docker build --cache-from=app -t app .
- run:
    name: Save Docker image layer cache
    command: |
      mkdir -p /caches
      docker save -o /caches/app.tar app
- save_cache:
    key: v1-{{ .Branch }}-{{ epoch }}
    paths:
      - /caches/app.tar

ここが本稿の肝である.基本的には,以下のことを順番に行っている.

  1. v1-{{ ブランチ名 }} を接頭辞とするキーを持つキャッシュが存在する場合,/caches/app.tar にリストアする.app.tar は,(あれば)前回ビルドした Docker イメージファイルである.
  2. /caches/app.tar が存在する場合,それを Docker にロードする.これにより,前回ビルドした Docker イメージレイヤーが再利用できる.
  3. Docker イメージをビルドする.--cache-from=イメージ名 を指定すること.
  4. ビルドした Docker イメージを /caches/app.tar に保存する
  5. 次回のビルドで利用できるように,/caches/app.tar をキャッシュする.キャッシュキーは v1-{{ ブランチ名 }}-{{ Unix エポックタイム }} としている.

このような回りくどいことをしているのは,デフォルトではリモート Docker Engine におけるレイヤーキャッシングが行われないからである. 実は,このレイヤーキャッシングを行う機能が存在するが,正式リリース版では有料機能になる可能性がある.また,Beta 版においてもサポートに問い合わせて有効にしてもらう必要があるらしい.

イメージレイヤーをキャッシュすることで実際にどの程度の速度改善が得られるかは,サンプルプロジェクトの CircleCI ビルド履歴 を参照してほしい.例えば, build #12 はキャッシュあり,build #13 はキャッシュなし(without cache)でビルドしたものである. 残念ながら,この例では 22 秒程度の改善しか見られなかった.

テスト

- run:
    name: Run tests
    command: |
      docker-compose -f ./docker-compose.test.yml up

Docker Compose でテストを実行している. 今回のサンプルプロジェクトではアプリケーションコンテナのみでテストが完結するが,データベースコンテナなどが必要になった場合,Docker Compose を用いるとセットアップが容易である.

プッシュ

- deploy:
    name: Push application Docker image
    command: |
      if [ "${CIRCLE_BRANCH}" == "master" ]; then
        login="$(aws ecr get-login)"
        ${login}
        docker tag app "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
        docker push "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
      fi

ブランチが master であるときだけ,ビルドした Docker イメージを ECR リポジトリにプッシュする.ログイン情報を取得するために,先にインストールした AWS CLI を用いる. 今回は行っていないが,実際のプロジェクトではイメージをプッシュした後にデプロイを実行することが多い(例えば,Amazon EC2 Container Service を用いる場合,ecs-deploy などを使う).

おわりに

CircleCI 2.0 においてもリモート Docker Engine によって Docker イメージのビルドが問題なく行えることが確認できた. また,効果は限定的だが,Docker イメージレイヤーのキャッシングも行えることを見た.

問題にぶつかったら,公式ドキュメントはもちろん,コミュニティフォーラムが参考になる.