Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

コンテナビルドを速くするためのテクニック

こんにちは! 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について詳しくは以下を参考下さい。

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はネット上に情報が少なめですが、上手く使うとビルド時間を短縮できます。

もし他にも良いテクニックがあればフィードバックいただけると幸いです。