三行まとめ
- BuildJet が提供するArmマシンを用いてArmで動くイメージを作成する
- Docker Buildx を用いると複数のイメージに同じタグを貼ることができる
- Actionsのx86_64マシンでamd64で動くイメージを、BuildJetのArmマシンでarm64で動くイメージをそれぞれ作成してくっつけた
はじめに
Apple Silicon搭載マシンの登場、Raspberry Piの普及などArmが身近になってきた人も多いのではないでしょうか。
Raspberry Pi上で動くKubernetesなどを運用する際にはarm64とamd64の双方に対応したイメージが同一のイメージタグで存在すると手元とデプロイ先で同じようにコンテナを使えて嬉しいです。このようなイメージのことをマルチプラットフォームイメージと呼びます。
今回はlinux/amd64とlinux/arm64の双方に対応したマルチプラットフォームイメージをBuildJetを用いて高速に作る話をします。
なおGoやRustのようにクロスコンパイルができる仕組みのある言語で書かれたアプリケーションのマルチプラットフォームイメージは、BuildJetを使わなくてもDockerfileを工夫することによってイメージの作成を速くすることができます。
過去エントリで解説をしているのでそちらも参照ください。
タイトルの8倍という数字はdocker buildx build --platform=linux/amd64,linux/arm64 .
としてNext.jsのコンテナイメージをビルドするGitHub Actionsのworkflowと比較した結果となります。
BuildJetとは
BuildJetはGitHub Actionsのworkflowを実行するためのマシンを提供する*1サービスです。マシンだけを提供しているので、BuildJetを使うからと言ってworkflow定義ファイルの変更をする必要はないです。 buildjet.com
Y Combinatorにも採択されています。
BuildJetには特徴が2つあると思っています。
1つ目がGitHub Actionsが管理しているランナーと比べてシングルコア性能が良いマシンをCore数を揃えて比較すると半額で提供している*2ことです。
2つ目がArmアーキテクチャのマシンを含むたくさんの種類のマシンが使える*3ことです。 GitHub Actions のManaged RunnerのラインナップになかったArmのマシンは使えるのが嬉しいです。今回はこのArmマシンが使えるという特徴を活かします。
それぞれの特徴は脚注のリンクにあるので詳しい説明は本家に任せます。
方法
実際にBuildJetを用いてマルチプラットフォームイメージを作成する方法を紹介していきます。今回紹介するコードは以下のリポジトリに置くのでそちらもぜひ参考にしてください。
BuildJetに登録する
BuildJetに登録します。GitHub Market Placeからも登録できます。
BuildJetを有効にする
リポジトリに対してBuildJetがアクセスできる権限を付与します。あたり前ですがこの権限が無いと使えないです。
runs-onにBuildJetのランナーを指定する
いつもubuntu-latest
と書いているところに、ドキュメント*4を見ながらBuildJetが提供するランナーの名前を書きます。
今回はamd64イメージビルド用としてパブリックリポジトリなら無料で使えるubuntu-latest
、arm64イメージビルド用としてBuildJet最安のbuildjet-2vcpu-ubuntu-2204-arm
を採用しました。
arm64のイメージとamd64のイメージを作るときはruns-onを変えた同じjobを定義する必要があります。runs-onを除いて共通なのでmatrixを用いると良いでしょう。
使ったリポジトリの該当箇所はここです。
# 省略 build: name: Build and Publish image strategy: fail-fast: true matrix: runs-on: - "ubuntu-latest" - "buildjet-2vcpu-ubuntu-2204-arm" runs-on: ${{ matrix.runs-on }} steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io username: "azuki-bar" password: ${{ secrets.GITHUB_TOKEN }} - name: Prepare-tag id: tags run: | arch="" # https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context case "${{ runner.arch }}" in "X64" ) arch="amd64" ;; "ARM64" ) arch="arm64" ;; esac echo "tag=${{ github.sha }}-${arch}" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v4 with: push: true tags: ghcr.io/azuki-bar/hello-buildjet-nextjs:${{ steps.tags.outputs.tag }} provenance: false
後述のイメージを束ねるために使うdocker buildx imagetools create
コマンドは対象のコンテナイメージがコンテナレジストリに存在しているほうが便利に使えます。なので今回はGitHub Container Registryに各プラットフォームのイメージをアップロードしています。
アーキテクチャごとにタグを付けて一度アップロードしていますが、この例では最終的に欲しいタグにアーキテクチャの接尾辞を付けることとしています。多分タグを付けずにコンテナイメージのsha256のままでも良いですが未検証です。
provenance
をfalseにする必要があります。そうしないとプラットフォームがunknownなイメージが最終的に作られてしまいます。
複数のイメージを束ねるマニフェストを作る
前セクションでプラットフォーム固有のタグが付いたイメージができました。しかし一つのタグで複数プラットフォーム対応イメージをダウンロードできるようにするという目的はまだ達成できていないです。ので1つのタグで複数プラットフォーム対応イメージを作成していきます。
複数のイメージタグを束ねて一つのイメージタグにするにはDocker Buildxのdocker buildx imagetools create
コマンド*5を使います。
使ったリポジトリの該当個所はここです。
merge-images: runs-on: "ubuntu-latest" needs: ["build"] steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io username: "azuki-bar" password: ${{ secrets.GITHUB_TOKEN }} - name: Create a New Image run: | image_tag="ghcr.io/azuki-bar/hello-buildjet-nextjs:${{ github.sha }}" docker buildx imagetools create \ --tag ${image_tag} \ ${image_tag}-arm64 \ ${image_tag}-amd64
イメージタグを作成する段階ではarmネイティブマシンは必要ありません。なのでGitHub Actionsのマネージドランナーを使用します。
また注意が必要ですが、docker buildx imagetools create
コマンドは--dry-run
オプションを指定しない限りコンテナレジストリにプッシュしようとします。なので何らかのコンテナレジストリにログインすることは必須です。
めでたしめでたし
以上でマルチプラットフォームに対応したコンテナイメージを作成することができました。
実際に使ってみる
実際にイメージが対応しているのか見てみましょう。
uname
でカーネルとCPUアーキテクチャが何か表示してからイメージを実行してみましょう。
amd64マシンで実行した結果は次の通りです。
$ uname --kernel-name --machine Linux x86_64 $ docker run --rm -p 3000:3000 ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872 Unable to find image 'ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872' locally 1040a07209feac57d0ae064eedd86ef0892da872: Pulling from azuki-bar/hello-buildjet-nextjs a7ca0d9ba68f: Already exists (省略) 39aea93f8f16: Pull complete Digest: sha256:d4a9aac5ad5c9c03f5b2c16c3c879f882a7245f37f2c05e1971c70104b2a362f Status: Downloaded newer image for ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872 Listening on port 3000 url: http://afcdea87b1e1:3000
イメージをプルして実行できていることが分かります。
同様にarm64マシンでも実行してみましょう。
$ uname --kernel-name --machine Linux aarch64 $ docker run --rm -p 3000:3000 ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872 Unable to find image 'ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872' locally 1040a07209feac57d0ae064eedd86ef0892da872: Pulling from azuki-bar/hello-buildjet-nextjs 0b41f743fd4d: Pull complete (省略) b399976cc073: Pull complete Digest: sha256:d4a9aac5ad5c9c03f5b2c16c3c879f882a7245f37f2c05e1971c70104b2a362f Status: Downloaded newer image for ghcr.io/azuki-bar/hello-buildjet-nextjs:1040a07209feac57d0ae064eedd86ef0892da872 Listening on port 3000 url: http://19f0a027d7b1:3000
こちらも同様にイメージをプルして実行できています。
amd64マシンとarm64マシンの両方で同じイメージタグを使っていることに注目してください。 またイメージレイヤごとのハッシュ値が異なっていることにも注目してください。
速度比較
このセクションではQEMUを用いた、おそらく一番メジャーな方法でマルチプラットフォームイメージを作成したときと比較します。
使用したマシンはGitHub Actionsが提供しているubuntu-latest
*6とBuildJetが提供しているbuildjet-2vcpu-ubuntu-2204-arm
*7です。
またビルドするイメージはNext.jsのドキュメント*8にて紹介されているDockerfileを修正したものとします。アプリケーションの中身はnpx create-next-app
をしただけとなっています。
中身が見たい人はこのエントリで何度も参照しているリポジトリ*9を見てください。
runs-on: ubuntu-latest
として QEMU を用いてマルチプラットフォームイメージを作成した場合、トータルで21m46s掛かっています。
https://github.com/Azuki-bar/hello-buildjet/actions/runs/5209520747/jobs/9399463926
今回の記事にした方法でマルチプラットフォームイメージを作成した場合、2m36sかかります。 内訳としてはイメージのビルドにamd64,arm64のそれぞれで1m21s,1m5sかかっていて、マージするのに10sかかっています。
グラフです。
1度しか検証していないので何とも言えないですが、今回の計測ではイメージの作成時間が1/8になったことが分かります。
まとめ
BuildJetが提供するArmマシンを使用することでarm64に対応したイメージをQEMUを使って作成するよりも高速に作成することができます。これがエミュレータを介さない力です。
以上です。
*1:libvirtを用いたオレオレVM管理ツールを作ってベアメタルで運用しているらしい。 https://buildjet.com/for-github-actions/blog/why-we-want-to-fix-github-actions
*2:https://buildjet.com/for-github-actions/docs/getting-started/how-is-buildjet-for-github-actions-different に記述がある。サーバ用CPUではなくゲーミングCPUを使っているらしい。
*3:https://buildjet.com/for-github-actions/docs/runners/hardware に記述がある。2vCPUのマシンから32vCPUのマシンがある。
*4:https://buildjet.com/for-github-actions/docs/runners/hardware にruns-onに指定すべき値が書いてある。
*5:コマンドのドキュメントは https://docs.docker.com/engine/reference/commandline/buildx_imagetools_create/ を参照してください。
*6:2コア7GB
*7:2vCPU3GB
*8:https://nextjs.org/docs/app/building-your-application/deploying#docker-image で https://github.com/vercel/next.js/tree/canary/examples/with-docker-multi-env が例として紹介されている
*9:https://github.com/Azuki-bar/hello-buildjet/blob/1040a07209feac57d0ae064eedd86ef0892da872/Dockerfile