背景
过去,我一直在使用 x86_64 架构的 MacBook Pro 进行开发,服务器环境则是 x86_64 架构的 CentOS,因此从未担心过多架构的 Docker 镜像构建问题。
然而,公司最近更新了一批 M1 MacBook Pro。
在 x86 系统上运行为 ARM/M1 架构构建的镜像时,可能会遇到以下错误:
|
|
这意味着容器中的二进制文件并非为当前 CPU 架构构建的。
那么,如何构建一个多架构的 Docker 镜像呢?
本文将介绍两种方法:通过 manifest
和 buildx
构建多架构镜像。
方法一: 使用 manifest
1.1 什么是 Docker manifest
Docker manifest 是镜像的元数据列表,包含镜像的大小、层数(layers)、digest、操作系统、CPU 架构等信息。通过 manifest,我们可以为不同 CPU 架构提供兼容的镜像,实现「build once, run anywhere」。
1.2 创建 manifest
在创建 manifest 前,先构建出所需的镜像。以下示例中,我们从 Docker Hub 拉取两个不同架构的镜像(alpine:3.19
),并将其分别标记为 arm-v6
和 amd64
:
镜像名称 | CPU 架构 |
---|---|
harbor.baijiayun.com/proxy/alpine3.19 | arm-v6 |
harbor.baijiayun.com/proxy/alpine3.19 | amd64 |
使用 manifest 前,需确保目标镜像已推送至仓库,manifest 命令仅适用于仓库中的镜像。
在 Harbor 中可以看到两个不同架构的镜像。接下来,通过以下命令创建 manifest:
|
|
推送完成后,Harbor 中将显示一个包含不同架构的镜像。此 manifest 镜像支持自动适配系统,Docker Engine 会根据当前系统选择合适的镜像。可通过 docker manifest inspect
查看详细的元数据:
|
|
打开 Harbor 找到对应的镜像,切到 Artifacts
页面,查看的结果如下:
1.3 manifest 的优势
- 统一镜像访问:跨平台兼容性更好,支持多架构和多操作系统。
- 简化镜像管理:统一推送和拉取镜像。
- 高效分发:自动匹配系统的正确镜像。
方法二:使用 buildx
在 19.03 之后的 Docker 版本中,为改善构建速度,Docker image 构建引擎被重写,这也是后来熟知的 BuildKit。BuildKit 是 Docker 镜像构建引擎的一次彻底重写,它保留了之前的所有功能,同时增加了一些很棒的新特性,以生成更好的 OCI 镜像,例如:
- 可以为 COPY 指令添加
--chown
#34263 - 真正的并行化多阶段镜像构建,只在需要依赖项时才等待
我建议即使只构建单架构镜像,也要总是启用 Buildkit,因为它至少可以提高构建速度。
在后面的版本中,BuildKit 总是默认开启的。需要手动开启或关闭时,只需要设置环境变量即可,如:
|
|
谈到多架构构建,BuildKit 内置的 buildx 就可以胜任,使用方式也比较简单。
3.1 创建 container builder
默认情况下,Docker 只是开启了 BuildKit 特性,但是并不能直接构建 non-native 平台的镜像。想开启这个功能,就要使用 docker-container
driver,更多细节可参考官方文档。
使用 docker-container
driver 有以下优势:
- 能够指定 BuildKit 版本
- 能够构建 multi-arch 镜像
- 有更多的缓存功能,如 multi caches
创建 docker-container
driver 的方式也很简单,直接通过 buildx 命令,如下所示:
|
|
创建后,通过 docker buildx ls
可以查看创建的结果:
3.2 构建并推送镜像
创建了 docker-container driver 后,即可使用 –platform 指定需要构建的平台。构建命令如下:
|
|
3.3 小结
通过 manifest 的方式可以创建多架构的镜像,而 buildx 的方式更简单。不用先创建两个镜像,组装成 manifest 文件再推送 manifest,而是直接通过 buildx 指定 platform 参数,中间的过程全部都是由 buildx 自动完成。在实际使用中,更推荐使用 buildx 的方式。
4. 构建多架构 Go 服务实践
有了前面的基础后,构建多架构的 Go 服务乍一看好像比较简单:Go 在编译时提供了 GOOS 与 GOARCH 参数,构建的时候设置下这两个参数就好了。
可问题是,当使用 buildx 在外层构建时,内部的 Dockerfile 是如何知道要构建哪个目标的镜像呢?
在仔细查阅了官方文档后,得知到,当使用 buildx 构建时,buildx 会将一些信息通过 build_arg 的方式传递到 Dockerfile,以方便在 Dockerfile 中使用这些变量。
根据 文档 可知,以下 ARG 变量会被自动设置:
TARGETPLATFORM
- 构建的目标平台,如linux/amd64
,linux/arm/v7
,windows/amd64
.TARGETOS
- TARGETPLATFORM 对应的 OSTARGETARCH
- TARGETPLATFORM 对应的 CPU 架构TARGETVARIANT
- TARGETPLATFORM 对应的 variant,如指定 platform=linux/arm/v7 那么 arch 就是 arm variant 就是 v7BUILDPLATFORM
- 构建环境的平台BUILDOS
- BUILDPLATFORM 对应的 OSBUILDARCH
- BUILDPLATFORM 对应的架构BUILDVARIANT
- BUILDPLATFORM 对应的 variant
有了这些变量后,那么在 buildx 中指定 platform 参数后,就可以通知到 Dockerfile 了。
4.1 编译目标二进制文件
|
|
根据上一节的变量自动注入的知识可以得到,当执行以下命令时:
|
|
这个 Dockerfile 会被多次构建,每次构建时,执行的 go build 参数又不同。在这个 case 中,会分别执行
|
|
5. CI 中如何使用呢
将选择 CPU 架构的地方,通过下拉框的形式交给用户选择,再将得到的变量传给 shell 中执行对应的结果即可。