linuxでマルチアーキテクチャに対応したdockerイメージを作ろうの巻(buildx)

はじめに

現在使われているコンピューターには殆どの場合CPUが存在します。

CPUにも種類があり数年前までは、x86というインテルとAMDのCPUが一般的なコンピュータの主流でしたが、スマートフォンの普及によりARMというCPUも紛れ込むようになりました。

ARMを利用したコンピューターを名指しするとラズベリーパイですね、はい。

で、最近流行りのdockerですが、有名所のイメージ以外はこのARMに対応していない場合がありますし、自分が作るイメージもARMのイメージ作りたかったらARM機でイメージを作る必要がりました。

開発機x86じゃん?一緒にARMのイメージも作りたいじゃん?

それできるよ。そうbuildxならね

仕組み

MACだと意識せずできるっぽいので置いておいて、linuxの話をします。

そもそもx86でARMのイメージ作りたければ、

x86でARMが動作する状態でなけれないけません。

x86でARMを動かすには、ARMのエミュレーターを動かす必要があります。

これをlinuxカーネルとエミュレーターのqemuがいい感じにやってくれるのがbinfmt_miscというものです。

その上でdockerのbuildxを実行した環境で実行可能なCPUアーキテクチャのイメージが作成できるという感じです。

前提として以下のコマンドが実行できなければ、カーネルが仕組みを利用できないので、注意してください。

sudo /sbin/modprobe binfmt_misc

x86でarmを動かそう

実際にx86でarmのバイナリを動かしてみます。最終的にはこれが実行できるようになります。

まずは動かないことを確認

まずはx86であることを確認します。

~# uname -m
x86_64

今回の実験台としてbusybox君に登場してもらいます。x86とarmをダウンロードして実行してみます。

~# wget  https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
~# wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-armv8l
~# chmod +x busybox-*

x86はちゃんと動きますね。

~# ./busybox-x86_64 
BusyBox v1.31.0 (2019-06-10 15:54:54 CEST) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2015.
Licensed under GPLv2. See source distribution for detailed
copyright notices.

armはというと、「このバイナリ実行できないよ」と言われています。

実行できないというよりは、実行方法が登録されていないという意味合いです。

~# ./busybox-armv8l 
bash: ./busybox-armv8l: cannot execute binary file: Exec format error

もちろんarm版ubuntuイメージも動作しません

~# docker run --rm -t arm64v8/ubuntu uname -m
Unable to find image 'arm64v8/ubuntu:latest' locally
latest: Pulling from arm64v8/ubuntu
a25fe3630538: Pull complete 
326fa3abf061: Pull complete 
ff1b87601e0a: Pull complete 
Digest: sha256:b6c4e3af24ef7a0ff26b56188fc15f6a868dacca523050fb30fc0267bc51b861
Status: Downloaded newer image for arm64v8/ubuntu:latest
standard_init_linux.go:211: exec user process caused "exec format error"

qemuのインストール

まずはエミュレーターがなければどうにもできないのでこれをインストールします。

~# sudo apt update
~# sudo apt install qemu-user qemu-user-static

そうするとCPUのエミュレータが使えるようになりました。

~# qemu-
qemu-aarch64              qemu-armeb                qemu-i386-static          qemu-mips-static          qemu-mipsn32-static       qemu-ppc-static           qemu-riscv32-static       qemu-sh4eb-static         qemu-tilegx-static
qemu-aarch64-static       qemu-armeb-static         qemu-m68k                 qemu-mips64               qemu-mipsn32el            qemu-ppc64                qemu-riscv64              qemu-sparc                qemu-x86_64
qemu-aarch64_be           qemu-cris                 qemu-m68k-static          qemu-mips64-static        qemu-mipsn32el-static     qemu-ppc64-static         qemu-riscv64-static       qemu-sparc-static         qemu-x86_64-static
qemu-aarch64_be-static    qemu-cris-static          qemu-microblaze           qemu-mips64el             qemu-nios2                qemu-ppc64abi32           qemu-s390x                qemu-sparc32plus          qemu-xtensa
qemu-alpha                qemu-debootstrap          qemu-microblaze-static    qemu-mips64el-static      qemu-nios2-static         qemu-ppc64abi32-static    qemu-s390x-static         qemu-sparc32plus-static   qemu-xtensa-static
qemu-alpha-static         qemu-hppa                 qemu-microblazeel         qemu-mipsel               qemu-or1k                 qemu-ppc64le              qemu-sh4                  qemu-sparc64              qemu-xtensaeb
qemu-arm                  qemu-hppa-static          qemu-microblazeel-static  qemu-mipsel-static        qemu-or1k-static          qemu-ppc64le-static       qemu-sh4-static           qemu-sparc64-static       qemu-xtensaeb-static
qemu-arm-static           qemu-i386                 qemu-mips                 qemu-mipsn32              qemu-ppc                  qemu-riscv32              qemu-sh4eb                qemu-tilegx 

大丈夫だと思いますが、qemu-*-staticが「/usr/bin/」にあることを確認してください。下記で紹介するスクリプトがここにコマンドがある事を前提に動作します。

~# which qemu-aarch64-static 
/usr/bin/qemu-aarch64-static

エミュレータをカーネルに登録

このままだと実行できる環境があるだけで、実際にこれを利用するようにカーネルに登録しないといけません。

これは起動時に毎回行う必要があるので、systemdに登録でもしましょう。

まずは必要なものを集めます。

~# wget https://raw.githubusercontent.com/qemu/qemu/master/scripts/qemu-binfmt-conf.sh
~# chmod +x qemu-binfmt-conf.sh 
~# sudo mv qemu-binfmt-conf.sh /usr/local/bin/qemu-binfmt-conf.sh
cat <<EOF | sudo tee /usr/local/bin/register.sh
#!/bin/sh
QEMU_BIN_DIR=${QEMU_BIN_DIR:-/usr/bin}
if [ ! -d /proc/sys/fs/binfmt_misc ]; then
    echo "No binfmt support in the kernel."
    echo "  Try: '/sbin/modprobe binfmt_misc' from the host"
    exit 1
fi
if [ ! -f /proc/sys/fs/binfmt_misc/register ]; then
    mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
fi
if [ "${1}" = "--reset" ]; then
    shift
    find /proc/sys/fs/binfmt_misc -type f -name 'qemu-*' -exec sh -c 'echo -1 > {}' \;
fi
exec /usr/local/bin/qemu-binfmt-conf.sh --qemu-suffix "-static" --qemu-path "${QEMU_BIN_DIR}" $@
EOF
sudo chmod +x /usr/local/bin/register.sh
cat <<EOF | sudo tee /etc/systemd/system/register.service
[Unit]
Description= register cpu emulator
[Service]
ExecStart = /usr/local/bin/register.sh
Restart = no
Type = simple
RemainAfterExit=yes
[Install]
WantedBy = multi-user.target
EOF

登録を行います。

sudo systemctl daemon-reload
sudo systemctl enable register.service
sudo systemctl start register.service

以下のようになれば登録されました。

sudo systemctl status register.service
● register.service - register cpu emulator
     Loaded: loaded (/etc/systemd/system/register.service; enabled; vendor preset: enabled)
     Active: active (exited) since Tue 2020-10-20 14:03:03 JST; 10s ago
    Process: 2592756 ExecStart=/usr/local/bin/register.sh (code=exited, status=0/SUCCESS)
   Main PID: 2592756 (code=exited, status=0/SUCCESS)
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-riscv64-static as binfmt interpreter for riscv64
10月 20 14:03:03 niilab register.sh[2592756]: /usr/local/bin/qemu-binfmt-conf.sh: 269: echo: echo: I/O error
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-xtensa-static as binfmt interpreter for xtensa
10月 20 14:03:03 niilab register.sh[2592756]: /usr/local/bin/qemu-binfmt-conf.sh: 269: echo: echo: I/O error
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-xtensaeb-static as binfmt interpreter for xtensaeb
10月 20 14:03:03 niilab register.sh[2592756]: /usr/local/bin/qemu-binfmt-conf.sh: 269: echo: echo: I/O error
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-microblaze-static as binfmt interpreter for microblaze
10月 20 14:03:03 niilab register.sh[2592756]: /usr/local/bin/qemu-binfmt-conf.sh: 269: echo: echo: I/O error
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-microblazeel-static as binfmt interpreter for microblazeel
10月 20 14:03:03 niilab register.sh[2592756]: Setting /qemu-or1k-static as binfmt interpreter for or1k

実際にarm版のbusyboxを動かすと動作します。

./busybox-armv8l 
BusyBox v1.31.0 (2019-06-10 15:54:51 CEST) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2015.
Licensed under GPLv2. See source distribution for detailed
copyright notices.

arm版のイメージも動くことがわかります。

docker run --rm -t arm64v8/ubuntu uname -m
aarch64

マルチアーキテクチャイメージを作ろう

ここまでできればあとはイメージ作成の設定を行うだけでできます。

ただしこの機能は実験的なものであるため、デフォルトでは利用できません。

dockerの設定

以下のように実験的機能を有効にします。

mkdir ~/.docker/
cat <<EOF | tee ~/.docker/config.json
{
  "experimental": "enabled"
}
EOF

そうすると*がついたものが表示されるようになります。

docker help
Usage:  docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
Options:
      --config string      Location of client config files (default "/home/niilab/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/home/niilab/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/home/niilab/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/home/niilab/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit
Management Commands:
  app*        Docker Application (Docker Inc., v0.8.0)
  builder     Manage builds
  buildx*     Build with BuildKit (Docker Inc., v0.4.2-tp-docker)
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts
  engine      Manage the docker engine
  image       Manage images
  manifest    Manage Docker image manifests and manifest lists
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes

現在利用しているイメージ作成エンジンはマルチアーキテクチャに対応していないので、新しいビルドプロファイルを作成します。

この際、このプロファイルでビルド可能な候補を登録します。

docker buildx create --name multi-arch-builder --platform linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6

そうすると新しく追加されます。

docker buildx ls
NAME/NODE             DRIVER/ENDPOINT             STATUS   PLATFORMS
multi-arch-builder    docker-container                     
  multi-arch-builder0 unix:///var/run/docker.sock inactive linux/amd64*, linux/386*, linux/arm64*, linux/arm/v7*, linux/arm/v6*
default *             docker                               
  default             default                     running  linux/amd64, linux/386

これを利用してイメージを作成する指定を行います。

docker buildx use multi-arch-builder

最後に出力先にdockerhubを用いるのでログインします。

docker login
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /home/server/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded

マルチアーキテクチャのイメージを作る

ここまで来たらあとはビルドするだけです。

以下のようなdockerfileを用意しました。

FROM    python:3.8-alpine
COPY    ./test.py /test.py
RUN     python3 /test.py
CMD     python3 /test.py
import platform↲
print(platform.uname())

ビルドしたいアーキテクチャを指定して実行します。この際useridとなっている場所は上記でログインしたアカウント名です。

docker buildx build --platform linux/amd64,linux/arm64 -t userid/python-test --push  .

buildができるとdockerhubに登録されます。

まとめ

以上がマルチアーキテクチャに対応したdockerイメージを作ろうの巻でした

ちゃんちゃん