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

これは、クラウドネイティブのコンサルタントであり、Kubernetes認定サービスプロバイダーであるContainer SolutionsのチーフサイエンティストであるDockerキャプテン Adrian Mouat からのゲスト投稿です。 Adrian は O'Reilly Media から出版された "Using Docker" の著者です。 彼は現在、Kubernetesクラスター内のイメージのフローを安全に管理するように設計されたコンテナイメージレジストリであるTrowを開発しています。 エイドリアンは定期的な会議の講演者兼トレーナーであり、KubeCon EU、DockerCon、CraftConf、TuringFest、GOTO Amsterdamなどのいくつかのイベントで講演しています。

Docker イメージは、新しいソフトウェアやサードパーティ製ソフトウェアをテストおよびデプロイするための標準ツールになりました。 私はオープンソースのTrowレジストリの主な開発者であり、Dockerイメージは人々がツールをインストールする主な方法です。 私が画像を提供しなかった場合、他の人は自分でロールすることになり、作業が重複し、メンテナンスの問題が発生します。

デフォルトでは、作成するDockerイメージはlinux/amd64プラットフォーム上で実行されます。 これは、開発マシンやクラウドプロバイダーの大部分で機能しますが、他のプラットフォームのユーザーは寒さにさらされません。 Raspberry Pisから構築されたホームラボ、IoTデバイスを製造する企業、IBMメインフレームで稼働している組織、低電力arm64チップを利用するクラウドなど、これはかなりの聴衆です。 これらのプラットフォームのユーザーは、通常、独自のイメージを構築するか、別のソリューションを見つけます。

では、これらの他のプラットフォーム用のイメージを構築するにはどうすればよいでしょうか。 最も明白な方法は、単にターゲットプラットフォーム自体にイメージを構築することです。 これは多くの場合に機能しますが、s390xをターゲットにしている場合は、IBMメインフレームにアクセスできることを願っています(Phil Estesがガレージにいくつか持っていると聞いています)。 Raspberry PisやIoTデバイスなどのより一般的なプラットフォームは、通常、電力が制限されており、低速であるか、イメージを構築できません。

では、代わりに何ができるでしょうか? さらに2つのオプションがあります:1)ターゲットプラットフォームをエミュレートするか、2)クロスコンパイルします。 興味深いことに、2つのオプションを組み合わせるのが最も効果的であることがわかりました。

エミュレーション

最初のオプションであるエミュレーションを見てみましょう。 QEMUと呼ばれる素晴らしいプロジェクトがあり、たくさんのプラットフォームをエミュレートできます。最近の buildx の作業により、QEMU を Docker で使用するのがこれまでになく簡単になりました。

QEMU統合は、binfmt_miscハンドラーの少し不可解な名前を持つLinuxカーネル機能に依存しています。 Linuxが認識できない実行可能ファイル形式(つまり、異なるアーキテクチャ用の形式)を検出すると、その形式(つまり、エミュレータまたはVM)を処理するように構成された「ユーザースペースアプリケーション」があるかどうかをハンドラーに確認します。 存在する場合は、実行可能ファイルをアプリケーションに渡します。

これを機能させるには、関心のあるプラットフォームをカーネルに登録する必要があります。 Dockerデスクトップを使用している場合、これは最も一般的なプラットフォームですでに行われています。 Linuxを使用している場合は、最新のdocker/binfmtイメージを実行することで、Dockerデスクトップと同じ方法でハンドラーを登録できます。

docker run --privileged --rm docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64

これを行った後、Dockerを再起動する必要がある場合があります。 登録するプラットフォームをもう少し制御したい場合、またはより難解なプラットフォームを使用したい場合(例: PowerPC) を参照してください。

buildxを使用する方法はいくつかありますが、最も簡単な方法は、Docker CLIで実験的な機能をまだ有効にしていない場合は、編集することです~/.docker/config.json 以下を含めるには:

{
    ...
     "experimental": “enabled”
}

これで実行docker buildx lsできるようになり、次のような出力が得られるはずです。

$ docker buildx ls
NAME/NODE     DRIVER/ENDPOINT             STATUS   PLATFORMS
default       docker                               
  default     default                     running  linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

別のプラットフォーム用のイメージを構築してみましょう。 このドッカーファイルから始めます。

FROM debian:buster

CMD uname -m

通常どおりにビルドして実行すると、次のようになります。

$ docker buildx build -t local-build .
…
$ docker run --rm local-build
x86_64

しかし、ビルドするプラットフォームに明示的に名前を付けると、次のようになります。

$ docker buildx build --platform linux/arm/v7 -t arm-build .
…
$ docker run --rm arm-build
armv7l

成功!armv7 イメージをx86_64ラップトップでビルドして実行することができました。 この手法は効果的ですが、より複雑なビルドでは、実行が遅すぎたり、QEMUでバグが発生したりすることがあります。 そのような場合は、イメージをクロスコンパイルできるかどうかを調べる価値があります。

クロスコンパイル

いくつかのコンパイラは、特にGoやRustを含む外部プラットフォーム用のバイナリを出力することができます。 Trow レジストリ プロジェクトでは、クロスコンパイルが他のプラットフォーム用のイメージを作成するための最も迅速で信頼性の高い方法であることがわかりました。 たとえば、Trow armv7 イメージの Dockerfile を次に示します。 最も関連性の高い行は次のとおりです。

RUN cargo build --target armv7-unknown-linux-gnueabihf -Z unstable-options --out-dir ./out 

これは、バイナリを実行するプラットフォームをRustに明示的に指示します。 次に、マルチステージビルドを使用して、このバイナリをターゲットアーキテクチャの基本イメージにコピーできます(静的にコンパイルした場合はスクラッチを使用することもできます)、完了です。 ただし、Trowレジストリの場合、最終的なイメージに設定したいものがさらにいくつかあるため、最終段階は実際には次のように始まります。

FROM --platform=linux/arm/v7 debian:stable-slim 

このため、私は実際にはエミュレーションとクロスコンパイルの両方のブレンドを使用しています - クロスコンパイルは、最終的なイメージを実行して構成するためのバイナリとエミュレーションを作成しています。

マニフェスト リスト

エミュレーションに関する上記のアドバイスでは、引数を使用して--platformビルドプラットフォームを設定しましたが、FROM行debian:busterで指定されたイメージを のままにしました。 これは意味をなさないように見えるかもしれません - 確かにプラットフォームは、ユーザーが後の段階で決定することではなく、ベースイメージとその構築方法に依存しますか?

ここで起こっていることは、Dockerがマニフェストリストと呼ばれるものを使用していることです。 これらは、さまざまなアーキテクチャのイメージへのポインターを含む特定のイメージのリストです。 公式の Debian イメージにはマニフェストリストが定義されているので、ラップトップでイメージをプルすると自動的に amd64 イメージが取得され、Raspberry Pi でプルすると armv7 イメージが取得されます。

ユーザーを満足させるために、独自の画像のマニフェストリストを作成できます。 前の例に戻ると、まずイメージを再構築してリポジトリにプッシュする必要があります。

$ docker buildx build --platform linux/arm/v7 -t amouat/arch-test:armv7 .
…
$ docker push amouat/arch-test:armv7
…
$ docker buildx build -t amouat/arch-test:amd64 .
…
$ docker push amouat/arch-test:amd64

次に、これら 2 つの個別のイメージを指すマニフェスト リストを作成し、それをプッシュします。

$ docker manifest create amouat/arch-test:blog amouat/arch-test:amd64 amouat/arch-test:armv7
Created manifest list docker.io/amouat/arch-test:blog
$ docker manifest push amouat/arch-test:blog
sha256:039dd768fc0758fbe82e3296d40b45f71fd69768f21bb9e0da02d0fb28c67648

これで、Dockerは現在のプラットフォームに適したイメージをプルして実行します。

$ docker run amouat/arch-test:blog
Unable to find image 'amouat/arch-test:blog' locally
blog: Pulling from amouat/arch-test
Digest: sha256:039dd768fc0758fbe82e3296d40b45f71fd69768f21bb9e0da02d0fb28c67648
Status: Downloaded newer image for amouat/arch-test:blog
x86_64

Raspberry Piを手にしている人は、イメージを実行してみて、そのプラットフォームでも実際に機能することを確認できます。

要約すると;Docker イメージのすべてのユーザーが amd64 を実行しているわけではありません。 buildx と QEMU を使用すると、わずかな追加作業でこれらのユーザーをサポートすることができます。

お誕生日おめでとう、ドッカー!