ドッカーコン

Dockerfile: 最初から最適化まで

David Karlsson、テクニカルライター、Docker

2023年11月24日収録
このプレゼンテーションでは、Dockerfile を記述して最適化する方法を示します。 Dockerfile の手順の機能を確認し、ビルドを高速化するために Dockerfile を最適化するためのパターンを確認します。

写し

こんにちは、Dockerfileへようこそ:開始から最適化まで。 デビッド・カールソンです。 私はDockerのドキュメントチームに所属しています。 私はテクニカルライターです。 今日は、Dockerfileと、Dockerでビルドし、最適化されたDockerfileをゼロから作成する方法について説明します。

私がDockerに参加したのは1年ちょっと前です。 また、それ以前はDockerを使用していたので、 Docker Composeの使用やイメージの構築に慣れていました。 そして、私はそれが何を意味するのか、かなりよく理解できたと思いました。 しかし、この講演は、Moby と BuildKit のメンテナーと緊密に協力し、当時は知らなかったすべてのヒントとコツを学んだ過去 1 年間で学んだことの要約です。

目次

    議題

    今日の講演の議題は、ビルドとは何か、ビルドに何が含まれるか、ビルドで使用されるコンポーネントは何かの紹介です。 そして、現時点ではDockerfileは含まれていませんが、新しいDockerfileを最初から作成する小さなデモプロジェクトがあります。 次に、その Dockerfile を使用して、ビルドの実行を高速化したり、イメージ サイズを小さくしたりするために最適化する方法を確認します。 最後に、より高度な機能を見ていきますが、必ずしも高度な機能とは限りません。 ただし、ビルドの柔軟性を拡張したり、ビルドに多様性を追加したり、テストなど、Docker ビルドでできることを知らなかったりするために使用できる機能があります。 それでは、本題に入りましょう。

    Dockerfile とは

    すべては Dockerfile から始まります。 Dockerfile は、ビルドのステップを実行するビルド命令です。 そして、それはDSLテキストファイルです。 構文はSQLのようなものです。 これには、出力を作成するためにビルダーによって実行される一連の命令が含まれています。 基本的な。 少しズームアウトすると、Dockerfileが実際にこれらすべてのフロントエンドになっていることがわかりますが、これは私にとっては愉快なことです。 これは、ビルドチームの同僚がバックエンドにどの程度いるかを示しています。 しかし、いずれにせよ、どうやらそれはフロントエンドのようです。 次に、 Docker EngineDocker Desktop に組み込まれたコマンドライン クライアントである Buildx があります。 これらは、ビルドを呼び出すために使用されます。

    Docker build を実行するときは、Buildx を使用しています。 そして、実際にビルドを実行しているエンジンである BuildKit があります。 今日は主にフロントエンドについて説明しますが、ビルドを呼び出すには Buildx を使用します。 そして、ビルドコンテキストがあります。 これは、本題に入る前に私が言及したかったことです。

    ビルド コンテキストは Docker コンテキストとは異なります。 ご存じの方もいらっしゃると思いますが、ビルド コンテキストは、ビルドがアクセスできるファイルのセットで、渡します。 コマンド ラインで docker build コマンドを実行する場合は、それが渡す位置引数です。 多くの場合、これは現在の作業ディレクトリを示すドットになります。 そのため、現在の作業ディレクトリを、その場合にビルダーがアクセスできるファイルのセットとして渡します。 しかし、それは他のものでもかまいません。 これは Git URL にすることができ、その場合、BuildKit ビルダーはリポジトリを直接複製します。 また、ローカルマシンからは何も送信していません。 タールボールにすることができます。 それを使う人もいます。 おそらくそうはならないでしょう。

    ドッカーファイルの作成

    さて、イントロはこれで十分です。 Dockerfile の作成に取り掛かりましょう。 まず、私が持っているサンプルプロジェクトを簡単に見てみましょう。 これはGoプロジェクトであり、リクエストを呼び出すことができるAPIを備えた小さなHTTPサーバーです。 また、IDが与えられた場合、ユーザーの名前と誕生日が返されます。 だから、非常に便利なアプリケーションです。 さて、このためのDockerファイルを作成しましょう。 Dockerfileを作成すると、ファイル名はDockerfileになりますよね? 私がいつもDockerファイルで最初にやりたいことは、この行を一番上に追加することです。 これは parse ディレクティブと呼ばれます。 この場合、このファイルで最新バージョンの Docker ファイル構文を使用することを指定しています。 これを追加すると、ビルドの実行時に動的に解決される Dockerfile 構文の最新機能にアクセスするために、Docker Engine や Docker Desktop をアップグレードする必要がなくなります。 基本的にこの行をDockerfileに常に含めるのは素晴らしいことです。 ただし、ビルドには何もしません。 そして、それが次のステップです。

    基本イメージ

    次に、FROM 命令を追加します。 これにより、このビルドに使用する基本イメージを指定します。 私のプロジェクトはGoプロジェクトです。 そこで今回は、Go バイナリーのビルドに必要なすべてのコンパイラー・ツールを含む Golang Docker 公式イメージを使用します。 さて、この時点から、Dockerfile に追加するすべての命令は、このイメージに基づくコンテナーで実行されます。 内部にバンドルされているコマンドにアクセスできます。

    作業ディレクトリ

    次に、コンテナーに作業ディレクトリを設定します。 次のコマンドは /src 内で実行されます。 次に、コピー命令を使用します。ビルドコンテキストからすべてのファイルをコピーします。 ここでも、コピーするファイルは、コンテナー内の現在の作業ディレクトリにビルド用のファイルのセットを指定します。 これで、正しい作業ディレクトリにいます。 すべてのファイルが入っています。 ビルドコマンドを実行できます。 最初にgo mod downloadで依存関係をダウンロードし、ビルドしてバイナリを作成します。 そして最後に、コンテナのエントリ ポイントを指定しますが、これは、このイメージに基づいてコンテナを実行したときに実行されるバイナリです。 さて、それでおしまい。

    これで、このイメージを構築できます。 ここでは、DC demo タグを使用します。 ここに表示される出力は、BuildKit からの出力です。 これにより、Dockerfile にある実行中のすべての命令が表示されます。 それが終わったら、イメージの名前を指定して docker run できます。 この場合、いくつかのポートをホストに公開します。 つまり、ローカルホスト上のこれらのポートに対して実行できるはずです。 さて、この時点で、基本的なイメージが機能しています。 Goアプリケーションをコンテナとして構築することに成功しました。

    レイヤーの表示

    次に、最適化に取り掛かります。 最初に見ていくのはレイヤーです。 レイヤーについて知っている、または知っている人はどれくらいいますか? よし、よし。 そうする人もいます。 したがって、レイヤーは画像を構成するものです。 また、ビルドキャッシュを構成するものも重要です。 大まかに言うと、Dockerファイルの各命令はレイヤーに対応していると考えることができます。 そのレイヤーが構築されると、画像のレイヤーが作成されます。 なぜこれが私たちにとって重要なのかというと、何かを変えるときです。 何も変更されていない場合、まず、そのレイヤーはビルドキャッシュによって再利用されます。

    今のところ、私たちのイメージはおそらく6つのレイヤーで構成されています。 何も変更しなければ、この6つのレイヤーはどれも再構築されません。 しかし、レイヤーを変更すると、Dockerがそれを検出するため、そのレイヤーとそれに続くすべてのレイヤーを再構築する必要があります。 したがって、ケーキを例にとると、黄色のレイヤーで何かを変更した場合、レイヤーと上のレイヤーをすくい上げてから、それらのレイヤーを再ベイクする必要があります。 つまり、命令の順序が重要です。 なぜなら、何かを変更した場合、不必要に再構築したくないからです。

    現在の Dockerfile または現在のイメージのビルド出力を見てみましょう。 変更せずにビルドを実行すると、すべてのステップがキャッシュされます。 何も実行されません。 ソース コードに変更を加えてリビルドすると、現時点での Dockerfile の記述方法では、copy 命令のキャッシュが無効になり、その後に続くすべてのレイヤーが無効になります。 現時点では、そのステップの後に依存関係をダウンロードするgomodのダウンロードがあるため、これは非常に非効率的です。 そのため、コードを変更するたびに、依存関係も最初からダウンロードします。 この場合、これは小さなプロジェクトであり、あまり問題になりませんが、依存関係をダウンロードする大きなプロジェクトにはかなりの時間がかかることが想像できます。

    これを軽減するために、命令順序で遊ぶことができます。 まず第一に、ソースコードを変更すると無効になるのは、このコピーレイヤーです。 これを依存関係をダウンロードした後に下に移動すると、この時点では依存関係レイヤーが無効になりません。 しかし、ダウンロードする依存関係がわからないため、依存関係をダウンロードできないため、Dockerfileが壊れています。 それを知るには、パッケージ管理ファイルが必要です。 そこで、その前に別のコピー命令を追加して、パッケージ管理ファイル (この場合は go.mod と go.sum) のみをコピーします。 つまり、ソースコードを変更した後に再構築すると、go.modファイルとgo.sumファイルを変更しない限り、gomodダウンロードレイヤーは毎回キャッシュされたままになります。 さて、レイヤーは以上です。

    マルチステージビルド

    これについては、後で最適化しますので、ご興味があればお知らせします。 さて、多段階、多段階について話しましょう。 私にとって、私の意見では、これはこれまででDockerビルドの最も重要な機能です。 それはおそらく主観的なものです。 しかし、それがあなたにできることは、ビルドとランタイムの間のきれいできれいな分離を作ることです。 なぜなら、今のところそれがないからです。 使用しているベースイメージには、すべてのGoコンパイルツールとすべてのソースコードも含まれており、本番環境でイメージを実行するためには必要ありません。

    これこそが、マルチステージビルドが修正に役立つことです。 基本的には、別のベースイメージを選択できます。 その後、前のステージから新しいステージにリソースをコピーして、不要なものをすべてイメージから効果的に削除できます。 これが何を意味するのかをさらに明確にするために、現在の画像はこの時点で約 600 メガバイトです。 大きい。 そして、バイナリを静的にビルドすると、バイナリは 8 メガバイトになります。 そのため、改善の余地があります。

    これを修正するために、マルチステージビルドを追加しましょう。 まず、ステップを分離し、別の基本イメージを使用して下部に別のステージを追加しましょう。 次に、コピー命令をフラグ –from と共に使用し、コピー元のステージの名前を指定します。 次に、現在のステージにコピーするファイルまたはファイル。 ベースステージがあり、イメージステージですべてのビルドコマンドを実行します。 前の段階からこの完成したバイナリをコピーする以外のコマンドは実行しません。 これで、そのアプリケーションを構築でき、これら 2 つの画像のサイズ ( 25 メガバイトと 600 メガバイト) を比較できます。 つまり、このサイズの約 5%です。 私たちが構築した新しいイメージは、2つのレイヤーになっています。 これは、ベース イメージ レイヤー、Alpine イメージ、およびコピー先のバイナリです。

    キャッシュ・マウントとバインド・マウント

    次に、マウントについて見ていきます。 ビルドのパフォーマンスを向上させるために使用できるマウントにはさまざまな種類があります。 今日はキャッシュマウントとバインドマウントについて説明します。 キャッシュマウントを使用すると、ビルドキャッシュに永続ストレージを使用できます。 また、パッケージ化されたキャッシュを維持するのに役立ちます。 依存関係を変更する場合でも、1 つまたは 2 つの依存関係を変更するだけであれば、すべてを再ダウンロードする必要はありません。 バインドマウントは、ビルドに使用するコンテナにファイルをコピーするためのより効率的な方法です。 したがって、私たちのプロジェクトはGoプロジェクトであり、使用しているGoモジュールのキャッシュマウントを追加したいと思います。

    いくつかのドキュメントを閲覧した後、Goモジュールのキャッシュディレクトリは、使用しているイメージの/ go / pkg / modであることがわかりました。 これは、キャッシュマウントを設定するディレクトリであり、そのディレクトリ内にあるものはすべて永続的にキャッシュされます。 これを行うには、Dockerfile に戻ります。 そして、go modのダウンロードステップでは、ここに別のフラグ–mountを追加し、cacheと入力してから、キャッシュするディレクトリのディレクトリをターゲットにします。 これにより、go mod downloadコマンドがキャッシュマウントに書き込めるようになります。 次に、build コマンドに同じものを追加して、build コマンドが同じキャッシュから読み取れるようにします。 ここで、依存関係の 1 つを変更した場合、たとえば、使用している新しいバージョンの echo にアップグレードします。 他のすべての既存の依存関係では、キャッシュはそれらに対して引き続き有効であるか、それらに対して引き続き使用されます。 したがって、これはパフォーマンスにとって素晴らしいことです。

    バインドマウントは、コンテキストファイルをビルドコンテナにマウントする方法です。 copy と非常によく似ていますが、ファイルを一時的にマウントするだけです。 そのため、最終的なコンテナーにこれらのファイルを必要としないビルド ステップに適しています。 確かに、それはより効率的であり、おそらくビルドでそのようなタイプの効率を探しているエッジケースです。 しかし、いずれにせよ、このケースではバインドマウントを使用することをお勧めします。

    Dockerfile に戻ってバインド マウントを追加する場合は、ここでも –mount フラグを使用します。 そして、go.mod ファイルと go.sum ファイルを go mod download run 命令に追加します。 したがって、これは、これら 2 つのファイルをビルド コンテキストからコンテナーにコピーせずに直接バインドしています。 次に、それらのファイルがすでにマウントされているため、その前にコピー命令を実際に削除できます。 次に、build コマンドに同じものを追加できます。 今回は、ディレクトリ全体をマウントします。 ソースを出力できます。 私はターゲットドットの現在のディレクトリを使用するだけです。 これにより、すべてのビルド コンテキストがコンテナーにマウントされます。 そして、ここでもコピー命令を削除します。 大丈夫です。 このDockerfileはかなり見栄えがします。

    ビルド引数

    最適化から少し離れて、ビルド引数について説明します。 ビルド引数は、たとえば、ビルド時に値を挿入できる優れた方法です。 バイナリをビルドしている場合は、起動前後やダッシュダッシュバージョンの呼び出しなどでバイナリのバージョンを出力できるようにしたい場合があります。 ビルド引数は、そのための優れた方法です。 また、ビルド引数が Dockerfile 内のパッケージのバージョンを便利に管理する方法についても説明します。

    この場合、ここでデモするのは、バージョンのプリントアウトのようなものです。 サーバーを起動すると、実行中の現在のバージョンが出力されます。 そして、ビルド引数でそれを制御して、ソースコードにハードコーディングするのではなく、バイナリをビルドするときにそのバージョンを設定できるようにしたいと考えています。

    Docker ファイルにビルド引数を追加するには、arg 命令または arg キーワードを使用します。 ここでは、ビルド引数バージョンを作成します。 デフォルト値を 0、 0、 0に設定します。 次に、ここでの指示でその引数を使用できます。 Go で変数値を上書きするには、リンカー フラグを使用します。 それが、この種の謎めいたフラグがここで行っていることです。 コード内の version 変数を、ビルド引数 version の値に設定します。 したがって、–build arg フラグを使用してバイナリまたはイメージをビルドすると、コンテナーの実行時に表示される値になります。

    もう一つ、私がいつもやりたいのは、必ずしもDockerfileにバージョンを散りばめるのは好きではないということです。 できることは、バージョン値をトップレベルの引数として設定することです。 そして、ベースイメージを選択するときにそれらを参照できます。 だからFROM golang、そして私はビルド引数go versionを使用します。 また、必要に応じて、Dockerfileを変更することなく、別のGoバージョンでこのイメージを簡単にビルドすることもできます。 また、デフォルト値を–build argフラグで上書きすることもできます。

    テスティング

    他のものを見てみましょう。 議論の構築については、後でマルチプラットフォームを行うときにも説明します。 では、テストについて見てみましょう。 最近まで、Docker buildでテストできることを知りませんでした。 それは私には接続されませんでしたが、どうやらあなたは接続できます。 また、テストを実行するための分離された環境が提供されるため、非常に便利です。 並列のように分離されたテストを実行できますが、これは他の方法では実行が非常に困難です。 しかし、Docker build と BuildKit を使用すると、それは非常に簡単です。 これは、テストを実行するための一貫した環境を作成するのに役立ちます。 そのため、同僚のマシンではテストが失敗しても、Dockerでテストを実行していて、すべて同じバージョンを使用しているため、同僚のマシンではテストが機能しているというケースはありません。

    テストを実行するために、まずここで雑用を行い、現在のベース ステージを 2 つのステージに分割します。 これは、すべての依存関係を設定するベースステージと、バイナリをコンパイルするビルドステージを持つように、それを分離するためのものです。 次に、ベースをベースとして使用して、ステージ test を呼び出して、ここに新しいステージを再度追加します。 この段階では、go test コマンドを実行し、現在のリポジトリにあるテストを実行します。 また、バインドマウントとキャッシュマウントを使用すると、パッケージや依存関係を再度ダウンロードする必要もありません。

    これで、— target を使用して Docker build でこのテストを実行し、実行する Docker ファイル内のステージを選択できます。 また、Docker ビルド ターゲット test が実行され、テスト ステージがビルドされます。 テストが失敗すると、ビルドは失敗します。 テストが失敗しなければ、ビルドは成功します。 これは、テストが再現可能な環境で実行され、何が起こっているかをより適切に制御するための非常に優れた方法です。

    テスト結果のエクスポート

    次に、画像から結果をエクスポートする方法を見てみましょう。 テストがあります。 イメージも構築できているので、すべてがうまくいっています。 エクスポートを追加して、コンテナー イメージ以外のものも構築できます。 そのため、最初に行うことは、テストの実行後にテスト結果とテストカバレッジレポートをファイルシステムにエクスポートする方法を追加することです。

    ここでも、いつものように Dockerfile に戻ります。 まず、テストのカバレッジ レポートを生成するフラグを追加し、テスト結果をファイルにパイプ処理します。 この時点で、テストを実行する 2 つのファイルを作成しています。 カバレッジ レポートを作成し、テスト結果ファイルを作成しています。 そして、次のビットは慣用句ではないかもしれません。 確信がもてません。 次に、そのコマンドの終了コードを確認し、エラーが発生した場合は結果を出力し、ビルドを終了して失敗させます。 さて、基本的にここで行っていた変更は、テスト段階で、カバレッジのテスト結果を2つのファイルに出力することでした。 次に、たとえば、これらのテスト結果やカバレッジレポートをCIにアーカイブする場合に、これらのファイルをエクスポートできるようにしましょう。

    そのために、新しいステージを追加し、今回は FROM SCRATCH イメージを使用します。 これはDockerの特別なイメージであり、何も含まれていないイメージです。 これは、何も入っていない完全に空の画像です。 この段階で私が行っているのは、テストステージからこれら2つのテストファイルをコピーすることだけです。 それです。 これにより、2つのファイルのみを含む空の画像が作成されます。 便利なのは、ビルドコマンドを実行できるようになったことです。 そして、私は-oフラグを使用しますが、あなたがそれを見ることができるかどうかはわかりませんが、コマンドの最後には-oとパスがあり、ビルド結果が何であれ、マシン上のディレクトリに出力できます。 ここでは、2 つのファイルのみを含む空の基本イメージを選択するターゲット エクスポート テストを必ず使用します。 これを実行すると、テスト結果とカバレッジレポートの2つのファイルを含むoutディレクトリが得られます。

    ここで行ったことを別の方法で見てみましょう。 つまり、ベースイメージ、つまりgolangベース、Dockerの公式イメージがあります。 これをテストのベースとして使用し、テストを実行し、これら 2 つのファイルを作成します。 次に、完全に空白の新しいステージを作成し、これら 2 つのファイルをコピーして完了です。 そして、それをファイルシステムにエクスポートします。 これで、このステージをファイルシステムにもエクスポートできます。 これには大量のファイルが含まれます。 ですから、それには注意してください。 コンテナー内にあるものが何であれ、ダッシュ、ダッシュ出力、または -o フラグを使用すると、指定したディレクトリにエクスポートされます。

    また、この手法を使用して、バイナリを作成し、バイナリをエクスポートし、イメージをビルドするだけでなく、たとえばリリースを取得したり、マシンで実行したりするためにアップロードできるバイナリをビルドすることもできます。 同じ手法を使用しているため、新しいステージを最初から使用し、ビルド結果をこのステージにコピーします。 以上です。 これにより、Docker buildでバイナリを直接ビルドしてエクスポートし、ここでもoutフラグまたはダッシュダッシュ出力フラグを使用できます。 したがって、ここでは-oフラグを使用してビルドしており、ファイルシステム上にLinuxアーム64 バイナリが作成されました。 これについては、後で説明します。

    マルチプラットフォームビルド

    さらにいくつかの点を取り上げます。 次に、マルチプラットフォームビルドを用意します。 マルチプラットフォーム ビルドは、複数の異なるアーキテクチャで実行できるイメージをビルドする方法です。 既定では、イメージをビルドすると、現在実行している CPU アーキテクチャがターゲットになります。 私はMac、M1 Macを使用しています。 そのため、イメージを構築するときは、Arm64 イメージになります。 しかし、x86 マシンの場合、Linux イメージを構築している場合は、Linux AMD64 イメージになります。

    マルチプラットフォームを使用すると、さまざまなプラットフォームで実行できるイメージを構築できます。 そして、これは 1 つの Dockerfile を使用して、1 つのイメージを作成するパターンです。 しかし、アーキテクチャごとに異なる Dockerfile を使用したり、アーキテクチャごとに異なるタグを作成したりする人を実際に見たことがあるかもしれません。 そして、それはあなたが本当に追求したくないパターンです。 マルチプラットフォーム イメージを使用すると、それをはるかに簡単に行うことができます。

    マルチプラットフォームビルドを行うには、3つの方法があります。 エミュレーションがあります。 したがって、QEMUを使用して非ネイティブプラットフォームでビルドをエミュレートします。 複数の異なるネイティブノードを使用し、ビルダーである BuildKit を、それぞれ異なるアーキテクチャの複数のノードで実行し、ネイティブにビルドすることができます。 これは、使用するには複雑な設定です。 3番目のオプションは、非常に素晴らしいことですが、可能な場合はクロスコンパイルを使用することです。 そして、それはあなたの言語やコンパイラの機能を利用して、clangやllvm、gccなどの非ネイティブバイナリを生成することです。 Goでは、Rustでそれを行うことができます。 それも簡単です。

    ここでは、エミュレーションとクロスコンパイル、およびいくつかの違いについて説明します。 エミュレーションを使用すると、マルチプラットフォーム イメージの構築を開始する最も簡単な方法です。 Dockerfile について何も変更する必要はありません。 欠点は、これが非常に遅くなる可能性があることです。 通常のビルドを実行するよりも 10x 遅くなる可能性があります。 これは、作業の CPU 負荷の程度によって異なります。 しかし、はい、それが問題にならない場合もあります。 たとえば、ビルドが非常に速く、それがわずかな違いの問題である場合、エミュレーションはまったく問題ありません。 決めるのはあなた次第です。

    マルチプラットフォームの使用を開始する前に、2 つのことのうちの 1 つを選択する必要があります。 Dockerのネイティブビルダーとデフォルトのイメージストアは、現時点ではそれをサポートしていないためです。 たとえば、Dockerコンテナドライバーを使用して新しいビルダーを作成できます。 これにより、一度に複数のプラットフォーム向けに実行およびビルドできるようになります。 または、実験的な機能である containerd イメージ ストアを使用して、既定のイメージ ストアを containerd を使用するように変更することもできます。 そして、私はこれを行うことをお勧めします。 これは素晴らしい機能です。 毎日使っています。 だからそれを試してみてください。 しかし、はい、それが実験的であることを知っておいてください。 しかし、これにより、ビルダーを交換することなくマルチプラットフォームイメージを構築できます。

    エミュレーション

    エミュレーションを使用すると、マルチプラットフォーム イメージを簡単に構築できます。 ビルド コマンドに –platform フラグを追加するだけです。 次に、ビルドするプラットフォームを指定します。 この場合、Linux AMD64 と Linux Arn64. 繰り返しになりますが、前述したように、このプロジェクトのネイティブアーキテクチャ用にビルドすることは 8であるため、ここで考慮すべきパフォーマンスのペナルティがあります。9 お代わり。 エミュレーションを使用したビルドは 32 秒です。 x 10ほどではありませんが、はい、間違いなく遅くなります。 さて、マルチプラットフォームイメージを構築した後は、次のようになります。 Docker イメージ LS を実行する場合は、2 つのタグがあります。 現時点では、さまざまなプラットフォームを示すUXの懸念があります。 ただし、このイメージを Docker Hub にプッシュすると、Docker Hub は、このイメージがマルチプラットフォームになったことを示します。 この2つのプラットフォームは、Docker Hubでタグを調べるとリストアップされているので、x86 マシンでそのイメージをプルすれば、問題なく動作することになります。 これがエミュレーションです。

    クロスコンパイル

    それでは、クロスコンパイルを使用してマルチプラットフォーム イメージを構築する、はるかにエキサイティングな方法を見てみましょう。 これははるかに高速です。 そして、ネイティブアーキテクチャから構築しています。 エミュレーションは実行していませんが、マシンのネイティブ アーキテクチャからビルドし、ターゲットとする他のすべての異なるアーキテクチャを出力します。 ただし、Dockerfile を変更する必要があります。 Docker ビルドに存在する事前定義されたビルド引数のいくつかを利用する必要があるためです。 ここにリストされていますが、すべてではありません。 はい、これらのビルド引数を使用してバイナリをクロスコンパイルします。 これらのビルド引数がどのように機能するかの例を次に示します。 –platform フラグを何に設定するかによって、これらのビルド引数はビルド内で異なる方法で解決されます。 とりあえず、参考までに残しておきます。 これについては、ドキュメントでも説明されています。

    大まかに言うとこんな感じです。 Dockerfile があり、これらの特別なターゲット OS とターゲット アーキテクチャのビルド引数を使用します。 次に、そのステージを基本的に 2 つに分割し、指定したプラットフォームごとに同時に実行します。 これは本当に効率的で素晴らしいことです。 Goでクロスコンパイルするには、よく知らない方のために、これらのGOOS変数とGOARCH変数を、ビルドするOSとプラットフォームに設定できます。 次に、通常どおり go build コマンドを実行すると、指定したバイナリが作成されます。 この場合、GOOSをLinuxに、GOARCHをAMD64 に設定し、Linux AMD64 バイナリを作成します。

    これを Dockerfile で活用する方法を見てみましょう。 最初に行うことは、使用しているビルドプラットフォームを固定することです。 これは、既定では、プラットフォーム フラグに複数の値を指定して実行すると、エミュレートが試行されるため、エミュレーションが発生しないようにするためです。 しかし、これを追加しても、何もエミュレートされません。 つまり、ビルドプラットフォームをマシンが現在実行しているものに固定することになります。

    ビルド ステージでは、これらの定義済みのビルド引数をいくつか追加しますが、これは、このステージでこれらのビルド引数を使用して、それらを使用することができることを意味します。 ターゲット OS は、プラットフォーム ターゲットの OS に解決され、ARCH は CPU アーキテクチャです。 次に、これらのGo固有の変数をそれらの値に設定できますが、少なくとも私が使用したケースでは、これらが1対1にマッピングされているように見えるため、これは非常に簡単です。 しかし、はい、これは使用しているコンパイラによって異なります。

    Cコンパイラを使用している場合は、これを行う方法が異なります。 しかし、幸いなことに、そのための助っ人がいます。 Tõnis Tiigiによる XXという本当に素晴らしいプロジェクトがあります。 マルチプラットフォームビルドを多数行っていて、クロスコンパイルでそれを行う予定がある場合は、それをより簡単な方法で行うのに役立つ優れたプロジェクトであるため、これを確認してください。 これは、クロスコンパイルを使用したビルドのスクリーンショットです。 エミュレーションによるビルドは 32 秒かそこらでした。クロスコンパイルでは、同じターゲットが 9されます。2 お代わり。 通常のネイティブビルドよりもコンマ数秒長いだけです。

    本当にエキサイティングなのは、これを先ほど見たエクスポート機能、つまり、クロスコンパイルを使用するだけで、マルチプラットフォームを構築し、異なるアーキテクチャの複数のバイナリをすべて同じビルドでエクスポートできることです。 したがって、この場合、2 つのアーキテクチャ用にビルドし、出力ディレクトリを調べると、指定されたアーキテクチャ用に 2 つの異なるバイナリが作成されています。 これを使用して、ローカルアーキテクチャから構築することもできます。 実際には、ここではローカルのプラットフォームを使用する必要があります。 しかし、はい、これにより、クロスコンパイルを使用してダーウィンバイナリを構築することもでき、それを自分のマシンで実行できます。

    結論

    さて、これはDockerで構築し、Dockerfileのさまざまな機能を利用するためのヒントとコツでした。 この時点で、Dockerファイルは次のようになります。 これは私にはかなり良さそうです。 ここでは、レイヤー、マルチステージビルド、キャッシュマウント、バインドマウント、マルチプラットフォームビルド、ビルド引数、テスト、結果のエクスポートなどの機能を取り上げました。 これを自分で試してみたい場合は、 ドキュメントのビルドガイド を確認して、手順を実行してください。 よし、これでおしまい。 ありがとうございました。

    さらに詳しく

    この記事には、DockerCon 2023のプレゼンテーションの YouTube トランスクリプトが含まれています。 「Dockerfile: From Start to Optimized」は、DockerのテクニカルライターであるDavid Karlsson氏によって発表されました。

    自分に合ったサブスクリプションを見つける

    今すぐ専門家に連絡して、Dockerサブスクリプションのコラボレーション、セキュリティ、サポートの完璧なバランスを見つけてください。