最新在信创项目中,经常需要构建支持amd64和arm64架构的镜像,而有的场景在同一个 Kubernetes 集群中的节点是混合架构的,也就是说,其中某些节点的 CPU 架构是 x86 的,而另一些节点是 ARM 的。为了让我们的镜像在这样的环境下运行,一种最简单的做法是根据节点类型为其打上相应的标签,然后针对不同的架构构建不同的镜像,比如 demo:v1-amd64
和 demo:v1-arm64
,然后还需要写两套 YAML:一套使用 demo:v1-amd64
镜像,并通过 nodeSelector
选择 x86 的节点,另一套使用 demo:v1-arm64
镜像,并通过 nodeSelector
选择 ARM 的节点。很显然,这种做法不仅非常繁琐,而且管理起来也相当麻烦,如果集群中还有其他架构的节点,那么维护成本将成倍增加。
你可能知道,每个 Docker 镜像都是通过一个 manifest 来描述的,manifest 中包含了这个镜像的基本信息,包括它的 mediaType、大小、摘要以及每一层的分层信息等。可以使用 docker manifest inspect
查看某个镜像的 manifest 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ docker manifest inspect aneasystone/hello-actuator:v1 { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 3061, "digest": "sha256:d6d5f18d524ce43346098c5d5775de4572773146ce9c0c65485d60b8755c0014" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 2811478, "digest": "sha256:5843afab387455b37944e709ee8c78d7520df80f8d01cf7f861aae63beeddb6b" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 928436, "digest": "sha256:53c9466125e464fed5626bde7b7a0f91aab09905f0a07e9ad4e930ae72e0fc63" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 186798299, "digest": "sha256:d8d715783b80cab158f5bf9726bcada5265c1624b64ca2bb46f42f94998d4662" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 19609795, "digest": "sha256:112ce4ba7a4e8c2b5bcf3f898ae40a61b416101eba468397bb426186ee435281" } ] } |
可以加上 --verbose
查看更详细的信息,包括该 manifest 引用的镜像标签和架构信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker manifest inspect --verbose aneasystone/hello-actuator:v1 { "Ref": "docker.io/aneasystone/hello-actuator:v1", "Descriptor": { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:f16a1fcd331a6d196574a0c0721688360bf53906ce0569bda529ba09335316a2", "size": 1163, "platform": { "architecture": "amd64", "os": "linux" } }, "SchemaV2Manifest": { ... } } |
我们一般不会直接使用 manifest,而是通过标签来关联它,方便人们使用。从上面的输出结果可以看出,该 manifest 通过 docker.io/aneasystone/hello-actuator:v1
这个镜像标签来关联,支持的平台是 linux/amd64
,该镜像有四个分层,另外注意这里的 mediaType
字段,它的值是 application/vnd.docker.distribution.manifest.v2+json
,表示这是 Docker 镜像格式(如果是 application/vnd.oci.image.manifest.v1+json
表示 OCI 镜像)。
可以看出这个镜像标签只关联了一个 manifest ,而一个 manifest 只对应一种架构;如果同一个镜像标签能关联多个 manifest ,不同的 manifest 对应不同的架构,那么当我们通过这个镜像标签启动容器时,容器引擎就可以自动根据当前系统的架构找到对应的 manifest 并下载对应的镜像。实际上这就是 多架构镜像( multi-arch images ) 的基本原理,我们把这里的多个 manifest 合称为 manifest list( 在 OCI 规范中被称为 image index ),镜像标签不仅可以关联 manifest,也可以关联 manifest list。
可以使用 docker manifest inspect
查看某个多架构镜像的 manifest list 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
$ docker manifest inspect alpine:3.17 { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c", "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:ecc4c9eff5b0c4de6be6b4b90b5ab2c2c1558374852c2f5854d66f76514231bf", "platform": { "architecture": "arm", "os": "linux", "variant": "v6" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:4c679bd1e6b6516faf8466986fc2a9f52496e61cada7c29ec746621a954a80ac", "platform": { "architecture": "arm", "os": "linux", "variant": "v7" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:af06af3514c44a964d3b905b498cf6493db8f1cde7c10e078213a89c87308ba0", "platform": { "architecture": "arm64", "os": "linux", } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:af6a986619d570c975f9a85b463f4aa866da44c70427e1ead1fd1efdf6150d38", "platform": { "architecture": "386", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:a7a53c2331d0c5fedeaaba8d716eb2b06f7a9c8d780407d487fd0fbc1244f7e6", "platform": { "architecture": "ppc64le", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:07afab708df2326e8503aff2f860584f2bfe7a95aee839c8806897e808508e12", "platform": { "architecture": "s390x", "os": "linux" } } ] } |
这里的 alpine:3.17
就是一个多架构镜像,从输出结果可以看到 mediaType
是 application/vnd.docker.distribution.manifest.list.v2+json
,说明这个镜像标签关联的是一个 manifest list,它包含了多个 manifest,支持 amd64、arm/v6、arm/v7、arm64、i386、ppc64le、s390x 多个架构。我们也可以直接在 Docker Hub 上看到这些信息:
很显然,在我们这个混合架构的 Kubernetes 集群中,这个镜像是可以直接运行的。我们也可以将我们的应用构建成这样的多架构镜像,那么在这个 Kubernetes 集群中就可以自由地运行我们自己的应用了,这种方法比上面那种为每个架构构建一个镜像的方法要优雅得多。
那么,我们要如何构建这样的多架构镜像呢?一般来说,如果你使用 Docker 作为你的构建工具,通常有两种方法:docker manifest
和 docker buildx
。
使用 docker manifest
创建多架构镜像
docker build
是最常用的镜像构建命令,首先,我们创建一个 Dockerfile
文件,内容如下:
1 2 |
FROM alpine:3.17 CMD ["echo", "Hello"] |
然后使用 docker build
构建镜像:
1 |
$ docker build -f Dockerfile -t aneasystone/demo:v1 . |
这样一个简单的镜像就构建好了,镜像能正常运行。不过这样构建的镜像有一个问题,Docker Engine 是根据当前我们的系统自动拉取基础镜像的,我的系统是 x86 的,所以拉取的 alpine:3.17 镜像架构是 linux/amd64
的:
1 2 3 |
$ docker image inspect alpine:3.17 | grep Architecture "Architecture": "amd64", |
如果要构建其他架构的镜像,可以有三种办法。第一种是最原始的方法,Docker 官方为每种 不同的架构创建了不同的独立账号,比如下面是一些常用的账号:
- ARMv6 32-bit (arm32v6): https://hub.docker.com/u/arm32v6/
- ARMv7 32-bit (arm32v7): https://hub.docker.com/u/arm32v7/
- ARMv8 64-bit (arm64v8): https://hub.docker.com/u/arm64v8/
- Linux x86-64 (amd64): https://hub.docker.com/u/amd64/
- Windows x86-64 (windows-amd64): https://hub.docker.com/u/winamd64/
所以我们就可以通过 amd64/alpine
和 arm64v8/alpine
来拉取相应架构的镜像,我们对 Dockerfile
文件稍微修改一下:
1 2 3 |
ARG ARCH=amd64 FROM ${ARCH}/alpine:3.17 CMD ["echo", "Hello"] |
然后使用 --build-arg
参数来构建不同架构的镜像:
1 2 |
$ docker build --build-arg ARCH=amd64 -f Dockerfile-arg -t aneasystone/demo:v1-amd64 . $ docker build --build-arg ARCH=arm64v8 -f Dockerfile-arg -t aneasystone/demo:v1-arm64 . |
不过从 2017 年 9 月开始,一个镜像可以支持多个架构了,这种方法就渐渐不用了。第二种办法就是直接使用 alpine:3.17
这个基础镜像,通过 FROM
指令的 --platform
参数,让 Docker Engine 自动拉取特定架构的镜像。我们新建两个文件 Dockerfile-amd64
和 Dockerfile-arm64
,Dockerfile-amd64
文件内容如下:
1 2 |
FROM --platform=linux/amd64 alpine:3.17 CMD ["echo", "Hello"] |
Dockerfile-arm64
文件内容如下:
1 2 |
FROM --platform=linux/arm64 alpine:3.17 CMD ["echo", "Hello"] |
然后使用 docker build
再次构建镜像即可:
1 2 |
$ docker build --pull -f Dockerfile-amd64 -t aneasystone/demo:v1-amd64 . $ docker build --pull -f Dockerfile-arm64 -t aneasystone/demo:v1-arm64 . |
注意这里的 --pull
参数,强制要求 Docker Engine 拉取基础镜像,要不然第二次构建时会使用第一次的缓存,这样基础镜像就不对了。
第三种方法不用修改 Dockerfile
文件,因为 docker build
也支持 --platform
参数,我们只需要像下面这样构建镜像即可:
1 2 |
$ docker build --pull --platform=linux/amd64 -f Dockerfile -t aneasystone/demo:v1-amd64 . $ docker build --pull --platform=linux/arm64 -f Dockerfile -t aneasystone/demo:v1-arm64 . |
在执行
docker build
命令时,可能会遇到下面这样的报错信息:
1 2 3 4 5 6 7 8 9 10 11 |
$ docker build -f Dockerfile-arm64 -t aneasystone/demo:v1-arm64 . [+] Building 1.2s (3/3) FINISHED => [internal] load build definition from > Dockerfile-arm64 0.0s => => transferring dockerfile: > 37B 0.0s => [internal] load .> dockerignore 0.0s => => transferring context: > 2B 0.0s => ERROR [internal] load metadata for docker.io/library/alpine:3.> 17 1.1s ------ > [internal] load metadata for docker.io/library/alpine:3.17: ------ failed to solve with frontend dockerfile.v0: failed to create LLB > definition: unexpected status code [manifests 3.17]: 403 Forbidden |
据 这里 的信息,修改 Docker Daemon 的配置文件,将
buildkit
设置为 false 即可:
1 2 3 |
"features": { "buildkit": false }, |
构建完不同架构的镜像后,我们就可以使用 docker manifest 命令创建 manifest list,生成自己的多架构镜像了。由于目前创建 manifest list 必须引用远程仓库中的镜像,所以在这之前,我们需要先将刚刚生成的两个镜像推送到镜像仓库中:
1 2 |
$ docker push aneasystone/demo:v1-amd64 $ docker push aneasystone/demo:v1-arm64 |
然后使用
docker manifest create
创建一个 manifest list,包含我们的两个镜像:
1 2 3 |
$ docker manifest create aneasystone/demo:v1 \ --amend aneasystone/demo:v1-amd64 \ --amend aneasystone/demo:v1-arm64 |
最后将该 manifest list 也推送到镜像仓库中就大功告成了:
1 |
$ docker manifest push aneasystone/demo:v1 |
如果推送时报错说没有manifest,则直接使用下面命令创建即可:
1 |
docker manifest create ycr.yyiuap.com/os/openeuler:20.03-sp2 ycr.yyiuap.com/os/openeuler:20.03-sp2-arm64 ycr.yyiuap.com/os/openeuler:20.03-sp2-amd64 |
可以使用 docker manifest inspect
查看这个镜像的 manifest list 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$ docker manifest inspect aneasystone/demo:v1 { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:170c4a5295f928a248dc58ce500fdb5a51e46f17866369fdcf4cbab9f7e4a1ab", "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 528, "digest": "sha256:3bb9c02263447e63c193c1196d92a25a1a7171fdacf6a29156f01c56989cf88b", "platform": { "architecture": "arm64", "os": "linux", "variant": "v8" } } ] } |
也可以在 Docker Hub 上看到这个镜像的架构信息:
使用 docker buildx
创建多架构镜像
从上一节可以看出,使用 docker manifest
来构建多架构镜像的步骤大致分为以下四步:
- 使用
docker build
依次构建每个架构的镜像; - 使用
docker push
将镜像推送到镜像仓库; - 使用
docker manifest create
创建一个 manifest list,包含上面的每个镜像; - 使用
docker manifest push
将 manifest list 推送到镜像仓库;
每次构建多架构镜像都要经历这么多步骤还是非常麻烦的,这一节将介绍一种更方便的方式,使用 docker buildx
来创建多架构镜像。
buildx 是一款 Docker CLI 插件,它对 Moby BuildKit 的构建功能进行了大量的扩展,同时在使用体验上还保持和 docker build
一样,用户可以很快上手。如果你的系统是 Windows 或 MacOS,buildx
已经内置在 Docker Desktop 里了,无需额外安装;如果你的系统是 Linux,可以使用 DEB 或 RPM 包的形式安装,也可以手工安装,具体安装步骤参考 官方文档。
使用 docker buildx
创建多架构镜像只需简单一行命令即可:
1 |
$ docker buildx build --platform=linux/amd64,linux/arm64 -t aneasystone/demo:v2 . |
不过第一次执行这行命令时会报下面这样的错:ERROR: multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. “docker buildx create –use”)
这是因为 buildx
默认使用的 构建器( builder ) 驱动是 docker driver
,它不支持同时构建多个 platform 的镜像,我们可以使用 docker buildx create
创建其他驱动的构建器( 关于 buildx
的四种驱动以及它们支持的特性可以 参考这里 ):
1 2 3 4 5 |
docker buildx create \ --name multi-platform \ --use --platform \ linux/amd64,linux/arm64 \ --driver docker-container |
构建:
1 2 3 4 5 |
DOCKER_BUILDKIT=1 docker buildx build \ --no-cache \ --output=type=local,name=openeuler,dest=./resources \ --platform=linux/amd64,linux/arm64 \ -f build/Dockerfile.os.openeuler . |
报错:ERROR: failed to solve: ycr.yyiuap.com/os/openeuler:20.03-sp2: failed to do request: Head “https://ycr.yyiuap.com/v2/os/openeuler/manifests/20.03-sp2”: dial tcp: lookup ycr.yyiuap.com on 10.3.15.14:53: no such host
原因:用docker buildx构建时DNS解析不到ycr的内网私有域名, 可以删除buildx构建器然后重建, 使构建 时使用主机网络:
1 2 3 4 5 6 7 |
docker buildx create \ --name multi-platform \ --use --platform \ linux/amd64,linux/arm64 \ --driver docker-container \ --driver-opt network=host \ --buildkitd-flags '--allow-insecure-entitlement network.host' |
再次尝试构建, 报错:ERROR: failed to solve: ycr.yyiuap.com/os/openeuler:20.03-sp2: failed to do request: Head “https://ycr.yyiuap.com/v2/os/openeuler/manifests/20.03-sp2”: x509: certificate signed by unknown authority
分析:docker daemon虽然已经配置 –insecure-registry=0.0.0.0/0,但是 buildx构建时需要单独配置一下:
The TOML file used to configure the buildkitd daemon settings has a short list of global settings followed by a series of sections for specific areas of daemon configuration.
The file path is /etc/buildkit/buildkitd.toml
for rootful mode, ~/.config/buildkit/buildkitd.toml
for rootless mode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
mkdir /etc/buildkit # 如果有CA证书, 可以配置一下镜像仓库的CA证书: cat > /etc/buildkit/buildkitd.toml << EOF [registry."ycr.yyiuap.com"] http = false insecure = false ca=["/etc/pki/ca-trust/source/anchors/ca.crt"] EOF # 如果不用CA证书,也可以配置成不认证: cat > /etc/buildkit/buildkitd.toml << EOF [registry."ycr.yyiuap.com"] http = false insecure = true EOF |
0 Comments