こんにちは、バックエンドエンジニアの藤井脩紀です。 今回は表題の通りCloud Buildを活用してDockerのマルチプラットフォームイメージをビルドする方法をご紹介しようと思います。
マルチプラットフォーム is 何
まず、マルチプラットフォームイメージとは何かから説明すると、複数のCPUアーキテクチャやOSに対応したイメージのことです。
より具体的にはDockerの公式ドキュメントとブログが参考になるかと思いますので挙げておきます。
動機
そしてマルチプラットフォームのイメージが必要となった背景ですが、Appleシリコンの登場によりローカルでの開発ではARM、サーバー上ではAMDのイメージが欲しいというニーズが生まれました。これに応えるべくマルチプラットフォームイメージのビルド方法を考えることになりました。
プラットフォームを意識したバイナリのビルド
前提知識としてですが基本的に実行ファイルをビルドする際は何も意識しなければそのビルド環境向けのネイティブバイナリが生成されることになります。
ではどうやってビルド環境と異なるプラットフォーム用にビルドするのかというと、QEMUによるエミュレーションやクロスコンパイルなどの選択肢が挙げられます。ただし、エミュレーションは実行速度が遅かったり、クロスコンパイルはビルドツールがサポートしている必要があるなどの課題があります。
なので理想論としてはやはりそれぞれネイティブな環境でビルドしたくなります。 そこで本記事では、Cloud Buildを活用してそれを実現する方法をご紹介します。
Remote Builder
ではCloud Buildを活用して具体的にどのように実現するかというお話ですが、 コミュニティ提供ビルダー である remote-builder を活用することにします。(注:こちらはOSSとして管理されているビルダーでありGoogleの公式製品ではありません)
remote-builderはDockerイメージであり、Compute Engineを立ち上げてそのインスタンス上でコマンドを実行してから破棄してくれるものです。こちらをCloud Buildのステップとして活用していくことになります。(図1参照)
ただし、こちらそのままでも十分汎用性のある実装になっているのですが、せっかくのOSSなのでフォークしたものを自分の使いやすいように改修して利用することにします。
そのフォークしたものが以下になります。(注:こちらはこのブログ用に作成したものでありミラティブで運用しているものとは異なります)
主要な変更点は以下の2つです。
(1) 起動スクリプトの指定
Compute Engineの立ち上げ後に実行される起動スクリプトを指定して、Dockerのインストールやユーザの作成などを行なっています。
コマンドとしては gcloud compute instances create
に --metadata-from-file "startup-script=${STARTUP_SCRIPT}"
という引数を指定しています。注意点としてコマンドの実行終了時点では起動スクリプトの完了は保証されないため独自に完了を判定する必要があります。
(2) インスタンスの実行時間制限
gcloud compute instances create
のデフォルト引数として --instance-termination-action=DELETE --max-run-duration=${INSTANCE_MAX_DURATION}
を追加しています。これはCompute Engineの起動時間に制限を行うもので、正常なケースであればremote-builderが適切にCompute Engineを破棄してくれるはずですが、予期せぬ事態に備えての安全策としてこちらを指定しています。なおこちらの引数ですが少なくともベースイメージとして使用している google/cloud-sdk:464.0.0
では完全な正式リリースがなされていないようで gcloud beta compute instaces create
とする必要がありました。
詳細は公式のドキュメントを参照してください。
VM のランタイムを制限する | Compute Engine ドキュメント | Google Cloud
主な変更点の紹介は以上です。 差分は以下のプルリクエストにおおよそまとまっていますので変更の詳細に興味があればそちらをご覧ください。
Docker remote builder by noi · Pull Request #2 · noi/gcp-docker-remote-builder · GitHub
ちなみにライセンスはフォーク元のままでApache License 2.0になっています。
How to Build
ここから具体的なマルチプラットフォームイメージのビルド方法について説明していきます。
以下を前提条件としてお話しするのでご注意ください。
- Cloud BuildがCompute EngineとContainer Registryを扱う権限を持っている
- Compute EngineもContainer Registryを扱う権限を持っている
また、DockerイメージのレジストリとしてContainer Registryを活用しているのですが、こちらは現在非推奨となっておりArtifact Registryに移行する必要があります。ミラティブのバックエンドでは現在移行中のためご容赦ください。
下準備
remote-builderを活用する前に、それ自体をビルドする必要があります。
まずリポジトリをクローンしてきます。
$ git clone https://github.com/noi/gcp-docker-remote-builder.git $ cd gcp-docker-remote-builder $ git checkout tags/blog1
そして、以下のコマンドを実行してCloud Build上でビルドを行うと gcr.io/$PROJECT_ID/docker-remote-builder
がContainer Registryにプッシュされます。($PROJECT_ID
はCloud Buildを実行するプロジェクトのIDに置き換わります)
$ gcloud builds submit --config ./cloudbuild.yaml .
これで下準備は完了です。
remote-builderの利用方法について少しだけ説明するとCloud Buildの構成ファイル上で以下のように活用します。
steps: - name: "gcr.io/$PROJECT_ID/docker-remote-builder" env: - INSTANCE_ARGS=--preemptible - ZONE=asia-southeast1-b - COMMAND=echo hoge
パラメータはenvとして指定します。最低限としては gcloud compute instances create
の引数として使用される INSTANCE_ARGS
と立ち上げたインスタンス上で実行される COMMAND
の2つを覚えておけば大丈夫だと思います。詳細についてはREADMEに記載があるのでご覧ください。(注:フォーク元とフォークで内容が異なる点にご注意ください)
マルチプラットフォームイメージのビルド
Dockerのマルチプラットフォームイメージの話に戻るのですが、前提としてこちらは複数のプラットフォームで動く魔法のバイナリという訳ではありません。その実体はそれぞれのプラットフォームごとの個別のイメージとなっており、それらをまとめるマニュフェストの存在によって実現されています。マニュフェストにはプラットフォームとイメージのマッピング情報が含まれており、イメージを取得する際にそのプラットフォームに適したものを選んでくれるという形になっています。つまりマルチプラットフォームイメージをビルドする際は、まずそれぞれのプラットフォーム用のイメージをビルドしてからマニュフェストを作成することになります。
よってremote-builderによるマルチプラットフォームイメージのビルドは以下の図2のような全体像になります。
軽く説明すると、remote-builderを用いた2つの並列ステップでAMDとARMのインスタンスを立ち上げてそれぞれのプラットフォーム用のイメージをビルドしてそのままContainer Registryにプッシュ、最後にもう一つのステップでマニュフェストを作成してそれもプッシュするという流れになります。
そしてこちらを行うためのCloud Build構成ファイル例が以下になります
steps: - id: 'build-and-push-amd64' name: "gcr.io/$PROJECT_ID/docker-remote-builder" env: - INSTANCE_ARGS=--preemptible --image-project ubuntu-os-cloud --image-family ubuntu-2204-lts --machine-type=n1-standard-4 --scopes=storage-rw - ZONE=asia-southeast1-b - COMMAND=gcloud auth configure-docker gcr.io -q && docker buildx build --platform=linux/amd64 -t gcr.io/$PROJECT_ID/docker-remote-builder-example:0.0.0-amd64 --push . waitFor: ['-'] - id: 'build-and-push-arm64' name: "gcr.io/$PROJECT_ID/docker-remote-builder" env: - INSTANCE_ARGS=--preemptible --image-project ubuntu-os-cloud --image-family ubuntu-2204-lts-arm64 --machine-type=t2a-standard-4 --scopes=storage-rw - ZONE=asia-southeast1-b - COMMAND=gcloud auth configure-docker gcr.io -q && docker buildx build --platform=linux/arm64 -t gcr.io/$PROJECT_ID/docker-remote-builder-example:0.0.0-arm64 --push . waitFor: ['-'] - id: 'create-and-push-manifest' name: 'gcr.io/cloud-builders/docker' entrypoint: 'bash' args: - -c - | set -ueo pipefail uri="gcr.io/$PROJECT_ID/docker-remote-builder-example:0.0.0" docker manifest create $${uri} $${uri}-arm64 $${uri}-amd64 docker manifest inspect $${uri} docker manifest push $${uri} waitFor: ['build-and-push-amd64', 'build-and-push-arm64']
こちらの例の重要な部分について解説していきます。
INSTANCE_ARGSについて
重要なのは --image-project
、 --image-family
、 --machine-type
の3つです。これらによってCompute EngineのOSやマシン種別を指定しています。
今回はそれぞれ以下の引数を指定してUbuntu 22.04のCompute Engineを立ち上げています。
- AMD64
--image-project ubuntu-os-cloud --image-family ubuntu-2204-lts --machine-type=n1-standard-4
- ARM64
--image-project ubuntu-os-cloud --image-family ubuntu-2204-lts-arm64 --machine-type=t2a-standard-4
これらの引数を選定する際に参考になりそうな情報も載せておきます。
--image-project
&--image-family
- 一覧確認コマンド:
gcloud compute images list
- ドキュメント:オペレーティング システムの詳細 | Compute Engine ドキュメント | Google Cloud
- 一覧確認コマンド:
--machine-type
を選ぶ際は- 一覧確認コマンド:
gcloud compute machine-types list
- ドキュメント:Compute Engine の汎用マシン ファミリー | Compute Engine ドキュメント | Google Cloud
- 一覧確認コマンド:
なお、--scopes=storage-rw
はContainer Registryを利用するための指定です。
COMMANDについて
まず、gcloud auth configure-docker gcr.io -q
の部分はContainer Registryを利用するための指定です。
主目的であるビルドには docker buildx build
を利用していますが、重要なのは --platform
です。
AMD64は linux/amd64
、 ARM64では linux/arm64
を指定しています。意味については見たままだと思うので特に言及することはありません。
マニュフェストの作成について
最後のステップである create-and-push-manifest
を見ていきます。
複雑な部分もないのでさほど説明の必要はないかもしれませんが、docker manifest create
の1つ目の引数がマニュフェストのタグで、後ろ2つがremote-builderでプッシュ済みのイメージのタグになります。イメージを取得する際はマニュフェストのタグを指定することになります。docker manifest inspect
は作成したマニュフェストの内容を確認するものなので省略してもいいと思います。そして docker manifest push
でプッシュして全てのタスクが完了となります。
注意点として、docker manifest
は実験的な機能である点に気をつけてください。
本記事の執筆に際して改めて調査してみたのですが、docker buildx imagetools create で代替可能かもしれません。(こちらには実験的な機能という表示がない)
ビルド方法の説明は以上になります。
課題
上記の手法で実際にビルドはできたもののブラッシュアップが足りておらず課題が残っているためそれについてもいくつか書いておきます。
(1) Compute Engineの立ち上げに時間がかかる
現状の手法だとCompute Engineをオンデマンドに利用しているため、毎回インスタンスの作成と初期化に時間がかかってしまうという課題を抱えています。 このような手法を用いる一番の理由は費用面への考慮になるかと思いますのでもしその面が許容できるのであれば、常時起動のインスタンスを利用する、またはインスタンスを作成&削除するのではなく起動&停止するといったインスタンスを再利用する形に変更することで時間短縮を図れるかと思われます。
また費用面への考慮に関連してここでご紹介させていただくのですが、インスタンスの作成時にはSpot VMを利用することでディスカウントを受けることができます。フォークのremote-builderではこちらを利用するようにしています。(gcloud compute instances create
に --provisioning-model=SPOT
を指定することで利用可能)
その他にはCUDを利用して料金を引き下げる方法などもありますので、このあたりは懐事情や利用状況などに相談する形になるかと思います。
(2) ビルドキャッシュが残らない
ビルドに利用するインスタンスを毎回破棄するため、何か手を加えないとキャッシュが残らず毎回一からのビルドになってしまいます。この対策としては上記で述べたインスタンスを再利用する方法や、docker buildx build
コマンドに --cache-from
を追加して前回のビルドイメージをキャッシュとして扱うなどが考えられるかと思います。
(3) マニュフェストを自前で作成する必要がある
今回ご紹介した手法では自前でマニュフェストを作成していますが、実は docker buildx build
の --platform
は --platform=linux/amd64,linux/arm64
のように複数指定することが可能になっており、その場合マニュフェストまで作成してくれます。ビルド環境と異なるプラットフォームはどうするのかというと特定のプラットフォームのビルドをリモートインスタンスで行うといった設定を行うことで解決できます。つまり linux/amd64
のビルドはCloud Build、linux/arm64
はCompute Engineで立ち上げたホストという構成にしてCloud Build上で docker buildx build
コマンドを一度叩くだけというシンプルな形にすることも可能だと思われます。
ではなぜそのようなやり方にしなかったのかというと、一つの理由としてremote-builderをさらに専用の形に改修する必要がありそうだったからです。そもそもremote-builderのよいところの一つとして汎用性の高さが挙げられるかと思います。Dockerイメージに限らず汎用的に活用できるので極度な変更は避けたかったというのがあり一旦今の形に落ち着いています。ただし一度試してみたいとも思っています。
上記の構成で行う場合は以下が参考になるかと思われます。
まとめ
今回の記事ではCloud Buildを活用したマルチプラットフォームのDockerイメージのビルド方法についてご紹介させていただきました。
まだブラッシュアップの余地がある状態ではありますが remote-builder を含む コミュニティ提供ビルダー というものがあるという情報はいつか役に立つかもしれないのでそれだけでも覚えていっていただけると幸いです。
それからここまで触れていなかったのですが、先日公式からDocker Build Cloudというものが公開されました。
今回ご紹介した方法を構築したのはそれ以前なので選択肢には挙がりませんでしたが、比較検討してみたいとは思っています。ただしミラティブのバックエンドでは基本的にビルドやCIをGoogle Cloudで完結しているので同様にGoogle Cloudに閉じたい方の参考になればと思います。
さいごに
ミラティブではバックエンドエンジニアを含め複数のポジションで力を貸してくださる方々を募集しています!