🦾

Windows(x64)上でArm版Dockerを起動してAArch64のCodeBuildコンテナをローカル実行

2024/03/30に公開

TL;DR

ビルドロジックの確認程度の動作ならArmマシンを立ち上げてのDocker使用は不要。
ホストがx64でもqemu-user-staticをインストールし、codebuild_build.shでAArch64のCodeBuildイメージを指定するだけで良い。

以下はホストをArm64(AArch64)とするための内容となる。

  • QEMUでArm64版Ubuntu(Cloud image版)を動かす
  • -machinevirt-9.0以上を指定する (CodeBuild用Amazon Linux向け)
  • Arm版Ubuntu内のDockerでコンテナを実行する

背景

ビルドコマンドのロジック確認程度の動作なら、ホストマシンがWindowsのx64アーキテクチャであってもWSL(Ubuntu)でsudo apt install -y qemu-user-staticqemu-user-staticをインストールした後、
codebuild_build.shでの起動イメージ-i
public.ecr.aws/codebuild/amazonlinux2-aarch64-standard:3.0-2024.03.21といったようにAArch64のリポジトリイメージタグを指定するだけでも一応動作はするようで、速度はある程度遅くなるが手間などを考えればこうするだけでも十分と思われる。
ただしアーキテクチャ差異による副作用が発生するリスクは当然あるので、必ずしも正しい動作やビルドオブジェクトの起動が保証できるわけではない。

  • x64環境上での単体での起動例
    docker run \
        --rm \
        -it \
        --entrypoint bash \
        public.ecr.aws/codebuild/amazonlinux2-aarch64-standard:3.0-2024.03.21 \
        -vc \
        "uname -a"
    
    WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64/v3) and no specific platform was requested
    uname -a
    Linux *** *** ** SMP *** *** ** **:**:** UTC **** aarch64 aarch64 aarch64 GNU/Linux
    
    アーキテクチャはAArch64と認識できている。

そこで、ホストもArm64とさせるためにQEMUを用いてみる。
QEMUはWindows版でも良いが、起動ログ出力ターミナルのLinux向けフォント指定コマンドがコマンドプロンプトで解釈されず、かなり読みにくくなるので体感でもそれほど速度が変わらなかったWSL(Ubuntu)でLinux版を起動する。
ただしQEMUで別アーキテクチャのマシンを起動するとかなり遅くなるので、ホストのアーキテクチャ依存なエラーが発生して先に進めないようなビルドコマンドを記述しているなどといったよほどの事情がない限り、qemu-user-staticを使用する方法で良いと考えている。

内容

Arm64対応QEMUのインストール

もしsudo apt install -y qemu-system-armでインストールした場合、
qemu-system-aarch64 -machine helpコマンドでvirt-9.0 QEMU 9.0 ARM Virtual MachineがなければDocker版のCodeBuild用Amazon Linuxが動かない(補足)ので、自前でビルドする必要がある。
※CodeBuildではないコンテナはvirt-7.2等でも動くものもある。

以下のような場合はCodeBuild用Amazon Linux向けに自前ビルドが「必要」

qemu-system-aarch64 -machine help

・・・
virt-7.0             QEMU 7.0 ARM Virtual Machine
virt-7.1             QEMU 7.1 ARM Virtual Machine
virt                 QEMU 7.2 ARM Virtual Machine (alias of virt-7.2)
virt-7.2             QEMU 7.2 ARM Virtual Machine
witherspoon-bmc      OpenPOWER Witherspoon BMC (ARM1176)
・・・

以下のような場合はCodeBuild用Amazon Linux向けの自前ビルドは「不要」

qemu-system-aarch64 -machine help

・・・
virt-7.2             QEMU 7.2 ARM Virtual Machine
virt-8.0             QEMU 8.0 ARM Virtual Machine
virt-8.1             QEMU 8.1 ARM Virtual Machine
virt-8.2             QEMU 8.2 ARM Virtual Machine
virt                 QEMU 9.0 ARM Virtual Machine (alias of virt-9.0)
virt-9.0             QEMU 9.0 ARM Virtual Machine
witherspoon-bmc      OpenPOWER Witherspoon BMC (ARM1176)
・・・

自前ビルド手順

基本は以下のページの通り。

  1. 作業ディレクトリ作成

    command
    mkdir -p ./qemu_build
    cd ./qemu_build
    
  2. ソースコードの取得

    command
    wget https://download.qemu.org/qemu-9.0.0-rc1.tar.xz
    tar xvJf qemu-9.0.0-rc1.tar.xz
    cd ./qemu-9.0.0-rc1
    

    リンク切れやビルドがうまくいかない場合は最新のコードをgitリポジトリから取得。

    command
    git clone https://gitlab.com/qemu-project/qemu.git . \
        --depth 1
    git submodule init
    git submodule update \
        --recursive \
        --depth 1
    
  3. ビルド
    ホスト環境を汚したくない場合はコンテナに入ってビルド。

    command
    docker run \
        --rm \
        -it \
        -v ./:/work \
        -w /work \
        ubuntu:22.04 \
        bash
    

    必要なパッケージなどをインストール。

    command
    apt update
    apt install -y \
        python3 \
        python3-pip \
        libglib2.0-dev \
        git
    pip3 install \
        sphinx==5.3.0 \
        sphinx_rtd_theme==1.1.1 \
        ninja==1.11.1.1
    

    ターゲットを指定してconfigure実行。

    ./configure \
        --target-list=aarch64-softmmu \
        --enable-slirp
    

    --target指定しないと全アーキテクチャがフルビルドされる。
    ビルド実行。

    command
    make -j8
    

    -jは指定しないと並列処理されないのでかなり時間がかかる。
    数字はマシンのCPU数に合わせる。

    ビルドが成功すると./build/qemu-system-aarch64が生成される。
    コンテナに入ってビルドした場合は抜ける。

    command
    exit
    
  4. qemu-system-aarch64の配置
    すでにqemu-system-aarch64がある場合は移動させておく。

    command
    sudo mv "$(which qemu-system-aarch64)" \
        "$(dirname "$(which qemu-system-aarch64)")/qemu-system-aarch64.old"
    
    command
    sudo cp ./build/qemu-system-aarch64 /usr/local/bin
    sudo chmod +x /usr/local/bin/qemu-system-aarch64
    
  5. 対応マシンの確認
    virt-9.0以上があることを確認する。

    qemu-system-aarch64 -machine help
    
    ・・・
    virt                 QEMU 9.0 ARM Virtual Machine (alias of virt-9.0)
    virt-9.0             QEMU 9.0 ARM Virtual Machine
    ・・・
    
version `SLIRP_4.7' not foundエラーが発生する場合

gitで取得したソースからコンテナでビルドした場合などで、環境によってはqemu-system-aarch64実行時に以下のようなエラーが出る場合がある。

qemu-system-aarch64: /lib/x86_64-linux-gnu/libslirp.so.0: version `SLIRP_4.7' not found (required by qemu-system-aarch64)

libslirpの特定バージョン(上記は4.7.0)をビルドしてインストールする必要がある。
コンテナ内でビルドしてもインストールがうまくいかないので、下記ページの通りホスト環境で各種パッケージを入れてビルドする。

command
wget https://gitlab.freedesktop.org/slirp/libslirp/-/archive/v4.7.0/libslirp-v4.7.0.tar.bz2
tar jxvf ./libslirp-v4.7.0.tar.bz2
cd ./libslirp-v4.7.0
mkdir ./build
cd ./build
sudo apt update
sudo apt install -y meson libglib2.0-dev
meson setup --prefix=/usr --buildtype=release ..
ninja
ninja install

QEMU向けArm64版Ubuntuの準備

以下参考。

  1. イメージのダウンロード

    command
    wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img
    

    ※このimgファイルはインストーラーの役割だけでなくそのまま仮想マシンの使用ディスクとして消費されていくので、複数環境を作る場合ややり直しをしたい場合はオリジナルを複製しておく。

  2. EFIボリューム作成

    command
    wget https://releases.linaro.org/components/kernel/uefi-linaro/latest/release/qemu64/QEMU_EFI.fd
    dd if=/dev/zero of=flash0.img bs=1M count=64
    dd if=QEMU_EFI.fd of=flash0.img conv=notrunc
    dd if=/dev/zero of=flash1.img bs=1M count=64
    
  3. 設定ファイルの準備
    ユーザー名はデフォルトのubuntu
    パスワードはubuntu

    command
    sudo apt install -y cloud-image-utils
    echo "#cloud-config
    password: ubuntu
    chpasswd: { expire: False }
    ssh_pwauth: True" >user-data
    cloud-localds --disk-format qcow2 ./cloud.img ./user-data
    
  4. 仮想マシン用ディスクのリサイズ
    適当に大きめのサイズにしておく。

    command
    qemu-img resize ./jammy-server-cloudimg-arm64.img 40G
    

    即時でサイズが変わるわけではなく可変で使用した分だけ増える。後から再拡張することも可能。

QEMU向けArm64版Ubuntuの起動

以下のようなスクリプトファイルを作成する。SSHはローカルの22222にマッピング。
-machinevirt-8.2でも良いかもしれないが未確認。

run.sh
#!/bin/bash
set -e

qemu-system-aarch64 \
    -m 4096 \
    -smp 8 \
    -cpu cortex-a710 \
    -machine virt-9.0 \
    -nographic \
    -drive file=flash0.img,format=raw,if=pflash \
    -drive file=flash1.img,format=raw,if=pflash \
    -drive if=none,file=jammy-server-cloudimg-arm64.img,id=hd0 \
    -device virtio-blk-device,drive=hd0 \
    -drive if=none,id=cloud,file=cloud.img \
    -device virtio-blk-device,drive=cloud \
    -device virtio-net-device,netdev=user0 \
    -netdev user,id=user0,hostfwd=tcp::22222-:22

起動

command
bash ./run.sh

※ここから先は処理が重く・遅くなるので注意。

初回起動時は以下のようなログが出てからSSHでログインできるようになる。

[  OK  ] Finished Apply the settings specified in cloud-config.
         Starting Execute cloud user/final scripts...
[  398.467441] cloud-init[1421]: Cloud-init v. 23.4.4-0ubuntu0~22.04.1 running 'modules:final' at ***, ** *** **** **:**:** +****. Up 397.37 seconds.
ci-info: no authorized SSH keys fingerprints found for user ubuntu.
<14>*** ** **:**:** cloud-init: #############################################################
<14>*** ** **:**:** cloud-init: -----BEGIN SSH HOST KEY FINGERPRINTS-----
<14>*** ** **:**:** cloud-init: 1024 SHA256:*** root@ubuntu (DSA)
<14>*** ** **:**:** cloud-init: 256 SHA256:*** root@ubuntu (ECDSA)
<14>*** ** **:**:** cloud-init: 256 SHA256:*** root@ubuntu (ED25519)
<14>*** ** **:**:** cloud-init: 3072 SHA256:*** root@ubuntu (RSA)
<14>*** ** **:**:** cloud-init: -----END SSH HOST KEY FINGERPRINTS-----
<14>*** ** **:**:** cloud-init: #############################################################
-----BEGIN SSH HOST KEY KEYS-----
***
-----END SSH HOST KEY KEYS-----
[  403.145587] cloud-init[1421]: Cloud-init v. 23.4.4-0ubuntu0~22.04.1 finished at ***, ** *** **** **:**:** +****. Datasource DataSourceNoCloud [seed=/dev/vda][dsmode=net].  Up 402.97 seconds
[  OK  ] Finished Execute cloud user/final scripts.
[  OK  ] Reached target Cloud-init target.

このログを出力しているターミナルからもログインできるが、入力がおかしくなるので別途ターミナルやコマンドプロンプトを立ち上げてSSHログインして操作することを推奨。

Arm64版UbuntuにDockerをインストール

通常のDockerインストールと同じ。

command
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

ターミナルを一回終了して再度ログインし、dockerコマンドが正しく実行されるかを確認する。

docker ps

CONTAINER ID   IMAGE              COMMAND                  CREATED        STATUS                 PORTS 

AArch64向けCodeBuildとlocal-builds、ビルドスクリプトを取得

ここからが本題。以下を実行してイメージを取得する。

command
docker pull public.ecr.aws/codebuild/amazonlinux2-aarch64-standard:3.0-2024.03.21
docker pull public.ecr.aws/codebuild/local-builds:aarch64
wget https://raw.githubusercontent.com/aws/aws-codebuild-docker-images/master/local_builds/codebuild_build.sh

環境によっては完了までに2,3時間以上かかる場合もあるが、ネットワーク・ディスクエラーや空き容量の圧迫等が発生しない限りは成功するはずなので終わるまで待つ。

local-buildsの修正

起動時に以下と同じエラーが出る場合はlocal-buildsを修正する必要がある。

Dockerfile
FROM public.ecr.aws/codebuild/local-builds:aarch64

COPY "./docker-compose.yml" \
    "/LocalBuild/agent-resources/docker-compose.yml"
COPY "./docker-compose-mount-src-dir.yml" \
    "/LocalBuild/agent-resources/docker-compose-mount-src-dir.yml"
command
export LOCALBUILD_CUSTOM_IMAGE_TAG="my-local-builds:aarch64" && \
    export LOCALBUILD_WORK_DIR="/workdir" && \
    docker run \
        -it \
        -v ./:"${LOCALBUILD_WORK_DIR}" \
        --rm \
        --entrypoint=/bin/bash \
        public.ecr.aws/codebuild/local-builds:aarch64 \
            -c "curl \
                -L \
                -o /usr/bin/yq \
                https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64 2>/dev/null&& \
                chmod +x /usr/bin/yq && \
                cp /LocalBuild/agent-resources/{docker-compose.yml,docker-compose-mount-src-dir.yml} ${LOCALBUILD_WORK_DIR} && \
                for file in \"docker-compose.yml\" \"docker-compose-mount-src-dir.yml\"; do \
                    yq -i \
                        '.version=\"3\" | \
                        .services.build.environment[0] = \"NO_PROXY=agent:3000\" |
                        .services.build.environment[2] = \"CODEBUILD_AGENT_PORT=http://agent:3000\" |
                        del(.services.build.links)' \
                        ${LOCALBUILD_WORK_DIR}/\${file}; \
                done" && \
    docker build -t "${LOCALBUILD_CUSTOM_IMAGE_TAG}" . && \
    sed -i "s/public\.ecr\.aws\/codebuild\/local-builds:latest/${LOCALBUILD_CUSTOM_IMAGE_TAG}/g" ./codebuild_build.sh

CodeBuildのローカル実行

  1. ビルド対象とbuildspec.ymlの作成
    適当に作成。
    command
    mkdir -p ./src
    echo "Hello world" >./src/source.txt
    
    src以下にbuildspec.ymlを作成。
    ./src/buildspec.yml
    version: 0.2
    
    env:
      variables:
        aaaa: "bbbb"
    phases:
      build:
        commands:
          - echo "${aaaa}"
          - uname -m
          - cat ./source.txt
          - echo "Build Test" >./result.txt
    artifacts:
      files:
        - ./result.txt
    
  2. 実行
    command
    bash ./codebuild_build.sh \
        -i public.ecr.aws/codebuild/amazonlinux2-aarch64-standard:2.0-2024.03.21 \
        -s ./src \
        -a ./results \
        -l my-local-builds:aarch64
    
    -lを指定をしないと内側のagentが正しく起動しない。
  3. 実行結果
    Removing network agent-resources_default
    WARNING: Network agent-resources_default not found.
    Removing volume agent-resources_source_volume
    WARNING: Volume agent-resources_source_volume not found.
    Removing volume agent-resources_user_volume
    WARNING: Volume agent-resources_user_volume not found.
    Creating network "agent-resources_default" with the default driver
    Creating volume "agent-resources_source_volume" with local driver
    Creating volume "agent-resources_user_volume" with local driver
    Creating agent-resources_build_1 ... done
    Creating agent-resources_agent_1 ... done
    Attaching to agent-resources_build_1, agent-resources_agent_1
    agent_1  | [Container] ****/**/** **:**:** Waiting for agent ping
    agent_1  | [Container] ****/**/** **:**:** Waiting for DOWNLOAD_SOURCE
    agent_1  | [Container] ****/**/** **:**:** Phase is DOWNLOAD_SOURCE
    agent_1  | [Container] ****/**/** **:**:** CODEBUILD_SRC_DIR=/codebuild/output/***/src
    agent_1  | [Container] ****/**/** **:**:** YAML location is /codebuild/output/srcDownload/src/buildspec.yml
    agent_1  | [Container] ****/**/** **:**:** Processing environment variables
    agent_1  | [Container] ****/**/** **:**:** Moving to directory /codebuild/output/***/src
    agent_1  | [Container] ****/**/** **:**:** Registering with agent
    agent_1  | [Container] ****/**/** **:**:** Phases found in YAML: 1
    agent_1  | [Container] ****/**/** **:**:**  BUILD: 4 commands
    agent_1  | [Container] ****/**/** **:**:** Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent_1  | [Container] ****/**/** **:**:** Entering phase INSTALL
    agent_1  | [Container] ****/**/** **:**:** Phase complete: INSTALL State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent_1  | [Container] ****/**/** **:**:** Entering phase PRE_BUILD
    agent_1  | [Container] ****/**/** **:**:** Phase complete: PRE_BUILD State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent_1  | [Container] ****/**/** **:**:** Entering phase BUILD
    agent_1  | [Container] ****/**/** **:**:** Running command echo "${aaaa}"
    agent_1  | bbbb
    agent_1  |
    agent_1  | [Container] ****/**/** **:**:** Running command uname -m
    agent_1  | aarch64
    agent_1  |
    agent_1  | [Container] ****/**/** **:**:** Running command cat source.txt
    agent_1  | Hello world
    agent_1  |
    agent_1  | [Container] ****/**/** **:**:** Running command echo "Build Test" >result.txt
    agent_1  |
    agent_1  | [Container] ****/**/** **:**:** Phase complete: BUILD State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent_1  | [Container] ****/**/** **:**:** Entering phase POST_BUILD
    agent_1  | [Container] ****/**/** **:**:** Phase complete: POST_BUILD State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent_1  | [Container] ****/**/** **:**:** Expanding base directory path: .
    agent_1  | [Container] ****/**/** **:**:** Assembling file list
    agent_1  | [Container] ****/**/** **:**:** Expanding .
    agent_1  | [Container] ****/**/** **:**:** Expanding file paths for base directory .
    agent_1  | [Container] ****/**/** **:**:** Assembling file list
    agent_1  | [Container] ****/**/** **:**:** Expanding result.txt
    agent_1  | [Container] ****/**/** **:**:** Found 1 file(s)
    agent_1  | [Container] ****/**/** **:**:** Preparing to copy secondary artifacts
    agent_1  | [Container] ****/**/** **:**:** No secondary artifacts defined in buildspec
    agent_1  | [Container] ****/**/** **:**:** Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
    agent_1  | [Container] ****/**/** **:**:** Phase context status code:  Message:
    agent-resources_build_1 exited with code 0
    Aborting on container exit...
    
    x64版と同様に動作し、./results/artifacts.zipが生成されて中にアーティファクトがあることが確認できる。
    cd ./results
    sudo unzip ./artifacts.zip
    cat ./result.txt
    
    Build Test
    

補足

virt-7.2でのエラー

-machinevirt-7.2を指定してUbuntuを起動した場合はCodeBuild実行時に以下のようなエラーとなる。

Fatal glibc error: This version of Amazon Linux requires a newer ARM64 processor
                   compliant with at least ARM architecture 8.2-a with Cryptographic
                   extensions. On EC2 this is Graviton 2 or later.

Windows版QEMUでのArm64版Ubuntu起動スクリプト

run.shと同じようなバッチファイルで起動できる。
ただし各種imgファイルをWindowsプラットフォームで作成する方法は未確認なので、そこだけはWSLやDocker、Ubuntu等で作成すれば良いと思われる。

run.bat
qemu-system-aarch64 ^
    -m 4096 ^
    -smp 8 ^
    -cpu cortex-a710 ^
    -machine virt-9.0 ^
    -nographic ^
    -drive file=flash0.img,format=raw,if=pflash ^
    -drive file=flash1.img,format=raw,if=pflash ^
    -drive if=none,file=jammy-server-cloudimg-arm64.img,id=hd0 ^
    -device virtio-blk-device,drive=hd0 ^
    -drive if=none,id=cloud,file=cloud.img ^
    -device virtio-blk-device,drive=cloud ^
    -device virtio-net-device,netdev=user0 ^
    -netdev user,id=user0,hostfwd=tcp::22222-:22

Podmanについて

Dockerの代わりにPodmanで--arch aarch64を指定すればできるのでは?というコメントも見受けられたが、内容を把握していないので今回は検証対象外としている。

結論

  • 動きはするが、とにかく重い・遅いし時間がもったいないのであくまで暫定的な検証環境として扱ったほうが良い。
  • Armマシンを立ち上げずにqemu-user-staticを使用する方法のほうが遅くならないし簡単。
  • どうしてもArmマシンで動かしたいなら、おとなしく同アーキテクチャのラズベリーパイやM1/M2 Mac等を購入してそちらで動かすことを推奨。

Discussion