はじめに
こんにちは。DevOps グループの中島です。
弊社では CI ジョブ実行のために EC2 でホストした GitLab Runner (Docker Executor) を利用しており、Docker コンテナ上で CI ジョブを実行しています。 それらのジョブのうちコンテナイメージをビルドするジョブにおいて、時間がかかっている問題がありました。 今回はその問題に対して比較的新しい機能を用いた解決策の紹介と、処理の概要について説明したいと思います。
背景
ビルドに時間がかかっている理由としては、レイヤーキャッシュが利用されていないのが 1 つの要因としてあげられました。 コンテナビルド用の Docker は dood (Docker outside of docker) を採用しており、レイヤーキャッシュは EC2 上に保存されますが、Docker のキャッシュは EC2 の容量の問題から毎日削除しています。 もし削除していないにしても、SaaS のような実行環境ではどのサーバで CI ジョブが実行されるかは定まらず、ローカルのキャッシュは利用できないと考えるのは妥当といえます。
この問題の解決策として、AWS の記事 で紹介されていた ECR にキャッシュを保存する方法*1 を利用しビルド時間の削減を達成することができました。
実装
以下に実装のポイントを説明します。
gitlab-ci.yml
build: image: docker:25-rc script: ...
ECR へのキャッシュの保存に対応しているのが Docker のバージョン 25 からのため、CI ジョブを実行するコンテナには docker:25-rc
を指定します。(現状では正式リリースされていないため rc を利用しています)
script
# キャッシュの格納先 ECR REPO_URI=<account-id>.dkr.ecr.<my-region>.amazonaws.com/<repository_name> # 1. Buildkit コンテナの作成 docker buildx create --use --name <buildkit_container_name> # 2. キャッシュの格納先を指定してビルド docker buildx build -t ${REPO_URI}:<image_tag> \ -t ${REPO_URI}:latest \ --load \ --cache-to mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${REPO_URI}:cache \ --cache-from type=registry,ref=${REPO_URI}:cache . # 3. Buildkit コンテナの削除 docker buildx rm <buildkit_container_name>
script 内のポイントは以下のとおりです。
docker buildx create --use
で Builder の driver として、デフォルトの docker ではなく、docker_container を作成して利用する指定をします。(docker driver では インラインキャッシュ しか対応していません)docker buildx build
でキャッシュの格納先を指定してビルドします。
オプション | 説明 | |
---|---|---|
--load | ローカルに保存する指定 (通常の docker build と異なりデフォルトではどこにも出力しないため、これを指定しないと warn が出ます) |
|
--cache-to | mode=max | 最終レイヤだけでなく、全てのレイヤをキャッシュの対象とする |
image-manifest=true, oci-mediatypes=true |
ECR に格納できる形式の指定 | |
type=registry,ref=${REPO_URI}:cache | キャッシュ格納先(書き込み)の指定 | |
--cache-from | type=registry,ref=${REPO_URI}:cache | キャッシュ格納先(読み込み)の指定 |
以上の実装で、一度ビルドしたレイヤーが ECR に保存され、次回以降のビルドではそのキャッシュが利用されるようになります。
ビルドの全体像と処理の流れ
GitLab Runner 上では Docker コンテナで CI ジョブが動いていて、そのジョブが Buildkit 用の Docker コンテナを起動していたりします。 一応動作はしているものの、実際にはどのように各コンポーネントが連携して動いているのか全体像が把握できてないことから、どこの設定を変えれば何が変わるのかの確信が持てなかったため、少し詳しく調べてみました。 以下に図を用いてビルドの過程を示しています。
GitLab Runner Server 上では Docker が起動しています。gitlab-runner は CI ジョブ実行のため、Docker Engine API を用いて CI Job Container を起動します。
ホストの /etc/gitlab-runner/config.toml
で CI ジョブは /var/run/docker.sock
をマウントするように設定しているため (dood)、コンテナ内のジョブはこのソケットを経由してホストの dockerd にアクセスすることができます。
docker buildx create
で docker_container の Builder (Buildkit Container)を作成します。コンテナの起動は前述したとおりマウントしたソケットを経由し Docker Engine API を用いてホスト側で行われます。
Buildkit Container が作成されました。このコンテナは /var/lib/buildkit
配下にキャッシュなどを保存しますが、このパスはホスト側のファイルシステムにマウントされています。
docker buildx build
でビルドを実行します。ci-job 内で buildx がクライアントとして buildkitd と通信し、Buildkit Container 内でイメージのビルドが実際に行われます。ビルドの出力は CI Job Container に返却されます。
docker buildx build
で--load
を指定しているので、イメージを読み込む Docker Engine API を呼び出し dockerd にイメージを保存するよう依頼します。
その後、同様にdocker push
で保存したイメージをリモートにプッシュするよう依頼します。
終わりに
CI ジョブでイメージをビルドする処理の流れを追うことで、もやもやしていた感じがスッキリしました。自信を持って Buildkit を使えるようになった気がします。 このポストが誰かのお役に立てば幸いです。
We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp
*1:イメージにキャッシュを埋め込むインラインキャッシュを利用するという方法もありますが、マルチステージビルドでは利用に制限があるため採用しませんでした。