如何保存 / 同步多架构容器 Docker 镜像

本文最后更新于:2024年7月24日 晚上

前言

随着容器、芯片技术的进一步发展,以及绿色、节能、信创等方面的要求,多 CPU 架构的场景越来越常见。典型的应用场景包括:

  1. 信创:x86 服务器 + 鲲鹏 ARM 等信创服务器;
  2. 个人电脑:苹果 Mac M1 + Windows 电脑(或旧的 Intel 芯片苹果电脑);
  3. Edge:数据中心使用 x86 服务器,边缘 Edge 端使用低功耗的 arm 边缘设备(如树莓派等)。

容器云原生技术在这方面支持的是很好,但是实际使用中细节会有一些问题,举一个例子,就是:如何保存 / 同步多架构容器 Docker 镜像

本次先以将 Docker Hub 的镜像同步到本地镜像仓库为例说明。

词汇表

英文 中文 说明
multi-arch image 多架构镜像
variant 变体 不同变体指的如:redis 镜像的 arm/v5arm/v7 两种变体
manifest 清单
manifest-list 清单(的)列表
layer (镜像)层
image index 镜像索引 OCI 专有名词,含义和 manifest-list 相同
manifest digest 清单摘要

容器镜像如何支持多架构

一个多架构镜像(A multi-arch image)是一种容器镜像,它可以组合不同架构体系(如 amd64 和 arm)的变体(variants),有时还可以组合不同操作系统(如 windows 和 linux)的变体。运行支持多架构的镜像时,容器客户端会自动选择与你的 OS 和架构相匹配的镜像变体。

多架构镜像是基于镜像清单和清单列表实现的。

清单(Manifests)

每个容器镜像都由一个“清单”表示。清单是一个 JSON 文件,用于唯一标识镜像,并引用其层(layer)及其相应的大小。

hello-world Linux 镜像的基本清单类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1510,
"digest": "sha256:fbf289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 977,
"digest": "sha256:2c930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced"
}
]
}

清单列表 (Manifest-lists)

多架构镜像的 清单列表 (通常称为 OCI 镜像 的镜像索引)是镜像的集合(索引),您可以通过指定一个或多个镜像名称来创建一个。它包括有关每个镜像的详细信息,例如支持的操作系统和体系架构、大小和清单摘要 (manifest digest)。清单列表的使用方式与 docker pulldocker run 命令 中的镜像名称相同。

docker CLI 使用 docker manifest命令管理清单和清单列表。

🐾 Warning:

目前,该命令 docker manifest 和子命令是实验性的。有关使用实验性命令的详细信息,请参阅 Docker 文档。

✍️笔者注:可能是因为 实验性 的原因,使用过程中有几个多架构镜像碰到了诡异的问题。

您可以使用该命令 docker manifest inspect 查看清单列表。以下是多架构镜像hello-world:latest 的输出,它有三个清单:两个用于 Linux 操作系统体系架构,一个用于 Windows 体系架构。

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
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 524,
"digest": "sha256:83c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 525,
"digest": "sha256:873612c5503f3f1674f315c67089dee577d8cc6afc18565e0b4183ae355fb343",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 1124,
"digest": "sha256:b791ad98d505abb8c9618868fc43c74aa94d08f1d7afe37d19647c0030905cae",
"platform": {
"architecture": "amd64",
"os": "windows",
"os.version": "10.0.17763.1697"
}
}
]
}

使用 docker manifest 保存多架构镜像

这里是将多架构的镜像推送到本地镜像仓库步骤:

  1. 标记每个特定于体系结构的镜像并将其推送到容器注册表。以下示例假定有两个 Linux 体系结构:arm64 和 amd64。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    docker tag myimage:arm64 \
    192.168.2.23:5000/multi-arch-samples/myimage:arm64

    docker push 192.168.2.23:5000/multi-arch-samples/myimage:arm64

    docker tag myimage:amd64 \
    192.168.2.23:5000/multi-arch-samples/myimage:amd64

    docker push 192.168.2.23:5000/multi-arch-samples/myimage:amd64
  2. 运行 docker manifest create 以创建清单列表,以将前面的镜像合并到多架构镜像中。

    1
    2
    3
    docker manifest create 192.168.2.23:5000/multi-arch-samples/myimage:multi \
    192.168.2.23:5000/multi-arch-samples/myimage:arm64 \
    192.168.2.23:5000/multi-arch-samples/myimage:amd64
  3. 使用以下命令 docker manifest push 将清单推送到镜像仓库:

    1
    docker manifest push 192.168.2.23:5000/multi-arch-samples/myimage:multi
  4. 使用命令 docker manifest inspect 查看清单列表。上一节显示了命令输出的示例。

将多架构清单推送到镜像仓库后,使用多架构镜像的方式与处理单架构镜像的方式相同。例如,使用 docker pull 拉取镜像。

保存 / 同步多架构镜像实用脚本一 - 基于 docker manifest

场景一

已有多架构压缩包 需要 load 压缩包并将多架构镜像上传到本地镜像仓库

以 K3s 为例,官方在 release 时已经发布了多架构的离线镜像压缩包,分别为:

  • k3s-airgap-images-amd64.tar.gz
  • k3s-airgap-images-arm.tar.gz
  • k3s-airgap-images-arm64.tar.gz

这些包已经下载好,并传到客户 / 用户的离线环境机器上。现在需要 load 压缩包并将多架构镜像上传到本地镜像仓库

大致步骤

  1. docker load 压缩包
  2. 其中的镜像逐个打 tag, 改为 < 本地镜像仓库地址 >/.../...:<tag>-<arch>
  3. push 镜像
  4. 以上步骤重复 3 遍,将 tag 带有 -amd64 -arm -arm64 的镜像都 push 到本地镜像仓库
  5. 镜像逐个 docker manifest create 以创建清单列表
  6. 使用以下命令 docker manifest push 将清单逐个推送到镜像仓库

完整脚本如下:

🐾 Warning:

由于本人能力有限,在使用 k3s v1.21.7+k3s1 版本的 8*3 个离线镜像做测试的时候,总是 5 个成功,另外 3 个出现 manifest list 的 arch 和 manifest 对不上的情况。
不知道是我脚本问题还是 docker 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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/bin/bash
amd64_images="k3s-airgap-images-amd64.tar.gz"
arm64_images="k3s-airgap-images-arm64.tar.gz"
arm_images=""
list="k3s-images.txt"

usage() {
echo "USAGE: $0 [--amd64-images k3s-airgap-images-amd64.tar.gz] [--arm64-images k3s-airgap-images-arm64.tar.gz] [---arm-images k3s-airgap-images-arm.tar.gz] --registry my.registry.com:5000"
echo " [-l|--image-list path] text file with list of images; one image per line."
echo " [-x|--amd64-images path] amd64 arch tar.gz generated by docker save."
echo " [-a|--arm64-images path] arm64 arch tar.gz generated by docker save."
echo " [---arm-images path] arm arch tar.gz generated by docker save."
echo " [-r|--registry registry:port] target private registry:port."
echo " [-h|--help] Usage message"
}

push_manifest() {
export DOCKER_CLI_EXPERIMENTAL=enabled
manifest_list=()
for i_arch in "${arch_list[@]}"; do
manifest_list+=("$1-${i_arch}")
done

echo "Preparing manifest $1, list[${arch_list[@]}]"
docker manifest create "$1" "${manifest_list[@]}" --insecure
docker manifest push "$1" --purge --insecure
}

while [[$# -gt 0 ]]; do
key="$1"
case $key in
-r | --registry)
reg="$2"
shift # past argument
shift # past value
;;
-l | --image-list)
list="$2"
shift # past argument
shift # past value
;;
-x | --amd64-images)
amd64_images="$2"
shift # past argument
shift # past value
;;
-a | --arm64-images)
arm64_images="$2"
shift # past argument
shift # past value
;;
--arm-images)
arm_images="$2"
shift # past argument
shift # past value
;;
-h | --help)
help="true"
shift
;;
*)
usage
exit 1
;;
esac
done

if [[-z $reg ]]; then
usage
exit 1
fi

if [[$help ]]; then
usage
exit 0
fi

arch_list=()
if [[-n "${amd64_images}" ]]; then
arch_list+=("amd64")
fi
if [[-n "${arm64_images}" ]]; then
arch_list+=("arm64")
fi
if [[-n "${arm_images}" ]]; then
arch_list+=("arm")
fi

image_list=()
while IFS= read -r i; do
[-z "${i}" ] && continue
image_list+=("${i}")
done <"${list}"

for arch in "${arch_list[@]}"; do
[-z "${arch}" ] && continue

case $arch in
amd64)
docker load --input ${amd64_images}
;;
arm64)
docker load --input ${arm64_images}
;;
arm)
docker load --input ${arm_images}
;;
esac

for i in "${image_list[@]}"; do
[-z "${i}" ] && continue

case $i in
*/*)
image_name="${reg}/${i}"
;;
*)
image_name="${reg}/library/${i}"
;;
esac

docker tag "${i}" "${image_name}-${arch}"
docker rmi -f "${i}"
docker push "${image_name}-${arch}"
done
done

for i in "${image_list[@]}"; do
[-z "${i}" ] && continue

case $i in
*/*)
image_name="${reg}/${i}"
;;
*)
image_name="${reg}/library/${i}"
;;
esac
push_manifest "${image_name}"
done

使用方法:

1
./load-images-multi-arch.sh --registry 192.168.2.23:5000 --arm-images k3s-airgap-images-arm.tar.gz

日志输出如下:

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
$ ./load-images-multi-arch.sh --registry 192.168.2.23:5000 --arm_images k3s-airgap-images-arm.tar.gz
# docker load 镜像第一轮,是 amd64 架构的
67f770da229b: Loading layer [==================================================>] 1.45MB/1.45MB
Loaded image: rancher/library-busybox:1.32.1
...

# 打 tag 并 delete 原 tag 镜像,并 push
Untagged: rancher/coredns-coredns:1.8.3
The push refers to repository [192.168.2.23:5000/rancher/coredns-coredns]
85c53e1bd74e: Pushed
225df95e717c: Pushed
1.8.3-amd64: digest: sha256:db4f1c57978d7372b50f416d1058beb60cebff9a0d5b8bee02bfe70302e1cb2f size: 739
...

# docker load 镜像第二轮,是 arm64 架构的
...
32626eb1fe89: Loading layer [==================================================>] 526.8kB/526.8kB
Loaded image: rancher/pause:3.1

...
Untagged: rancher/pause:3.1
The push refers to repository [192.168.2.23:5000/rancher/pause]
32626eb1fe89: Pushed
3.1-arm64: digest: sha256:2aac966ece8906a535395f92bb25f0e8e21dac737df75b381e8f9bdd3ed56528 size: 527

# docker load 镜像第二轮,是 arm 架构的
8e322dc9c333: Loading layer [==================================================>] 5.045MB/5.045MB
efed3cfd1b26: Loading layer [==================================================>] 1.623MB/1.623MB
a46153382f22: Loading layer [==================================================>] 3.584kB/3.584kB
Loaded image: rancher/klipper-lb:v0.3.4
...

Untagged: rancher/coredns-coredns:1.8.3
The push refers to repository [192.168.2.23:5000/rancher/coredns-coredns]
9f4a0b0fd8b2: Pushed
225df95e717c: Layer already exists
1.8.3-arm: digest: sha256:dfc241eae22da74dd378535b69d7927f897acf48424cdcb90991b33f412cb7ae size: 739

# docker manifest create
Preparing manifest 192.168.2.23:5000/rancher/coredns-coredns:1.8.3, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/coredns-coredns:1.8.3
sha256:dc76fece93e42f05e7013e159097a0d426734fd268467f242d5b155dd49b0221
Preparing manifest 192.168.2.23:5000/rancher/klipper-helm:v0.6.6-build20211022, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/klipper-helm:v0.6.6-build20211022
sha256:e1c6842554ea37e66443cfab9a2422231bf8390b4c69711a74eb4cccde9d3dba
Preparing manifest 192.168.2.23:5000/rancher/klipper-lb:v0.3.4, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/klipper-lb:v0.3.4
sha256:98842bae8630a2aab1a94960185e152745ecf16ca69cf1eefdb53848cbc41063
Preparing manifest 192.168.2.23:5000/rancher/library-busybox:1.32.1, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/library-busybox:1.32.1
sha256:0b93c11bfd89ee5c971deaf9f312d115b2e1d797f79a7f68a266baecfb09a99f
Preparing manifest 192.168.2.23:5000/rancher/library-traefik:2.4.8, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/library-traefik:2.4.8
sha256:58464dda10504d271a17855541ed8d31a787ea25eb751ecce90e14256f23eb24
Preparing manifest 192.168.2.23:5000/rancher/local-path-provisioner:v0.0.19, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/local-path-provisioner:v0.0.19
sha256:0c797ef85540a4934ea84a9471f4f5a10c93f749ee668d92527361c61bbe98c3
Preparing manifest 192.168.2.23:5000/rancher/metrics-server:v0.3.6, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/metrics-server:v0.3.6
sha256:742595f61320bcaead987c5aafc3eb64b9a9151edb02b9e4d27f8abcae26d92e
Preparing manifest 192.168.2.23:5000/rancher/pause:3.1, list[amd64 arm64 arm]
Created manifest list 192.168.2.23:5000/rancher/pause:3.1
sha256:f3ef3cbaf2ea466a0c2a2cf3db0d9fbc30f4c24e57a79603aa0fa8999d4813b0

Skopeo 简介

最近 Skopeo 版本更新到了 v1.8, 最近的版本增加了一些与 多架构 有关的 flags, 使得通过 skopeo 进行多架构镜像的保存 / 同步更为方便。

📝 Notes:

目前关于多架构,只有 3 个选项,3 个选项都没有选择源镜像多个架构的其中几个的能力,但正在开发中。
具体见这个 Issue: feature: Support list of archs for sync command · Issue #1694 · containers/skopeo (github.com)

以下是一些相关 flags:

  • skopeo
    • --override-arch <arch>: 使用 arch 代替机器的架构来选择镜像。
    • --override-os <os>: 使用 os 代替机器的 OS 来选择镜像。
    • --override-variant <variant>: 使用 variant 运行的架构的变体来选择镜像。(不同变体指的如:redis 镜像的 arm/v5arm/v7 两种变体)
  • skopeo copy
    • --all, -a: 如果 source-image 引用的是一个镜像列表,那么 不要 只复制与当前操作系统和体系架构匹配的镜像(取决于全局的 --override-os--override-arch--override-variant选项的使用),而是尝试复制列表中的所有镜像,以及列表本身。
    • --multi-arch: 如果源镜像引用多架构镜像,则控制要复制的内容。默认设置是system
      • system: 仅复制与系统架构匹配的镜像
      • all: 复制完整的多架构镜像
      • index-only: 仅复制镜像索引 (image index).(index-only选项通常会失败,除非目标中已经存在每个架构所引用的镜像,或者目标注册中心支持稀疏索引。)
  • skopeo sync
    • --all, -a: 同上

📝 Notes:

根据 skopeo copy --multi-arch index-only 的描述,场景一 还有一种实现就是:

  1. docker manifest 之前的步骤,维持原状
  2. docker manifest createdocker manifest push 替换为 skopeo copy --multi-arch index-only

保存 / 同步多架构镜像实用脚本二 - 基于 skopeo copy

场景二

直接从 docker.io 同步镜像到本地镜像仓库

以 K3s 某一版本为例,镜像列表为:

  • rancher/coredns-coredns:1.8.3
  • rancher/klipper-helm:v0.6.6-build20211022
  • rancher/klipper-lb:v0.3.4
  • rancher/library-busybox:1.32.1
  • rancher/library-traefik:2.4.8
  • rancher/local-path-provisioner:v0.0.19
  • rancher/metrics-server:v0.3.6
  • rancher/pause:3.1

这里直接基于 镜像搬运工 skopeo 提供的脚本做修改,修改后如下:

📝 Notes:

因为较新版本的 skopeo 才有上面说的一系列 flags, 我的 Ubuntu apt 安装的 skopeo 还停留在 v1.5 版本,没有上述功能。所以直接通过 docker run 方式运行
除此之外还添加了 --multi-arch all 选项。

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
#!/bin/bash
GREEN_COL="\\033[32;1m"
RED_COL="\\033[1;31m"
NORMAL_COL="\\033[0;39m"
SOURCE_REGISTRY=$1
TARGET_REGISTRY=$2
IMAGES_LIST_FILE=$3
: ${IMAGES_LIST_FILE:="k3s-images.txt"}
: ${TARGET_REGISTRY:="192.168.2.23:5000"}
: ${SOURCE_REGISTRY:="docker.io"}

set -eo pipefail

CURRENT_NUM=0
ALL_IMAGES="$(sed -n '/#/d;s/:/:/p' ${IMAGES_LIST_FILE} | sort -u)"
TOTAL_NUMS=$(echo "${ALL_IMAGES}" | wc -l)

skopeo_copy() {
if docker run -it quay.io/skopeo/stable:latest copy --insecure-policy --src-tls-verify=false --dest-tls-verify=false \
--src-creds caseycui:xxxxxxxxxxxxxxxxxxxxx --multi-arch all --override-os linux -q docker://$1 docker://$2; then
echo -e "$GREEN_COL Progress: ${CURRENT_NUM}/${TOTAL_NUMS} sync $1 to $2 successful $NORMAL_COL"
else
echo -e "$RED_COL Progress: ${CURRENT_NUM}/${TOTAL_NUMS} sync $1 to $2 failed $NORMAL_COL"
exit 2
fi
}

for image in ${ALL_IMAGES}; do
let CURRENT_N192.168.2.23:5000UM=${CURRENT_NUM}+1
skopeo_copy ${SOURCE_REGISTRY}/${image} ${TARGET_REGISTRY}/${image}
done

运行效果如下:

1
2
3
4
5
6
7
8
9
$ bash sync.sh
Progress: 1/8 sync docker.io/rancher/coredns-coredns:1.8.3 to 192.168.2.23:5000/rancher/coredns-coredns:1.8.3 successful
Progress: 2/8 sync docker.io/rancher/klipper-helm:v0.6.6-build20211022 to 192.168.2.23:5000/rancher/klipper-helm:v0.6.6-build20211022 successful
Progress: 3/8 sync docker.io/rancher/klipper-lb:v0.3.4 to 192.168.2.23:5000/rancher/klipper-lb:v0.3.4 successful
Progress: 4/8 sync docker.io/rancher/library-busybox:1.32.1 to 192.168.2.23:5000/rancher/library-busybox:1.32.1 successful
Progress: 5/8 sync docker.io/rancher/library-traefik:2.4.8 to 192.168.2.23:5000/rancher/library-traefik:2.4.8 successful
Progress: 6/8 sync docker.io/rancher/local-path-provisioner:v0.0.19 to 192.168.2.23:5000/rancher/local-path-provisioner:v0.0.19 successful
Progress: 7/8 sync docker.io/rancher/metrics-server:v0.3.6 to 192.168.2.23:5000/rancher/metrics-server:v0.3.6 successful
Progress: 8/8 sync docker.io/rancher/pause:3.1 to 192.168.2.23:5000/rancher/pause:3.1 successful

最终效果

最终本地的镜像效果如下:

Docker Registry 中的多架构镜像

🎉🎉🎉

📚️ Reference


如何保存 / 同步多架构容器 Docker 镜像
https://ewhisper.cn/posts/52241/
作者
东风微鸣
发布于
2022年7月4日
许可协议