ソフトウェア開発の現場では、複数のオペレーティング システム (OS) やプロセッサー アーキテクチャで動作するアーティファクトを作成しなければならない場面が少なくありません。しかし、設計時に想定していたものとは異なる OS やアーキテクチャでアプリケーションを実行するのは、ほとんど不可能に近い所業です。このような理由から、多種多様なプラットフォームに応じたバージョンをビルドし、リリースするという手法が広まっています。しかし、アーティファクトのビルドに使用しているプラットフォームがデプロイ先のプラットフォームと異なる場合には、このようなやり方では対応が困難です。たとえば、アプリケーションの開発プラットフォームが Windows、デプロイ先が Linux や macOS のマシンであるような場合には、デプロイ先の OS やアーキテクチャごとにビルド マシンのプロビジョニングと設定の作業が必要になります。さまざまなテクニックを駆使すれば、パイプラインで複数の OS に対応したビルド実現することも不可能ではありません。しかし、プロセッサーのアーキテクチャは制約が厳しいため、デプロイ先と同じハードウェアでコンパイルと生成を行う必要があります。
Docker を使うと、いくつかのアプリケーションをパッケージ化し、デプロイに対応した変更不可能なアーティファクト (Docker イメージとコンテナ) にまとめ上げることができます。しかし、ビルドに際してプロセッサーのアーキテクチャに起因する制約に悩まされるという点では、Docker イメージも、従来型のパッケージ化手法と変わるところはありません。Docker イメージも、イメージを実行するハードウェアと同じアーキテクチャのハードウェアでビルドする必要があるのです。この記事では、CI パイプラインで Linux/amd64、Linux/arm64、Linux/riscv64 など、さまざまなプロセッサー アーキテクチャに対応した Docker イメージをビルドする方法を説明します。
はじめに
まずは、Chad Metcalf 氏によるサンプル コードのリポジトリをご覧になり、アプリケーションをパッケージ化してマルチアーキテクチャ対応の Docker イメージを作成する方法の例を確認してください。なお、この記事では、サンプルのうち継続的インテグレーションに関する部分を重点的に取り上げるつもりです。サンプルに含まれる CircleCI の config.yml
は、CI パイプラインのビルドに関する命令を記述したファイルです。このファイルは、.circleci/config.yml
ディレクトリにあります。この記事では、このリポジトリのファイルのなかから、特に .circleci/config.yml
と Makefile
の 2 つを見ていきます。
Makefile は、ビルドのプロセスを自動化する Make ユーティリティに必要なビルド/コンパイルの命令をまとめたものであると考えることができます。このプロジェクトの Makefile
には、CI パイプラインから実行する命令やコマンドが書かれています。
Docker Buildx を使用する
Makefile
と config.yml
のファイルを詳しく見ていく前に、Buildx について少し説明しておきましょう。Buildx は Docker CLI の拡張プラグインで、Moby BuildKit ビルダー ツールキットの全機能を Docker CLI で利用できるようにするものです。ユーザー エクスペリエンスは docker build
と同じですが、スコープの決まったビルダー インスタンスの作成や、多数のノードを対象とした同時ビルドなど、多くの機能が加わっています。
Buildx は、本稿執筆時点ではまだ試験的機能の段階です。このため、利用にあたっては Docker イメージをビルドするマシンに多少の環境設定が必要です。以下に示したのは、バージョン 19.03
以降の Docker を対象とした Buildx のインストール手順です。また、以下はあくまで、Docker 19.03 をインストールしてある Linux マシンを対象とした簡易版の手順ですのでご注意ください。それ以前のバージョンの Docker をお使いの場合も含めた Buildx の正式なインストール手順は、こちらを参照してください。以下のコマンドで、ソースから Buildx
バイナリをコンパイルおよびビルドし、Docker のプラグイン ディレクトリにインストールします。
export DOCKER_BUILDKIT=1
docker build --platform=local -o . git://github.com/docker/buildx
mkdir -p ~/.docker/cli-plugins
mv buildx ~/.docker/cli-plugins/docker-buildx
このほか、お使いの OS に応じた最新の Buildx バイナリをこちらからダウンロードし、バイナリ リリースを対象としたこちらの手順に従ってインストールするという方法もあります。
Docker ビルダー マシンに Buildx をインストールすると、以下をはじめとする Buildx の機能がすべて利用できるようになります。
Buildx の機能について、一度時間を取って確認してみることをお勧めします。Buildx は、マルチアーキテクチャ対応の Docker イメージを作成する際に欠かすことのできない技術であり、以下の例でも大いに活用しています。
CI パイプラインを設定する
サンプル プロジェクトの config.yml
ファイルでは、マルチアーキテクチャ対応のビルド実現に向けた各種のコマンドを実行するうえで、Makefile
ファイルとその機能を使用しています。この config.yml
では、単体のビルド ジョブで machine executor を使用しています。ひょっとすると、この方法が奇異に感じられる方もいらっしゃるかもしれません。CircleCI なら Docker executor を使って Docker イメージをビルドできるからです。
Docker プラットフォームでは、仮想マシン (VM) のようにカーネルをエミュレートするのではなく、ホスト OS のカーネルを共有および管理する手法を採用しています。つまり、Docker コンテナは実行中にホスト OS のカーネルを共有するので、その点で VM とはアーキテクチャが大幅に異なります。対する VM の方は、コンテナ技術をベースとするものではなく、OS のユーザー空間とカーネル空間をどちらも備えています。VM のサーバー ハードウェアは仮想化されており、VM にはそれぞれ固有の OS とアプリがあります。ホストとはハードウェア リソースを共有するだけにとどまるので、VM 内部でさまざまなプロセッサー アーキテクチャやカーネルをエミュレートできます。VM がこのようにカーネルとハードウェアをエミュレートできるという点こそ、マルチアーキテクチャ対応の Docker イメージをビルドするうえで machine
executor が最善の選択となる一番の理由です。
それでは、サンプル プロジェクトの config.yml
ファイルを見ていきましょう。
version: 2.1
jobs:
build:
machine:
image: ubuntu-1604:202007-01
environment:
DOCKER_BUILDKIT: 1
BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6
steps:
- checkout
- run:
name: Unit Tests
command: make test
- run:
name: Log in to docker hub
command: |
docker login -u $DOCKER_USER -p $DOCKER_PASS
- run:
name: Build from dockerfile
command: |
TAG=edge make build
- run:
name: Push to docker hub
command: |
TAG=edge make push
- run:
name: Compose Up
command: |
TAG=edge make run
- run:
name: Check running containers
command: |
docker ps -a
- run:
name: Check logs
command: |
TAG=edge make logs
- run:
name: Compose down
command: |
TAG=edge make down
- run:
name: Install buildx
command: |
BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.4.2/buildx-v0.4.2.linux-amd64"
curl --output docker-buildx \
--silent --show-error --location --fail --retry 3 \
"$BUILDX_BINARY_URL"
mkdir -p ~/.docker/cli-plugins
mv docker-buildx ~/.docker/cli-plugins/
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx install
# Run binfmt
docker run --rm --privileged tonistiigi/binfmt:latest --install "$BUILDX_PLATFORMS"
- run:
name: Tag golden
command: |
BUILDX_PLATFORMS="$BUILDX_PLATFORMS" make cross-build
お気付きの方もいらっしゃるかもしれませんが、この設定ファイルの command:
キーのほとんどは、Makefile
内の関数を実行するようになっています。このパターンは設定ファイルの YAML 構文を大幅に少なくすることができるものの、Makefile
で実際にどんな処理が実行されるのかがわかりにくくなるという欠点もはらんでいます。
では、この設定ファイルの command:
キーのなかから、特に重要なものを見ていきましょう。
version: 2.1
jobs:
build:
machine:
image: ubuntu-1604:202007-01
environment:
DOCKER_BUILDKIT: 1
BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6
steps:
- checkout
- run:
name: Unit Tests
command: make test
- run:
name: Log in to docker hub
command: |
docker login -u $DOCKER_USER -p $DOCKER_PASS
- run:
name: Build from dockerfile
command: |
TAG=edge make build
- run:
name: Push to docker hub
command: |
TAG=edge make push
上のコードでは、ビルドに machine
executor を使用するとともに、Docker で試験的機能と Buildx にアクセスするための変数 DOCKER_BUILDKIT
に値を代入しています。変数 BUILDX_PLATFORMS
は、Docker イメージを生成する OS とプロセッサー アーキテクチャのリストです。今回のリストでは、Linux OS とさまざまなプロセッサー アーキテクチャをターゲットに指定しています。
残りの run:
キーと command:
キーでは、アプリケーションの単体テストの実行方法、Docker Hub イメージのプッシュとプルに向けた Docker Hub への認証方法、/app
ディレクトリにある Dockerfile
を使った Docker イメージのビルド方法、Docker Hub にそのイメージをプッシュする方法を指定しています。
注: 上の docker login
のステップは、Docker Hub を対象としたリクエストを認証済みの状態にするためのものです。 プッシュ、プルを問わず、CircleCI を使って Docker Hub との間でイメージをやり取りする場合には、CircleCI の設定ファイルの docker pull
と docker push
のいずれにも支障が出ないよう、Docker Hub アカウントにログインすることをお勧めします。ログインすると、ジョブに対する Docker Hub のプル回数制限が緩和されます。
- run:
name: Install buildx
command: |
BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.4.2/buildx-v0.4.2.linux-amd64"
curl --output docker-buildx \
--silent --show-error --location --fail --retry 3 \
"$BUILDX_BINARY_URL"
mkdir -p ~/.docker/cli-plugins
mv docker-buildx ~/.docker/cli-plugins/
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx install
# Run binfmt
docker run --rm --privileged tonistiigi/binfmt:latest --install "$BUILDX_PLATFORMS"
上のコード スニペットでは、Buildx の機能を使って Buildx のバイナリをインストールしたうえで、Executor で使用するための設定をしています。Buildx というツールでマルチアーキテクチャ対応のイメージをビルドする方法はさまざまですが、最も簡単なのは QEMU を使うことです。QEMU は、マシンのエミュレーションと仮想化に広く使えるオープン ソース ツールです。BuildKit では、別のアーキテクチャのためのバイナリを実行しなければならない局面が発生すると、そのバイナリを binfmt_misc
ハンドラーに登録されているバイナリを使って自動で読み込みます。ホスト OS の binfmt_misc
に登録されている QEMU バイナリがコンテナ内部で透過的に動作するためには、fix_binary
フラグを設定した状態でバイナリを登録しておく必要があります。
docker run --rm --privileged tonistiigi/binfmt:latest --install "$BUILDX_PLATFORMS"
コマンドは、binfmt コンテナをプルしたうえで、先ほどファイル内で定義していた変数 $BUILD_PLATFORMS
に列挙されているプラットフォームそれぞれに生成するものです。
- run:
name: Tag golden
command: |
BUILDX_PLATFORMS="$BUILDX_PLATFORMS" make cross-build
上のコード スニペットは、パイプラインで最後に実行するコマンドを指定したものです。具体的には、目標とするマルチアーキテクチャ対応の Docker イメージをビルドするコードです。command:
キーは、Makefile
にある cross-build
という関数を呼び出しています。では、この関数に関連するコマンドをいくつか見ていきましょう。
# Makefile cross-build function
.PHONY: cross-build
cross-build:
@docker buildx create --name mybuilder --use
@docker buildx build --platform ${BUILDX_PLATFORMS} -t ${PROD_IMAGE} --push ./app
上のコード スニペットは、実際の cross-build
make
コマンドです。このコマンドではまず、新しい Buildx ビルダー インスタンスを作成します。続いて docker buildx build
コマンドにより、環境変数 ${BUILDX_PLATFORMS}
に列挙されているプラットフォームのそれぞれに個別の Docker イメージをビルドするプロセスを開始します。${BUILDX_PLATFORMS}
は、コマンドの --platform
フラグの引数となっています。-t
フラグは Docker イメージにタグ/名前を付けるもの、--push
フラグはビルド結果を自動的に Docker レジストリにプッシュするためのものです。今回の場合、Docker レジストリは Docker Hub になります。
まとめ
この記事では、さまざまな OS やプロセッサー アーキテクチャに対応した Docker イメージを 1 つの CI パイプラインからビルドする方法を紹介しました。また、Docker Buildx についても簡単に紹介しました。現時点で Buildx はまだ試験段階ですが、Docker の今後のリリースでビルド ユーティリティのデファクトになることが予想されます。私は、Buildx が Docker イメージのビルドに関して次世代を担うツールになると考えています。Buildx の包括的かつ高度で最適化された機能の数々により、Docker イメージのビルドは今後もっと便利になっていくことでしょう。
このほか、さまざまな OS やプラットフォーム アーキテクチャを対象とした Docker イメージをビルドする難しさについても簡単に説明しました。Docker コンテナと VM の技術的な違いを説明したのもここです。この 2 つは、抽象的な話にとどまるならよく似ていますが、核となる部分を考えると根本的に別物なのでした。最後に、繰り返しにはなりますが、Docker が新たに Docker Hub のプル回数制限を導入したという点を強調しておきたいと思います。つまり、Docker Hub を宛先とする呼び出しはすべて、認証が必要になりました。プッシュ、プルを問わず、CircleCI を使って Docker Hub との間でイメージをやり取りする場合には、CircleCI の設定ファイルの docker pull
と docker push
のいずれにも支障が出ないよう、Docker Hub アカウントにログインすることをお勧めします。
この記事をお読みくださり、ありがとうございました。皆さんのお役に立てたなら幸いです。ご意見やご感想がございましたら、Twitter でお気軽に @punkdata 宛にメンションしてください。