こんにちは! SREの栗山です。 最近観て良かった映画は「コーダ あいのうた」です。
今回は弊社で使っているコンテナビルドを速くするためのテクニックを紹介します。
以下のような一般的なテクニックに関しては他でよく紹介されているので今回は割愛します。
- Dockerfileでは変更が少ないものを上に、変更が多いものを下に定義し、キャッシュが効くようにする
.dockerignore
をちゃんと定義する- マルチステージビルドを活用する
bundle installの結果をキャッシュする
弊社のサーバーサイドではRuby on Railsをメインで使っています。
そのためコンテナビルド時にbundle install
をする必要がありますが、bundle install
はとても時間がかかりますよね。
以下のようにしてしまうとCOPYしたファイルに変更があるたびにキャッシュが使われずbundle install
が実行されてしまいます。
COPY . . RUN bundle install
そのため以下のようにします。こうするとGemfile、Gemfile.lockに変更があった場合のみbundle install
が実行されるようになります。
COPY Gemfile Gemfile.lock ./ RUN bundle install COPY . .
assets:precompileの結果をキャッシュする
次に、assets:precompile
も遅いためキャッシュしたいですよね。
以下のようにしてしまうと、bundle install
の時と同じようにCOPYしたファイルに変更があるたびにキャッシュが使われずにassets:precompile
が実行されてしまいます。
COPY . . RUN RAILS_ENV=production SECRET_KEY_BASE=hoge bundle exec rake assets:precompile
これに関しては泥臭いやり方になってしまうのですが、assets:precompile
に必要なフィアルをCOPYしてからassets:precompile
を実行することでキャッシュを効かすことができます。
COPY Rakefile . COPY bin bin COPY config/application.rb config/application.rb COPY config/boot.rb config/boot.rb COPY config/environment.rb config/environment.rb COPY config/environments/production.rb config/environments/production.rb COPY config/settings/production.yml config/settings/production.yml COPY config/initializers/assets.rb config/initializers/assets.rb COPY config/initializers/config.rb config/initializers/config.rb COPY app/assets app/assets RUN RAILS_ENV=production SECRET_KEY_BASE=hoge bundle exec rake assets:precompile COPY . .
なおassets:precompile
に必要なファイルはプロジェクトごとに変わってくるので、各自必要なファイルをCOPYしてください。
ファイルをつらつらとCOPYしていてだいぶuglyですが、ビルドする度にassets:precompile
する時間が短縮されるメリットを考えると背に腹はかえられないかなと思います。
もっとスマートなやり方があれば教えてください 🙇♂️
Export Cacheを使う
弊社ではコンテナビルドにCircleCIを使っているので、CircleCIを例にして説明していきます。
CI/CD Saasでは毎回実行されるマシンが異なるため、ローカルの時とは異なりビルドキャッシュが効かないという問題があります。
CircleCIでビルドキャッシュを効かすためには、Dockerレイヤーキャッシュを有効化します。
具体的には以下のようにdocker_layer_caching
をtrueにします。
- setup_remote_docker: docker_layer_caching: true
これによりフルビルドが避けられますが、Dockerレイヤーキャッシュが存在しない場合はフルビルドが発生してしまいます。
https://circleci.com/docs/ja/2.0/docker-layer-caching/#how-dlc-works を読むと、
DLC ボリュームは、ジョブで 3 日間使用されないと削除されます。
とあります。 Dockerレイヤーキャッシュが存在しない場合でもフルビルドが発生しないように、弊社ではExport Cacheを使っています。
Export CacheとはBuildxの機能の1つで、ビルド結果を外部へ保存しビルド時にはその結果を使用(pull)することでビルドを高速化するというものです。 Github Actionsを使っている方はdocker/build-push-actionで意識せず使っているかもしれません。
ちなみにCircleCIではDockerレイヤーキャッシュと併用した場合、レイヤーキャッシュが存在すればそっちを使い、なければExport Cacheで指定した場所からimageを取得するという挙動になります。
Rubyのコードだけの修正の場合(assets:precompileが実行されない場合) の弊社のビルド時間の例です。
ケース | ビルド時間 |
---|---|
Dockerレイヤーキャッシュが存在する場合 | 20s |
Export Cacheで指定した場所からimageを取得する場合 | 50s |
フルビルドの場合 | 7m |
Dockerレイヤーキャッシュが存在しない場合でも十分な効果がでていることが分かると思います。
Export Cacheについて詳しくは以下を参考下さい。
- https://docs.docker.com/engine/reference/commandline/buildx_build/#cache-from
- https://github.com/moby/buildkit#export-cache
Skaffoldを使った例
弊社はコンテナのbuild及びpushにSkaffoldを使っているので、SkaffoldとExport Cacheを使った例を紹介します。
まずskaffold.yamlを以下のようにします。
BuildKitを有効化:
build: local: useBuildkit: true
BUILDKIT_INLINE_CACHE
を有効化し、cacheFrom
で使用するcacheイメージを指定:
profiles: - name: production build: artifacts: - image: $MY_REGISTRY/$MY_REPOSITORY docker: buildArgs: BUILDKIT_INLINE_CACHE: 1 cacheFrom: - $MY_REGISTRY/$MY_REPOSITORY:cache
次にCircleCIのconfig.ymlは以下のようにします。
export DOCKER_BUILDKIT=1 export BUILDKIT_PROGRESS=plain skaffold build --profile production # Buildkitのcache-fromで使うcache用imageをpushする skaffold build --profile production --tag cache
BUILDKIT_PROGRESS=plain
とすることで、CircleCIのコンソールのログ出力が何千行と大量にでるのを抑制できます。
Dockerコマンドを使った例
dockerコマンドを使う場合も紹介します。
export DOCKER_BUILDKIT=1 export BUILDKIT_PROGRESS=plain docker context create buildx-build docker buildx create --use buildx-build docker buildx build . \ -t $MY_REGISTRY/$MY_REPOSITORY:$MY_TAG \ -t $MY_REGISTRY/$MY_REPOSITORY:$MY_CACHE_TAG \ --cache-from=type=registry,ref=$MY_REGISTRY/$MY_REPOSITORY:$MY_CACHE_TAG \ --cache-to=type=inline,ref=$MY_REGISTRY/$MY_REPOSITORY:$MY_CACHE_TAG \ --push
--cache-to=type=registry
を使っていないのは ECRがcache manifestをサポートしてないためです。
type=registry
が使えるようになればmode=max
が指定できるようになり中間レイヤーもキャッシュされるためより効率的なビルドができるようになりそうです。
また、試してはいないですが、GitHub Container RegistryやDockerHubはcache manifestをサポートしているようなのでtype=registry
が使えるそうです。
(そのため、Export CacheにGitHub Container Registryを使い、ECRにもpushするというテクニックも存在します。)
まとめ
Export Cacheはネット上に情報が少なめですが、上手く使うとビルド時間を短縮できます。
もし他にも良いテクニックがあればフィードバックいただけると幸いです。