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レイヤーのチェーンにリンクされます。 このリンクアクションは通常、ファイルにアクセスしたり移動したりすることなく、新しいアイテムがレイヤー配列に追加される単なるメタデータの変更です。 次の例に示すように、リモートレジストリに存在するレイヤーを使用してリモートで実行することもでき、プルまたはプッシュする必要はありません。
要約すると:
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
最上層はそのまま残され、ローカルマシンにプルする必要はまったくありません。 新しいレイヤーのみが新しいイメージマニフェストと一緒にプッシュされます。
との違い --link=false
新しいセマンティクスを自動的に使用するようにすべてのコマンドを変更するのではなく、 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 のドキュメントを参照してください。