CircleCi 2.0 で Dockerイメージをキャッシュしビルドを爆速で行う

Friday I/O です。
[株式会社ワムウ](http://wamw.jp/)では、毎週金曜日は 興味がある事柄に取り組み、その成果を何らかの形でアウトプットする日 としております。

# 前回記事のおさらい

前回の記事は [こちら](http://qiita.com/takecore@github/items/f46f4b8947afe6970c68)
CircleCI で Docker Compose を使用して環境のビルドからテストまで任せましょう、便利だね、という内容でした。
ですが __Dockerイメージのプル・ビルドに時間がかかる__ というのが問題点としてありました。

# 今回のゴール

CircleCIが提供しているcache機能を利用してDockerのイメージ群をキャッシュ化し、ジョブの実行時間を短くしよう

# アプリケーションの構成

前回の記事と同様です。

|    項目   | 名称    | バージョン |
|:---------:|---------|------------|
| Language  | PHP     | 7.1        |
| Framework | Laravel | 5.4        |
| DB        | MySQL   | 5.7        |

# 大まかな処理の流れ

- キャッシュを作成するジョブ
    - Docker イメージをプル・ビルドする
    - サーバに存在するDockerイメージをキャッシュ化（キャッシュが既に存在する場合スキップ）
    - キャッシュしたファイルをCircleCiに保存
- テストを実行するジョブ
    - キャッシュしたファイルをCircleCiから読み出す
    - Dockerイメージのロード
    - Docker Compose からテスト実行

# 結果

```yaml:.circleci/config.yml

---

version: 2
jobs:
  generate_cache:
    machine: true
    steps:
      - checkout
      - restore_cache:
          key: docker-{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.yml" }}-{{ checksum "Dockerfile" }}
          paths: ~/caches/images.tar
      - run:
          name: Check cache file, if not exists then pull images and generate cache.
          command: |
            if [ ! -f ~/caches/images.tar ]; then
              docker-compose pull sample-mysql
              docker-compose build
              mkdir -p ~/caches
              docker save $(docker images | awk 'NR>=2 && ! /^<none>/{print $1}') -o ~/caches/images.tar
            fi
      - save_cache:
          key: docker-{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.yml" }}-{{ checksum "Dockerfile" }}
          paths: ~/caches/images.tar

  test:
    machine: true
    steps:
      - checkout
      - restore_cache:
          key: docker-{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.yml" }}-{{ checksum "Dockerfile" }}
          paths: ~/caches/images.tar
      - run:
          name: Load Docker images
          command: docker load -i ~/caches/images.tar
      - run:
          name: Run tests
          command: |
            docker-compose run --rm sample-laravel-app /bin/bash -c '\
              cp .env.example .env && \
              composer install --no-interaction && \
              php artisan key:generate && \
              ./artisan migrate:refresh && \
              ./vendor/bin/phpunit -c phpunit.circle.xml'

workflows:
  version: 2
  build:
    jobs:
      - generate_cache
      - test:
          requires:
            - generate_cache
```

## 上記yaml についてのポイント

CircleCi の機能で `workflows` という機能と `restore_cache` `save_cache` という機能を使用しています。

- workflows [公式ドキュメント](https://circleci.com/docs/2.0/workflows/)  
ジョブをシーケンシャルに実行する仕組み  
上記のyaml の場合 `generate_cache`, `test` の順でジョブを実行する
- restore_cache, save_cache [公式ドキュメント](https://circleci.com/docs/2.0/caching/)  
キーバリューでファイルの保存を扱ってくれる仕組み  
キー名には任意のファイルのチェックサムを利用できる
一度作成したキーに対する値（ファイル）は残り続ける。（いつ削除されるのか不明なので詳しい方は教えてください。）

## 作っていて詰まったところ

`docker save` というコマンドでイメージ群をtarファイル化しています。
`docker save -o ~/caches/images.tar` のみの場合、Repository名が無いイメージとしてtarファイルが生成されるらしく、気がつくのに時間がかかりました。
そのファイルを `docker load` 後、 `docker-compose run` をしても対象のイメージが無いので、イメージのPull が始まりキャッシュしたのに何で？状態でした。
`docker save` 周りの疑問は[こちらの記事](http://www.memotansu.jp/2016/10/docker/594/) に助けられまた。

## 実際のジョブ実行時間について

以下に画像を添付します。

1. 一枚目がキャッシュ化対応前（前記事）のビルド
1. 二枚目が未キャッシュ時のビルド（本記事の内容）
1. 三枚目がキャッシュ済時のビルド（本記事の内容）

![スクリーンショット 2017-09-08 18.24.59.png](https://qiita-image-store.s3.amazonaws.com/0/16357/dfa0b7c7-2d54-4a7f-83ef-663f3e985fed.png)

---

![スクリーンショット 2017-09-08 18.21.50.png](https://qiita-image-store.s3.amazonaws.com/0/16357/85138a95-c67e-ea8b-f2d1-7ee3b6c9814d.png)

---

![スクリーンショット 2017-09-08 18.24.41.png](https://qiita-image-store.s3.amazonaws.com/0/16357/33fa7a32-9a64-e224-c004-0eb4d76c031b.png)

1 と 3 を比べて 1の方が（`キャッシュ化対応前`の方が）早かった。。。
扱うイメージが少ないのでキャッシュ化の恩恵が薄い、`workflows` でジョブを分けているのでその分時間がかかるのが原因かもしれません。
あまり込み入った環境でない限りキャッシュする必要は無さそう。
案件で扱った時はビルドが __約2分早くなった__ のが個人的観測値です。（もちろん制作する規模・環境によりますが）
こんなやり方もあるんだと参考までに。

サンプルのGithub のリポジトリは[こちら](https://github.com/takecore/sample-circleci-cache)