新しいBuildKitでのイメージのリベースとリモートキャッシュサポートの改善

BuildKit Builder エンジン、Dockerfile 1.4 フロントエンド、 Docker Buildx CLI の新しいバージョンをリリースしました。これらのそれぞれには、多くの新機能が付属しています。 このブログ投稿では、そのうちの1つであるDockerfilesの新しいコピーモードを示し、Dockerfilesでそれを使い始める必要がある理由を説明します。

Dockerfile 1.4 リリースでは、ビルドコンテキストまたは別のステージからファイルをコピーするためのコマンド ADD とコマンドが、 COPY 新しいフラグ '–link' を受け入れるようになりました。このフラグを使用すると、キャッシュ セマンティクスが大幅に向上し、新しい基本イメージの上にビルドを再構築せずに 2 日目の高速リベースを実行できます。

このフラグを使用するには、Dockerfile の先頭に を含む # syntax=docker/dockerfile:1.4 行を追加する必要があります。 これにより、このフラグをサポートする適切なフロントエンドイメージが読み込まれます。 フラグの正しいキャッシュセマンティクスを取得するには、BuildKit v0.10 も使用する必要があります。

# syntax=docker/dockerfile:1.4
FROM ...
COPY --link foo bar
docker buildx create --use --name mybuilder
docker buildx build .

この新しいフラグの機能の詳細に入る前に、現時点でDockerfileコマンドがどのように機能するかを見ていきましょう。

Docker イメージは、コンテナー ファイル システムを構成するレジストリ内の tar ボールであるレイヤーで構成されます。 画像をプルすると、これらのtarボールが重なり合って抽出されます。 この抽出がどのように行われ、ファイルが実際にディスクに格納されるかの実装は、基になるスナップショット作成者の種類によって異なります。 オーバーレイスナップショットを使用する場合、ファイルシステムは複数のディレクトリを1つに結合する特別なマウントを作成できます。 他のスナップショット作成者の場合、このプロセスには通常、ファイルの(浅い)コピーの作成が含まれます。

Dockerfile RUNのすべての 、またはADD コマンドは、 COPY 以前に作成されたコンテンツの上に追加される新しいスナップショットも作成します。ビルドの準備ができて、ビルド結果としてイメージをエクスポートする場合は、すべてのスナップショットを比較し、各スナップショットに追加された新しいファイルを含む新しいtarballを作成する「異なる」コンポーネントを実行します。

ここで理解しておくべき重要な概念は、新しいレイヤーを作成するには、前のレイヤー(親レイヤーとも呼ばれます)が以前に作成され、ディスク上に存在する必要があるということです。 コマンドを使用して一部のファイルをディレクトリに移動するたびに COPY 、同じステージ上の以前のすべてのコマンドを前に完了する必要がありました。 これがないと、ファイルのコピー先ディレクトリがなくなります。

この制限は、コマンド ADD に COPY 追加された新しい --link フラグで変更されます。このフラグが存在する場合、コマンドは別のモードで動作し、 COPY 代わりにファイルが完全に新しいスナップショットにコピーされます。 次に、この新しいスナップショットはそれ自体で新しいレイヤーtarballに変換され、そのtarballは以前のtarballレイヤーのチェーンにリンクされます。 このリンクアクションは通常、ファイルにアクセスしたり移動したりすることなく、新しいアイテムがレイヤー配列に追加される単なるメタデータの変更です。 次の例に示すように、リモートレジストリに存在するレイヤーを使用してリモートで実行することもでき、プルまたはプッシュする必要はありません。

メルゲオップ1

要約すると:

  • COPY --link=false (前の方法とデフォルト):ファイルは前のコマンドの結果の上にコピーされます レイヤーは、ディスク上のスナップショットを比較することによって後で作成されます
  • COPY --link=true: ファイルが新しい場所にコピーされ、独立したレイヤーに変換されます。 レイヤー識別子が前のレイヤーの上に追加されます

宛先ディレクトリから依存関係を削除することで、コマンドを完了する COPY 前に前のコマンドが完了するのを待つ必要がなくなります。 また、同じ Dockerfile ステージ上の以前のコマンドが変更されたときに、現在のコマンドのビルドキャッシュを無効にする必要もありません。

これにより可能になるユースケースの例をいくつか見てみましょう。

例: 既存のイメージのリベース

BuildKit v0.9 の以前のリリースでは、遅延イメージのプルという別の新機能が導入されました。 この機能が意味するのは、BuildKitがリモートイメージ/キャッシュにアクセスする必要があるときはいつでも、実際にそれらからファイルを読み取る必要があるタスクがあるまで、レイヤーのプルを遅らせるということです。 たとえば、レイヤーが別のイメージで使用されている場合、このプルは不要であり、BuildKitは、不変のダイジェストによって前のレイヤーを参照する新しいイメージを作成できます。

FROM ubuntu
ENV MYCONFIG=foo
VOLUME /data

たとえば、この Dockerfile を次のようにビルドするとします。 docker buildx build -t myuser/myubuntu --push . キャッシュのないクリーンなシステムでは、新しいイメージがリポジトリで準備されるまでにビルド全体が数秒しかかからないことに気付くでしょう。 これは、ubuntuイメージのレイヤーがローカルマシンにプルされたり、ハブリポジトリにプッシュされたりしないためです。 代わりに、BuildKit は Ubuntu レイヤーダイジェストを含む新しいイメージ構成とマニフェストを作成し、それらのみをプッシュします。 レイヤーは、レジストリのクロスリポジトリマウント機能を使用して、Ubuntuリポジトリから直接リンクされます。 このパターンは、リモートキャッシュがまだ最新であり、実際にはレイヤーをプルダウンしないことをビルドで検証する必要があるリモートキャッシュソースでも使用できます。

この方法は、画像構成のみを変更するような ENV VOLUME メタデータコマンドに適しています。や RUN のような COPY新しいレイヤーを作成するコマンドを使用した場合でも、これらのコマンドを実行するにはローカルファイルが必要なため、ベースイメージを最初にプルする必要がありました。

COPY --link この要件を削除します。 を使用するように COPY --link更新された一般的なマルチステージビルドDockerfileを見てみましょう。

#syntax=docker/dockerfile:1.4
FROM golang AS build
....
RUN go build -o /myapp .

FROM alpine:3.14
COPY --from=build --link /out/myapp /bin
ENTRYPOINT ["/bin/myapp"]

BuildKit v0.10 でこのファイルをビルドすると、最初に気付くのは、Alpine イメージをプルせずにビルドが完了していることです。 これは、ディレクトリへのコピー myapp が /bin/ Alpineファイルに依存しなくなったためです。 このイメージを別の Docker Hub リポジトリにプッシュすると、アルパイン レイヤーが直接リンクされます。 他の方法(たとえば、ローカルOCItarballに --output type=oci)でイメージをエクスポートする場合にのみ、レイヤーが実際にプルされます。

これで、このイメージを初めてビルドしてプッシュしたときに、将来このイメージを更新する必要がある場合に何が起こるかを確認できます。 セキュリティ修正を含む新しいAlpine 3.14イメージがリリースされた場合、または3.15に更新する場合。

すべてを再構築しないように、以前のビルドのリモートキャッシュを保存できます。 BuildKitは多くのキャッシュバックエンドをサポートしていますが、この場合、最も簡単なのは、ビルドキャッシュ情報をイメージ構成に埋め込むだけの「インラインキャッシュ」を使用することです。

インラインキャッシュを有効にするには、次のいずれかを実行します。

docker buildx build --cache-to type=inline --push -t mysuser/myapp .

又は

docker buildx build --build-arg BUILDKIT_INLINE_CACHE=1 --push -t mysuser/myapp .

これで、後続のビルドを実行するときに、イメージ自体をキャッシュソースとして使用できます。 たとえば、代わりに Alpine 3.15 を使用するように以前の Dockerfile を更新し、以前のキャッシュを使用してビルドするとどうなるかを見てみましょう。

FROM golang AS build
....
FROM alpine:3.15
COPY --from=build --link /out/myapp /bin/
ENTRYPOINT ["/bin/myapp"]
docker buildx build --cache-from myuser/myapp -t myuser/myapp --push .

最初のビルドと同様に、実際には alpine:3.15 ローカル コンピューターにプルされず、代わりにレイヤー BLOB がレジストリ内に直接移動されていることがわかります。 もっと興味深いのは、画像も引っ張られなかったことです golang 。 これは、バイナリが変更されていないことを確認 myapp できるため、イメージの2番目のレイヤーも変更されておらず、新しい高山イメージの上にリベースできるためです。 これはすべて、ローカルレイヤーなしで完全にリモートで行われます。

操作は COPY /bin ベースイメージのディレクトリに依存し、ベースイメージが変更されたためキャッシュが無効になり、AlpineとGolangの両方のイメージがプルされ、バイナリが myapp 再コンパイルされるため、これがないと --link 以前は不可能だったことに注意してください。

例: リモート・キャッシュ・サポートの改善

別の例として、複数の COPY コマンドがある場合のキャッシュの処理方法を見てみましょう。

#syntax=docker/dockerfile:1.4
FROM golang AS build
....
RUN go build -o /myapp .

FROM ubuntu AS config
...
RUN generate -o /myapp.config

FROM alpine:3.14
COPY --from=config --link /myapp.config /etc/
COPY --from=build --link /myapp /bin/
ENTRYPOINT ["/bin/myapp"]

このファイルには、別のビルドステージから生成された構成ファイルを追加する2番目のコピーを追加しました。 依存関係に複数のステージを使用し、最終ステージでそれらをすべて一緒にコピーすることは非常に一般的なパターンです。 これは、ビルドに最適な並列化とキャッシュの再利用を実現する方法です。

以前と同じように、インラインキャッシュを使用してこのDockerfileをビルドしてプッシュするとします。

docker buildx build --cache-to type=inline -t myuser/myapp2 --push .

次に、以前のインラインキャッシュを使用して再構築を行う必要があり、構成ファイルの生成が変更された場合に何が起こるかを考えてみましょう。 構成生成のステージを再度実行する必要がありますが、最後のステージはどうなりますか?

使用 --linkせずに、ファイルの場合 myapp.config 変更されたのは、アルパインの画像がプルされて抽出されたことを意味します。myapp.config そのスナップショットをコピーし、その依存関係 COPY myapp が変更されたため、再コンパイルして再度コピーする必要があります。ここでのキャッシュの再利用の可能性はコマンドの順序に依存し、キャッシュはキャッシュに一致した最後の COPY コマンドまで使用でき、それ以降のすべてのコマンドは再度実行する必要があることに注意してください。 のキャッシュ myapp が無効になっていたとしても、 myapp.config そのファイルは以前にコピーされたが、その逆ではないためです。

を追加すること --linkで、キャッシュの再利用が大幅に向上しました。 すべてのコマンドが独立し、 COPY ベースイメージに依存するコマンドはありません。 新しい構成が生成されると、新しいレイヤーに直接変換されます。 次に、このレイヤーが前の画像内で置き換えられます。 ベースイメージの最下層とそれを含む myapp 最上層はそのまま残され、ローカルマシンにプルする必要はまったくありません。 新しいレイヤーのみが新しいイメージマニフェストと一緒にプッシュされます。

メルジェオップ2

新しいセマンティクスを自動的に使用するようにすべてのコマンドを変更するのではなく、 COPY なぜ新しいフラグが追加されたのか不思議に思うかもしれません。 その理由は、まれに完全な下位互換性がないためです。 たとえば、コピー コマンド COPY myapp /path/to/myappが . 指定した /path/to/myapp 宛先ディレクトリにコンポーネントの1つにシンボリックリンクが含まれている場合は、それに従い、代わりにファイルがシンボリックリンクターゲットにコピーされます。 を使用すると --link、すべてのコピーが独立しており、コピー先のパスに含まれるファイルを確認することはできません。 したがって、シンボリックリンクをたどる代わりに、最初に常に新しいディレクトリ/path/to を作成し、 COPY --link myapp /path/to/myapp その中にファイルをコピーします。

あなたが見るかもしれない別のケースはのようなコマンド COPY myapp /usr/binです。 宛先パスがスラッシュで終わっていないことに注意してください。 以前のセマンティクスがなければ --link 、 がディレクトリであるかどうか /usr/bin をチェックしていたでしょう。 そうである場合、ファイルは としてコピー /usr/bin/myappされます。 そうでない場合、新しいファイルは通常のファイル bin としてコピー /usrされます。これらの種類のチェックでは、ディスク上のファイルを抽出して、そのタイプを検証できるようにする必要があり、 では --link許可されません。 --linkしたがって、を使用する場合は、宛先パスにシンボリックリンクが含まれていないことを確認し、あいまいな宛先ディレクトリ検出を使用しないようにする必要があります。

上記のケースは非常にまれであり、単純なDockerfileの変更で簡単に修正できるはずです。 コマンドで COPY シンボリックリンクに依存しない場合は、常に使用 --linkを開始することをお勧めします。 リンクされたコピーのパフォーマンスは、常に通常のコピーよりも優れているか同等である必要があり、ビルドのキャッシュの再利用と最適化が大幅に向上します。

の内部構造に COPY --linkもっと興味がある場合は、BuildKit の LLB 定義の新しい MergeOp 機能を利用しています。 MergeOp の詳細と、概念的には MergeOp の逆であるコンパニオン DiffOp 機能については、 BuildKit のドキュメントを参照してください。