1 of 34

CircleCIでLayer Cachingを使わずにdocker buildを高速化する

【東京】CircleCI ユーザーコミュニティ ミートアップ #9

2022/04/21

加瀬健太 @Kesin11

2 of 34

自己紹介

所属:DeNAのSWETグループ(SoftWare Engineer in Test)

Twitter: @Kesin11, GitHub: Kesin11

CI/CDサービスのマニアなので有名なサービスは大体チョットワカル

主な業務

モバイルゲーム開発のJenkins構築、運用、パイプライン構築

社内CircleCI Serverの運用チーム

3 of 34

docker build 毎日しますよね?

4 of 34

ビルドは早いほうが嬉しい

CircleCIではどうしますか?

5 of 34

6 of 34

完

7 of 34

完全に正しいが・・・

実際どうしてますか?

DeNAではCircleCI Server(オンプレ)なので実質無料

docker buildする場合にはバンバン使ってもらっている

8 of 34

Layer Cachingのお値段

ドキュメントによると1ジョブごとに300クレジット

Freeプランの無料枠は30,000クレジットなので100ビルド相当

Docker/Linux Medium(2CPU, 4GB)が10クレジット/分

Layer Cachingを有効にするだけで30分相当のクレジット

キャッシュによってビルド時間自体を減らせる

高いと見るか安いと見るか・・・

9 of 34

Layer Cachingに頼らずにビルドを�早くする方法はないだろうか?🤔

10 of 34

発表内容

docker buildの各種キャッシュ方式の解説

CircleCI上でのビルド実験

実験の結果の考察

おまけCircleCIでdocker buildする際の重要ポイント

11 of 34

まずbuildkitの有効化

DOCKER_BUILDKIT: 1

マルチステージビルドが並列ビルドされるので早くなる

もう基本中の基本なので語ること無し

とはいえ最新のcimg/base:stableを使っていても明示的に環境変数でONにする必要があるので忘れないように

12 of 34

docker build --build-arg BUILDKIT_INLINE_CACHE=1

pushするイメージにキャッシュ用のメタデータを書き込む

次回ビルドでキャッシュとしてpush済みのイメージを参照する

マルチステージビルドにおいては普通に使ってもキャッシュが有効になる場面はほぼ無い

忘れてOK

13 of 34

補足:buildx

この後の各種キャッシュ機能はbuildkitの機能を使います

dockerからbuildkitのフル機能を使うにはdocker buildxが必要

とりあえずdocker buildx buildを使うのが最新のビルド方法という認識で大丈夫です

​

14 of 34

(正式な名称はよくわからない。便宜上Mount cacheと呼びます)

DockerfileのRUNに追加する

RUN --mount=type=cache,target=/root/.cache/go-build go build …

RUN中にディレクトリ指定でマウントしたボリュームを次のビルドで使い回す

パッケージやビルドキャッシュを使い回せると次のビルドが早くなる

15 of 34

docker buildx build -f Dockerfile \

--cache-to=type=registry,mode=max,ref=$CACHE_IMAGE:cache \

--cache-from=type=registry,ref=$CACHE_IMAGE:cache \

-t $IMAGE:latest .

ビルドするイメージとは別にキャッシュ用レイヤーを別イメージとしてpushする

mode=maxにするとマルチステージの中間も含めて全レイヤーがpushされる

マルチステージビルドでもキャッシュを有効に使える

16 of 34

docker buildx build -f Dockerfile \

--cache-to=type=registry,mode=max,ref=$CACHE_IMAGE:cache \

--cache-from=type=registry,ref=$CACHE_IMAGE:cache \

-t $IMAGE:latest .

キャッシュとしてのイメージはdocker run可能なものではないので混ざらないようにイメージ名を別にした方が安全

CircleCIはコンテナレジストリの機能を提供していないのでGitHub/AWS/GCPあたりのレジストリを自分で用意する

17 of 34

docker buildx build -f Dockerfile \

--cache-to=type=local,mode=max,dest=/tmp/docker_cache \

--cache-from=type=local,src=/tmp/docker_cache \

-t $IMAGE:latest .

キャッシュ用レイヤーをローカルに書き出す

キャッシュとしての効果はRegistryと同じ

mode=maxの効果も同じ

18 of 34

docker buildx build -f Dockerfile \

--cache-to=type=local,mode=max,dest=/tmp/docker_cache \

--cache-from=type=local,src=/tmp/docker_cache \

-t $IMAGE:latest .

CircleCIのキャッシュ機能と併用して次回のビルドで使い回せる

注意:書き出されるディレクトリの中身は上書きではなく太り続ける

キャッシュ戦略をミスると古いファイルも残り続けてしまい、無駄なネットワーク転送が増え続けて本末転倒になってしまう

19 of 34

番外:dockerでビルドしない方法

何を言っているのか??

20 of 34

番外:dockerでビルドしない方法

CircleCIのremote dockerはスペックが固定されている(2core, 8GB)

docker build時にコンパイルなどCPUをぶん回す場合は頭打ちになる

(別解としてハイスペックmachine executorを使う方法もある)

最大で20CPU, 40GBまでスペックを上げられるCircleCIなのにもったいない

​

​

21 of 34

番外:dockerでビルドしない方法

ハイスペックなコンテナ内でビルドしてCOPYだけでイメージを焼く

例:go buildでバイナリを生成、DockerfileはCOPYだけ

Dockerのキャッシュの代わりに普段どおりCircleCIのキャッシュを使う

Dockerが担保していた再現性のあるビルドは担保されなくなるので注意

とはいえCircleCIはコンテナでビルドしているので元々再現性は高い

​

​

22 of 34

実験

CircleCI-Public/circleci-cliをforkしてdocker build中でバイナリをビルドするようなDockerfileを作成

中身はほぼ go mod, go build

これをCircleCI上でビルドさせる

https://github.com/Kesin11/my-circleci-docker-build-sandbox

詳細が気になる人はDockerfileや.circleci/config.ymlを参照

23 of 34

実験

キャッシュの有無で差が生じやすいようにsmall(1core, 2GB)

以下を比較

キャッシュを使わない通常のdocker build(DOCKER_BUILDKIT=1)

Mount cache

Registry cache

Local cache

dockerでビルドしない

24 of 34

実験

キャッシュが有効なケースと無効なケースを意図的に作り出す

有効なケース

CircleCIで再ビルド。何も変更しないのでキャッシュがフルに効く

無効なケース

go.modに適当なコメント行を追加し、不正なキャッシュを生成

わざと不正なキャッシュを参照させることで意図的に無効化させる

25 of 34

結果

それぞれのケースで5回計測

横軸はジョブ全体の時間(秒)

remote dockerの立ち上げに0-90秒程度の振れ幅があったため、最小値、中央値、最大値を表示

評価は中央値で行う(ソートも)

プロジェクト構成によって結果はかなり変わるのであくまで参考程度

26 of 34

考察 Mount cache

同一マシンにしかキャッシュが保持されない仕組み

CircleCIは毎回マシンが変わるのでキャッシュの意味がないことは予想通り

デメリットもなさそうなのでDockerfileに書いておくとローカルマシンでのビルドが早くなるメリット

27 of 34

考察 Registry cache

キャッシュが有効な場合はLocalとほぼ同等の早さ

ネットワーク的にLocalの方が有利と予想していたので意外だった

Localはrestore_cache, save_cacheのキーとパス設計が複雑になりがちなのでRegistryの方が簡単

28 of 34

考察 Local cache

キャッシュ無効の場合に特に遅い

ログを見るとキャッシュの書き出しに時間がかかっていた

CircleCIはremote dockerなのでネットワーク越しのIOであり実際にはローカルではないので時間がかかる?

書き出されたサイズは約1.5GBでこれをCircleCIのキャッシュに送るのも時間がかかっていた

29 of 34

考察 dockerでビルドしない

キャッシュが無効なら早い、有効ならRegistryやLocalに負ける

レイヤーキャッシュがフルに効く場合はdockerはビルドをほぼスキップしているため

良く言えばキャッシュの有無に依らずに安定している

​

​

30 of 34

自分なりの結論

buildxを有効にしてRegistry cache + Mount cacheが無難でシンプル

ただしキャッシュ用イメージの分だけコンテナレジストリに費用が発生するのをお忘れなく

キャッシュ用イメージのタグ戦略

featureブランチでは--cache-fromをmain、--cache-toは無し

キャッシュ書き出しの時間を節約

mainブランチでは--cache-fromをmain、--cache-toもmain

CIでよく使うキャッシュ戦略と同一

​

31 of 34

おまけ:CircleCI上のbuildxのセットアップ

クライアントとリモートのdockerのバージョンがある程度新しい必要がある

クライアント

cimg系のコンテナであればよほど古くない限り多分大丈夫

リモート(デフォルトが17.09.0-ceなのはそろそろ更新をお願いしたい・・・)

setup_remote_docker:

version: 20.10.11

CircleCI Serverの場合

remote docker用のVMのdockerでバージョンが固定されているため、古い場合は管理者にAMIのアップデートをしてもらう必要がある

32 of 34

おまけ:CircleCI上のbuildxのセットアップ

実はRegistry cacheを使うには以下のコマンドでbuildxの設定が必要

docker buildx create --use

ローカルでは問題ないがCircleCIだと以下のようなエラーになった

error: could not create a builder instance with TLS data loaded from environment. Please use `docker context create <context-name>` to create a context for current environment and then create a builder instance with `docker buildx create <context-name>`

ワークアラウンドとして以下のようにセットアップすれば動く

docker context create circleci�docker buildx create --use circleci

参考�https://support.circleci.com/hc/en-us/articles/360058095471-How-To-Use-Docker-Buildx-in-Remote-Docker-�http://www.er.crichardson.com/post/microservices/2022/01/18/build-multi-arch-docker-images-circleci.html

33 of 34

おまけ:.dockerignoreが超重要

何気ない `COPY . .` がキャッシュを無効化する

docker build時にディレクトリの中身がcontextとしてビルダーに送られる

contextも完全に一致しないとレイヤーキャッシュが効かない

docker buildするのに本来は不要なファイルが含まれていないか?

.git/の中身はコミットされるごとに変わる

.circleci/config.ymlは当然CircleCIの設定を変更すると変わる

.gitは気をつけていたが、.circleciを忘れていて実験を何度かやり直すハメになりました・・・キャッシュの振る舞いが変だと思ったら.dockerignoreをチェックしよう

​

34 of 34

参考