x86のWindows(WSL2)でマルチアーキテクチャイメージをビルドする

# 1. はじめに

株式会社オークファン 開発部の @ngok です。

昨今、M1 Macの登場やAWS Gravitonなどの低価格なインスタンスの利用が可能になったことにより ARMアーキテクチャのプロセッサが普及しました。
ただ、依然としてWindowsのPCにおいてはIntelやAMD製のx86アーキテクチャのプロセッサを使用することが多く、異なるCPUアーキテクチャにも対応した開発を求められることが多くなりつつあります。

本記事では x86のWindowsPCで、x86にもARMにも対応したマルチアーキテクチャのdockerイメージをビルドする方法を記載します。
また、マルチアーキテクチャイメージはdockerリポジトリにプッシュする必要がありますが、社内で使うことを想定してAWSのECRを使用します。


# 2. 経緯

オークファンでは社員に貸与するPCについて、WindowsまたはMacを社員が選択が可能となっています。

開発部でもWindowsのPCを使用している人もいればMacを使用している人もいて、それぞれに対応した開発環境が必要になっています。
開発環境にしばしば利用していた docker は、各人の使用OSに依存せずに利用でき便利でした。。。

そう、 ***M1 Mac*** が登場するまでは！！

基本的にdockerイメージをビルドしたマシンのCPUアーキテクチャ用にイメージがビルドされます。
WindowsPCのほとんどはIntelやAMD製のx86のCPUアーキテクチャですが、 M1のCPUを使用したMacでは ARMのCPUアーキテクチャとなります。

そのため、WindowsのPCでビルドしたイメージをM1 Macで起動しようとした場合エラーになり、その逆もしかりでした。
チーム内でのdockerイメージの共有が難しくなってしまいました。

Mac勢とWindows勢の隔たりが厚くなった瞬間でした。


# 3. 前提

- ホストOSにはWindows10を使用します
- PCのCPUはx86系です
- ビルド環境はWSL2 Ubuntu20.04を使用します
- docker をインストールしておきます
- docker buildx をインストールしておきます
- ECRにリポジトリを作成しておきます
- ビルドするイメージのDockerFileは任意で作成しておきます


# 4. マルチアーキテクチャイメージのビルド

## buildx のビルダーを作成

マルチアーキテクチャイメージをビルドするにはbuildxコマンドで使用するビルダーを設定する必要があります。
ビルダーに ARMアーキテクチャに対応したエミュレータ(qemu)を設定します。


- buildxに登録されているビルダーを確認する。  
linux/arm64などのARMアーキテクチャがPLATFORMSにないことを確認します。

```console
$ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT      STATUS  BUILDKIT                              PLATFORMS
default      docker
  default    default              running v0.11.7-0.20230525183624-798ad6b0ce9f linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
```

- ARM実行可能ファイルを登録する。実行後に `linux/arm64` などのARMアーキテクチャのPLATFORMSが追加されていることを確認します。  
最新の [qumuイメージ](https://hub.docker.com/r/docker/binfmt/tags?_gl=1*3559sx*_ga*MTk4NzM5NjYxLjE2OTg5MTExNTc.*_ga_XJWPQMJYHQ*MTY5OTQ5NDkxNi4zLjEuMTY5OTQ5NDkzOS4zNy4wLjA.)を使用します。  

```console
$ docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
$ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT      STATUS  BUILDKIT                              PLATFORMS
default      docker
  default    default              running v0.11.7-0.20230525183624-798ad6b0ce9f linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386, linux/arm64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6
```

- 新規のビルダーインスタンスを作成します。

```console
$ docker buildx create --name mybuilder
mybuilder
$ docker buildx ls
NAME/NODE             DRIVER/ENDPOINT      STATUS   BUILDKIT                              PLATFORMS
mybuilder             docker-container
  mybuilder           tcp://localhost:2375 inactive
default *             docker
  default             default              running  v0.11.7-0.20230525183624-798ad6b0ce9f linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386, linux/arm64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6
```

- 使用するビルダーを作成したビルダーに切り替えます。


```console
$ docker buildx use mybuilder
$ docker buildx ls
NAME/NODE             DRIVER/ENDPOINT      STATUS   BUILDKIT                              PLATFORMS
mybuilder *           docker-container
  mybuilder           tcp://localhost:2375 inactive
default               docker
  default             default              running  v0.11.7-0.20230525183624-798ad6b0ce9f linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386, linux/arm64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6
```

- ビルダーインスタンスを起動します。
```console
$ docker buildx inspect --bootstrap
[+] Building 1.1s (1/1) FINISHED
 => [internal] booting buildkit                                                                                    1.1s
 => => starting container buildx_buildkit_mybuilder0                                                               1.1s
Name:          mybuilder
Driver:        docker-container
Last Activity: 2023-11-02 08:27:49 +0000 UTC

Nodes:
Name:      mybuilder0
Endpoint:  tcp://localhost:2375
Status:    running
Buildkit:  v0.12.3
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
Labels:
 org.mobyproject.buildkit.worker.executor:         oci
 org.mobyproject.buildkit.worker.hostname:         0a171ebcd99e
 org.mobyproject.buildkit.worker.network:          host
 org.mobyproject.buildkit.worker.oci.process-mode: sandbox
 org.mobyproject.buildkit.worker.selinux.enabled:  false
 org.mobyproject.buildkit.worker.snapshotter:      overlayfs
GC Policy rule#0:
 All:           false
 Filters:       type==source.local,type==exec.cachemount,type==source.git.checkout
 Keep Duration: 48h0m0s
 Keep Bytes:    488.3MiB
GC Policy rule#1:
 All:           false
 Keep Duration: 1440h0m0s
 Keep Bytes:    24.21GiB
GC Policy rule#2:
 All:        false
 Keep Bytes: 24.21GiB
GC Policy rule#3:
 All:        true
 Keep Bytes: 24.21GiB
```

- ビルダーの一覧を表示して、ステータスが `running` になっていることを確認します。

``` console
$ docker buildx ls
NAME/NODE             DRIVER/ENDPOINT      STATUS   BUILDKIT                              PLATFORMS
mybuilder *           docker-container
  mybuilder0          tcp://localhost:2375 running  v0.12.3                               linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default               docker
  default             default              running  v0.11.7-0.20230525183624-798ad6b0ce9f linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386, linux/arm64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6
```

- ビルダーのイメージとコンテナの一覧を確認します。

```console
$ docker image ls | grep buildkit
moby/buildkit                                                                          buildx-stable-1                            ee33f441bff7   2 weeks ago     172MB

$ docker container ls | grep buildkit
0a171ebcd99e   moby/buildkit:buildx-stable-1   "buildkitd"              6 days ago     Up 2 minutes                                                buildx_buildkit_mybuilder0
```

## dockerイメージのビルドとプッシュ

x86とARMの両方に対応したイメージをビルドしてECRにプッシュします。  
イメージのビルドと同時にプッシュをします。  
これは、ローカルのdockerには自身のアーキテクチャに合ったイメージのみ保持することが可能で、マルチアーキテクチャイメージを持つことが出来ないためです。  

- ECRに接続できるようにログインします。(ECRのコンソール画面にあるプッシュ方法に記載がある内容です)

```console
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com
```

- ビルドしてECRにプッシュします。

```console
$ cd {DockerFileのあるディレクトリ}
$ docker buildx build --platform linux/amd64,linux/arm64 -t {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/{ECRのレジストリ名}:{タグ名} --push .
```

- AWSのコンソールでECRのイメージ一覧を表示してプッシュを確認します。( `アーティファクト・タイプ` に `image index` で追加されていること)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538612/e8b3d3a0-abe8-e391-356e-99a3758708c6.png)



# 5. 動作確認

## ビルドしたイメージをプルして起動

上記の手順でビルドしたイメージはローカルには残っていないので、一度プルしてから起動する必要があります。
ECRにプッシュしたマルチアーキテクチャ対応イメージをプルします。

- ECRに接続できるようにログインします。(ECRのコンソール画面にあるプッシュ方法に記載がある内容です)

```console
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com
```

- ECRからイメージをプルします。

```console
$ docker pull {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/{ECRのレジストリ名}:{タグ名}
```

- 取得したイメージを起動します

```console
$ docker run {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/{ECRのレジストリ名}:{タグ名}
```

# 6. 補足

## マルチアーキテクチャイメージをプルして再度プッシュするとマルチアーキテクチャに対応しなくなる

マルチアーキテクチャイメージをプルすると、ローカルに保持されるイメージは使用しているPCのCPUアーキテクチャに合ったイメージのみとなります。  
そのため、再度プッシュしてしまうとマルチアーキテクチャイメージではなくなってしまいます。

(例)
x86マシンでプルしたイメージを再度プッシュするとx86マシンでしか扱えないイメージとなる


## ECRに登録済みのイメージにタグを追加

ECRに既に登録してあるイメージに別のタグを追加したい場合があると思います。  

特定のアーキテクチャに対応したイメージであれば、プルした後に別のタグを設定してプッシュすることでタグを追加することが可能です。
しかし、マルチアーキテクチャイメージでは一度プルしてしまうとマルチアーキテクチャイメージではなくなるため、その手順では行えません。

そのため、ECRに登録されているイメージに対してはタグだけをリポジトリに直接設定する必要があります。  
プルをせずにタグだけを追加する方法を記載します。

### 前提条件

- AWS CLIがインストール済みであること

### 手順

- 対象イメージのmanifest情報を`batch-get-image` コマンドを使用して取得します。  
後の手順で使用するので変数に入れておきます。

```console
$ MANIFEST=$(aws ecr batch-get-image --repository-name {ECRのリポジトリ名} --image-ids imageTag={対象のタグ名} --output text --query images[].imageManifest
$ echo $MANIFEST
```

- 追加するタグを `put-image` コマンドでタグを追加します。
```console
$ aws ecr put-image --repository-name {ECRのリポジトリ名} --image-tag {新しいタグ名} --image-manifest "$MANIFEST"
```

- `describe-images` コマンドでリポジトリ情報を取得してタグが追加されたことを確認します。(AWSのコンソールからECRのイメージ一覧を確認してもいいです)

```console
$ aws ecr describe-images --repository-name {ECRのリポジトリ名}
```

# 7. おわりに

オークファンではECRやECSなど、コンテナ技術を利用したシステム構築を行っています。
そんな中dockerは中心技術になります。
時代や環境に適応した形でキャッチアップを続けていたらと思っています。