基于 Apple M 芯片搭建异构测试环境

因为需要测试 x86 相关的特性,手头只有 MacBook Pro M1,所以需要在没有 x86 服务器资源的情况下在 Apple M 系统中构建 x86 环境来跑测试。

基本原理

交叉编译是在一个平台上生成另一个平台上的可执行代码。将交叉编译的可执行代码打包成镜像就是想要的目标架构镜像了,但容器的交叉编译构建镜像依赖 QEMU 的虚拟化技术。

使用 QEMU 虚拟化技术可以虚拟化出相关的硬件设备,常用来在单台高性能物理服务器上虚拟化出多台虚拟机场景,例如 VirtalBox、Vmware、KVM 等虚拟化工具。对于多架构镜像也是可以选择这种方式创建出不同架构平台的虚拟机,然后使用基于目标架构机器的镜像构建结果组合方式来发布多架构镜像;但是这种方式除了管理复杂外,虚拟化本身也会有较多的资源损耗浪费。

目前技术上已经支持在宿主机系统内核支持虚拟化的情况下,可以直接基于目标架构指令的方式来构建镜像。该方式不需要事先虚拟化出各种平台的硬件环境,也不需要安装各种操作系统环境等复杂的操作,同时也不会有大量的虚拟化本身的资源损耗,通过内核支持在编译时直接编译成对应架构的可执行文件,然后构建出目标架构的容器镜像。这种方式的优点是构建便捷,资源利用率高;缺点是对操作系统内核版本有一定的要求。

环境搭建

本章节内容主要来自:多架构镜像的-dockerfile · Docker 文档

多架构 Dockerfile

在构建多架构镜像时,想要构建出不同的架构平台镜像,就需要在构建时获取到对应架构的基础镜像。在使用 docker buildx build 时通过 --platform 参数指定想要构建的目标平台,在 Dockerfile 中通过 --platform=$TARGETPLATFORM 来获取对应参数信息,下载对应架构的基础镜像。

在上文的 Dockerfile 基础上,修改后的 Dockerfile 内容如下:

FROM  --platform=$TARGETPLATFORM golang AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
WORKDIR /gobuild
COPY  get-cpu-os.go  .
RUN go build get-cpu-os.go

FROM --platform=$TARGETPLATFORM golang
ARG TARGETPLATFORM
ARG BUILDPLATFORM
WORKDIR /gorun
COPY --from=builder /gobuild/get-cpu-os .
CMD ["./get-cpu-os"]

Buildx 在构建时支持向 Dockerfile 传入的参数变量如下:

  • TARGETPLATFORM 构建镜像的目标平台,例如 linux/amd64, linux/arm/v7, windows/amd64。
  • TARGETOS TARGETPLATFORM 的 OS 类型,例如 linux, windows
  • TARGETARCH TARGETPLATFORM 的架构类型,例如 amd64, arm
  • TARGETVARIANT TARGETPLATFORM 的变种,该变量可能为空,例如 v7
  • BUILDPLATFORM 构建镜像主机平台,例如 linux/amd64
  • BUILDOS BUILDPLATFORM 的 OS 类型,例如 linux
  • BUILDARCH BUILDPLATFORM 的架构类型,例如 amd64
  • BUILDVARIANT BUILDPLATFORM 的变种,该变量可能为空,例如 v7

构建多架构镜像

按照 buildx 的依赖准备高版本的 docker 和 Linux 内核后,在构建多架构镜像前需先创建一个 builder 构建器实例。

例如通过 docker buildx create 命令创建一个 mybuilder 的构建器实例

docker buildx create --name=mybuilder --driver docker-container  --platform linux/amd64,linux/386,linux/riscv64,linux/arm/v7,linux/arm/v6,linux/s390x,linux/ppc64le,linux/arm64

该构建的参数:

  • --name=mybuilder: 给构建器指定一个名称,便于人来查看和管理
  • --driver docker-container: 指定构建器的驱动类型
  • --platform linux/amd64: 指定该构建器支持构建的架构类型

在构建器创建成功后,当前 docker 节点上 builder 实例列表可以通过 docker buildx ls 来查看

docker buildx ls
NAME/NODE    DRIVER/ENDPOINT             STATUS   PLATFORMS
mybuilder    docker-container                     
  mybuilder0 unix:///var/run/docker.sock inactive linux/amd64*, linux/386*, linux/riscv64*, linux/arm/v7*, linux/arm/v6*, linux/s390x*, linux/ppc64le*, linux/arm64*

在镜像构建时如果没有通过 --builder 指定明确的构建器,那么 buildx 会使用默认的构建器。

默认构建器 builder 名称后以 * 标记,可以通过 docker buildx use 来切换默认的构建器

docker buildx use mybuilder
docker buildx ls
NAME/NODE    DRIVER/ENDPOINT             STATUS   PLATFORMS
mybuilder *  docker-container                     
  mybuilder0 unix:///var/run/docker.sock inactive linux/amd64*, linux/386*, linux/riscv64*, linux/arm/v7*, linux/arm/v6*, linux/s390x*, linux/ppc64le*, linux/arm64*
default      docker                               
  default    default                     running  linux/amd64,  linux/386, linux/arm/v7, linux/arm/v6, linux/ppc64le

通过 docker buildx inspect 来单独查看某一个指定的 builder 构建器的详细信息

docker buildx inspect mybuilder
Name:   mybuilder
Driver: docker-container

Nodes:
Name:      mybuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    inactive
Platforms: linux/amd64*, linux/386*, linux/riscv64*, linux/arm/v7*, linux/arm/v6*, linux/s390x*, linux/ppc64le*, linux/arm64*

构建器在创建成功后状态是 Status: inactive, 需要通过 docker buildx inspect 添加 --bootstrap 参数来启动构建器后才能正常使用该构建器构建镜像。

启动 mybuilder 构建器

docker buildx inspect --bootstrap --builder mybuilder
[+] Building 17.6s (1/1) FINISHED                                             
 => [internal] booting buildkit                                        17.6s
 => => pulling image moby/buildkit:buildx-stable-1                     12.4s
 => => creating container buildx_buildkit_mybuilder0                   5.1s
Name:   mybuilder
Driver: docker-container

Nodes:
Name:      mybuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/amd64*, linux/386*, linux/riscv64*, linux/arm/v7*, linux/arm/v6*, linux/s390x*, linux/ppc64le*, linux/arm64*, linux/amd64/v2, linux/amd64/v3, linux/mips64le, linux/mips64

在一切准备就绪后,就可以通过 docker buildx build 来构建多架构镜像。

docker buildx build --builder mybuilder \
-t zhaowenyu/get-cpu-os:v4 \
--platform linux/amd64,linux/386,linux/arm/v7,linux/arm/v6,linux/s390x,linux/ppc64le,linux/arm64 \
-f Dockerfile-multi \
--push .

在构建多架构镜像时需要注意的参数说明

  • --builder mybuilder: 是指定使用哪个构建器,mybuilder 就是在前置步骤中创建出来的构建器。如果使用了 docker buildx use mybuilder 切换了当前默认构建器,那么这个 --builder mybuilder 也可以省略。
  • -t zhaowenyu/get-cpu-os:v4: 和 docker build 一样给构建的镜像指定的 TAG,这也是多架构镜像的核心入口,他是个 manifests list,即在一个 TAG 中可描述多个平台的镜像相关信息。
  • --platform linux/amd64,linux/386,linux/arm/v7,linux/arm/v6,linux/s390x,linux/ppc64le,linux/arm64: 指定构建的镜像目标架构列表列表,目前只有 docker-container 和 kubernetes 驱动支持。
  • -f Dockerfile-multi: 和 docker build 指定 Dockerfile 一样,这里只是为了便于和普通的 Dockerfile 区分添加了后缀,如果在上下文目录中只有 Dockerfile 文件,该参数也可以省略。
  • --push: 这里的 --push 是 --output=type=registry 的简写,指定了输出的类型和位置,registry 就是输出到远程镜像仓库,那就是 push 的意思了。
  • .: 注意,这里还有一个点,它和 docker build 的含义一样,是指 上下文 的概念,将代码或文件发送到 builder 构建器时的相对初始位置。

one more thing

其实还有一个更加取巧的办法,就是直接指定 base image 的架构,直接构建就可以:

FROM --platform=linux/amd64 ubuntu:latest

RUN apt update && apt install openssh-server sudo git vim make curl -y

# install node 18.x(LTS)
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs
# install yarn
RUN npm install --global yarn

RUN echo "export PS1='[\[\e[32m\]\u@\[\e[36m\]\h \[\e[35m\]\W\[\e[37m\]:\[\e[31m\]\#\[\e[0m\]]\$ \n>> '" >> /root/.bashrc

# add init git ssh config tools
#RUN echo '#!/bin/bash mkdir -p /root/.ssh && cp /root/ssh-host/id_rsa_ob /root/.ssh/ && cp /root/ssh-host/id_rsa_ob.pub /root/.ssh/ && cp /root/ssh-host/config /root/.ssh/' > /root/init-git-ssh.#sh
#RUN chmod u+x /root/init-git-ssh.sh

RUN mkdir -p /root/.ssh && touch /root/.ssh/authorized_keys && echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDIjZHyqhN/+9WrXd2q3hrAz9bFnlzj3cwBQxhWz1vjMsDi+IAGtDj1iWBR6umg1VEVvmGcMMNl5wD3v71+/E14RNAmtWo8MBGHvl3buM1jUGWA52T6fc93W32yW88/6FpPV/exYLj5OfGXVLwtMFAeAbp3yhFMdYgT+jPts9HDTuxRbo3ltpC3viLEoziZirphXVpePiafXyIpahMQ6m2HqEJMiXfv8GAgsliwZ73cfbN+593qINV2uEYO60WiMjFFPLmEAysFHNgf1hw0QjBQMsdg7e6c4WlR1WrPotW+K3ChqXyw73U4+Cs0lOkbC1HE4aRyg1pEgF0bgpFXKiwLhMb/fnI0TeEf3PPrg4PDsaRmOWyX3xGy3PfMiYevVVNmrNtuL+LJg1y+lfJOrtkKIkvtEoEeS2qv7wJFug+5HUKQVP/JvREKU9W+vgNmX4Jite0nYq9jZNwlT/Tli1Nys06sWTaOcc6lCMVno/XAo2Tpn8bl35NQOjYtGID+UnU= edony@B-1V73X3RW-0109.local" >> /root/.ssh/authorized_keys

RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config

RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config

RUN ssh-keygen -A

RUN echo 'root:test' | chpasswd

RUN service ssh start

EXPOSE 22

RUN wget https://go.dev/dl/go1.21.6.linux-amd64.tar.gz

RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.6.linux-amd64.tar.gz

RUN echo "PATH=$PATH:/usr/local/go/bin" >> /root/.bashrc

CMD ["/usr/sbin/sshd","-D"]

测试一下:

docker build -t edony/ubuntu-dev:v1.5 -f ubuntu-dev-x86.dockerfile .

docker network create dev-net --subnet=10.11.0.0/16

docker run --rm -d -p 2023:22 --name ubuntu-dev --platform linux/amd64 --net dev-net --ip 10.11.0.11 -v /Users/edony/code:/root/code edony/ubuntu-dev:v1.5

References

  1. 多架构镜像 · Docker 文档