如何使用 Skopeo 做一个优雅的镜像搬运工

Posted by Mike on 2022-06-01

1. 基础介绍

描述: 作为公司内部 PaaS toB 产品的打包发布人员,容器镜像对我们打工人而言就像是工地上的砖头 🧱,而我的一部分工作就是将这些砖头在各个仓库之间搬来搬去,最终将这些砖头打包放在产品的安装包中,形成一个完整的 PaaS 产品安装包。

  • Q: 在 PaaS (平台即服务)中的大家常说的 ToB 与 ToC 到底是什么?

ToC 面向普通用户服务, 主要是让用户体验感好,解决用户使用方面的问题记录,并返回给前后端开发。
ToB 是面向企业用户服务, 产品可用、其中最关键是让Boss使用Happly!

  • Q: 假如有如下场景,我们从 dockerhub 公共仓库中下载一个 GB 以上的镜像,到本地的私有仓库中,我想通常你会这样做先 docker pull 到本地,然后使用 docker tag 更改为私有仓库地址加上镜像名称版本,最后再使用docker push 上传镜像到私有仓库中,以供其它内网机器拉取并使用。虽然该方法是可行,但是如果有多个大于 GB 以上的镜像需要上传到私有仓库,每次都要先解压 layer 到本地,然后再压缩 layer 上传到私有仓库中,你能想象此过程花费的时间有多久吗? 对于我们运维工程师来说时间就是金钱,所以需想尽一切方法来节约时间成本,那有没有一种办法可以直接将 registry 上的 blob 复制到另一个 registry,中间过程不涉及对镜像 layer 的解压缩,这岂不美哉。

解决方案当然是存在的,如果你不想使用docker进行images镜像拉取上传,我们完成可以使用skope工具来完全替代 docker-cli 来搬运镜像,skopeo是一个命令行实用程序,可对容器映像和映像存储库执行各种操作。

什么是 Skopeo ?

skopeo 使用 API V2 Registry,例如 Docker Registry、Atomic Registry、私有Registry、本地目录和本地 OCI 镜像目录。skopeo 不需要运行守护进程,它可以执行的操作包括:

  • 通过各种存储机制复制镜像,例如,可以在不需要特权的情况下将镜像从一个 Registry 复制到另一个 Registry
  • 检测远程镜像并查看其属性,包括其图层,无需将镜像拉到本地
  • 从镜像库中删除镜像
  • 当存储库需要时,skopeo 可以传递适当的凭据和证书进行身份验证

镜像存储特点

根据 Robin 大佬在 《镜像仓库中镜像存储的原理解析》文章里得出的结论:

  • 通过 Registry API 获得的两个镜像仓库中相同镜像的 manifest 信息完全相同。

  • 两个镜像仓库中相同镜像的 manifest 信息的存储路径和内容完全相同。

  • 两个镜像仓库中相同镜像的 blob 信息的存储路径和内容完全相同

项目信息

2. 安装编译

描述: Skopeo 官方安装&编译方式参考文档: https://github.com/containers/skopeo/blob/main/install.md

本节安装实践环境将在 Ubuntu 20.04 LTS 以及 docker 20.10.12 中进行实践源码编译以及 apt 仓库源下载安装实践。

2.1 源码编译(静态)

描述: 要构建 skopeo 二进制文件您至少需要 Go 1.12 版本以上, 其次构建 skopeo 有两种方法,即在容器中或者在本地环境中构建(安装环境较为复杂), 此处为了方便演示将采用容器方式进行编译构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1.拉取skopeo源码到本地
$ git clone --depth=1 https://github.com/containers/skopeo.git # https://github.com/containers/skopeo.git
$ cd skopeo
$ sed -i 's#proxy.golang.org#https://goproxy.cn#g' skopeo/Makefile

# 2.下载镜像构建依赖
$ sudo apt-get install go-md2man # 构建手册依赖于 go-md2man。
$ whereis go-md2man # 获得本机中go-md2man路径。

# 3.构建静态二进制文件
$ BUILD_IMAGE="golang:latest"
$ docker run --name skopeo-build -v $PWD:/src -v /usr/bin/go-md2man:/go/bin/go-md2man -w /src -e CGO_ENABLED=0 -e GOPROXY=https://goproxy.cn,direct ${BUILD_IMAGE} \
sh -c 'make BUILDTAGS=containers_image_openpgp GO_DYN_FLAGS='
# CGO_CFLAGS="" CGO_LDFLAGS="" GO111MODULE=on go build -mod=vendor -ldflags '-X main.gitCommit=df4d82b960572c19e9333381a203c0ac475766d7 ' -gcflags "" -tags "containers_image_openpgp" -o bin/skopeo ./cmd/skopeo

# 4.运行编译生成的skopeo可执行文件
$ cd ./bin # /opt/software/skopeo/bin
$ ./skopeo --help
# Various operations with container images and container image registries
# .......
# Use "skopeo [command] --help" for more information about a command.

构建关键参数解析:

  • CGO_ENABLED=0 : 设置该环境变量, 禁用 CGO 会导致 Go 在可能的情况下更喜欢静态连接库,而不是动态链接到系统库 (解决可以在Ubuntu或者其它linux发行版中执行编译后二进制文件)。
  • GOPROXY=https://goproxy.cn,direct : Golong 依赖下载镜像站,加快go get依赖拉拉取。
  • BUILDTAGS=containers_image_openpgp : 设置该make参数消除了对libgpgme 及其配套库的依赖, Skopeo 的一些特性依赖于非 Go 库,例如 libgpgme 和 libdevmapper。
  • GO_DYN_FLAGS= : 清空该make参数 (否则会强制创建动态可执行文件)

2.2 分发包安装

描述: skopeo 可能已经打包在您的发行版中,此处以 ubuntu 20.04 为例进行安装。

1
2
3
4
5
6
7
8
9
10
11
# 1.只支持 Ubuntu 20.10 and newer 发行版 
$ sudo apt-get -y update
$ sudo apt-get -y install skopeo

# 2.但 Kubic 项目为 Ubuntu 20.04 提供了软件包,我们可以通过如下方式在我们及其上进行安装。
$ . /etc/os-release
$ echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
$ curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get -y upgrade
$ sudo apt-get -y install skopeo

2.3 容器安装运行

Skopeo 容器镜像可在 quay.io/skopeo/stable:latest 获得, 例如我们采用podman命令进行如下操作:

1
$ podman run docker://quay.io/skopeo/stable:latest copy --help

3. 快速上手

3.1 命令浅析

描述: skopen 是操作各种容器映像和容器映像仓库的工具,其使用方法及其可用命令如下:

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
$ ./skopeo --help    # 子命令可采用如下命令 skopeo [command] --help 命令
Usage:
skopeo [flags]
skopeo [command]
Available Commands:
copy # 复制一个镜像从 A 到 B,这里的 A 和 B 可以为本地 docker 镜像或者 registry 上的镜像;
delete # 删除一个镜像 tag,可以是本地 docker 镜像或者 registry 上的镜像;
help # 帮助查看
inspect # 查看一个镜像的 manifest 或者 image config 详细信息;
list-tags # 列出存储库名称指定的镜像的tag
login # 登陆某个镜像仓库,类似于 docker login 命令
logout # 退出某个已认证的镜像仓库, 类似于 docker logout 命令
manifest-digest # 计算文件的清单摘要是一个sha256sum 值
standalone-sign # 使用本地文件创建签名
standalone-verify # 验证本地文件的签名
sync # 将一个或多个图像从一个位置同步到另一个位置 (该功能非常Nice)
Flags:
--command-timeout duration # 命令超时时间(单位秒)
--debug # 启用debug模式
--insecure-policy # 在不进行任何策略检查的情况下运行该工具(如果没有配置 policy 的话需要加上该参数)
--override-arch ARCH # 处理镜像时覆盖客户端 CPU 体系架构,如在 amd64 的机器上用 skopeo 处理 arm64 的镜像
--override-os OS # 处理镜像时覆盖客户端 OS
--override-variant VARIANT # 处理镜像时使用VARIANT而不是运行架构变量
--policy string # 信任策略文件的路径 (为镜像配置安全策略情况下使用)
--registries.d DIR # 在目录中使用Registry配置文件(例如,用于容器签名存储)
--tmpdir string # 用于存储临时文件的目录
-h, --help help for skopeo
-v, --version Version for Skopeo

3.2 Skopeo 初体验

描述: 在使用体验skopeo之前,我们需要了解一哈 Skopeo 可以在那些图像和存储库类型上执行镜像操作(官网文档走一波):

Repository types Describe Example
containers-storage:docker-reference 适用于后端是 Podman, CRI-O, Buildah 的情况 containers-storage:
dir:path 适用于将manifest, layer tarballs 和 signatures 存储为单独文件的现有本地目录路径的情况 dir:/tmp/alpine:latest
docker://docker-reference 适用于Registry中实现"Docker Registry HTTP API V2"的镜像的情况 docker://harbor.weiyigeek.top/myblog:v2.8
docker-archive:path[:docker-reference] 适用于采用docker save命令导出镜像以tar格式存储的文件的情况 docker-archive:alpine.tar
docker-daemon:docker-reference 适用于存储在 docker 守护进程内部存储中的图像的情况 docker-daemon:alpine:latest
oci:path:tag 适用于符合"Open Container Image Layout Specification"的目录中的图像标记 oci:alpine:latest

温馨提示: 同一个镜像存在的方式有可能不同,不同类型方式存储对镜像的 layer 处理的方式也不一样,。

测试环境说明

1
2
3
Docker 官方 hub 仓库 -> docker.io             # 官网地址: https://hub.docker.com/
私有 Harbor 仓库 -> harbor.weiyigeek.top
临时创建的本地仓库 -> 192.168.12.111:5000 # 一梭子解决: docker run -d -p 5000:5000 --name registry -v /opt/data/registry:/var/lib/registry registry:2

说明: 上述仓库都是在Registry中支持Docker Registry HTTP API V2版本的。

3.2.1 Skopeo login/loout - 远程仓库 Auth

描述: 在使用 skopeo 前如果 src 或 dest 镜像是在 registry 仓库中的并且配置了非 public 的镜像需要相应的 auth 认证, 此时我们可以使用 docker login 或者 skopeo login 的方式登录到 registry 仓库,然后默认会在~/.docker目录下生成 registry 登录配置文件 config.json ,该文件里保存了登录需要的验证信息,skopeo 拿到该验证信息才有权限往 registry push 镜像。

  • 登陆认证
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
# (1) skopeo login 登陆示例 (两种方式)
$ skopeo login -u WeiyiGeek -p testpassword harbor.weiyigeek.top
# Login Succeeded!

# (2) docker login 登陆示例
$ docker login -u WeiyiGeek docker.io
$ docker login -u WeiyiGeek harbor.weiyigeek.top
$ docker login -u anonymous -p anonymous 192.168.12.111:5000 # 实际上临时仓库没有配置认证, 账号密码随意即可。
# WARNING! Using --password via the CLI is insecure. Use --password-stdin.
# WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
# Login Succeeded

# (3) docker login 生成的 registry 登录配置文件(base64编码安全性不多说)
$ cat ~/.docker/config.json
{
"auths": {
"192.168.12.111:5000": {
"auth": "YW5v*******Q=="
},
"harbor.weiyigeek.top": {
"auth": "YWR*******LkA="
},
"https://index.docker.io/v1/": {
"auth": "d2Vp**************kyZA=="
}
}
}
  • 注销认证
1
$ skopeo logout myregistrydomain.com:5000

温馨提示: 如果企业自建harbor仓库(一般都会设置自签证书)或者其它私有仓库配置证书,为了防止出错建议进行以下操作(正式环境请根据需要进行配置)。

1
2
3
4
5
6
7
# (1) 在 /etc/docker/daemon.json 中配置 insecure-registries 字段,表示允许不安全的仓库。
"insecure-registries": ["harbor.weiyigeek.top","192.168.12.111:5000"]

# (2) 从官方文档可知客户端要使用tls与Harbor通信使用的还是`自签证书`,那么必须建立一个目录:`/etc/docker/certs.d`
# 如果配置可能会出现 x509: certificate signed by unknown authority 错误提示。
$ mkdir -vp /etc/docker/certs.d/harbor.weiyigeek.top
$ cp -a /deployapp/harbor/harbor.pem /etc/docker/certs.d/harbor.weiyigeek.top/harbor.crt

温馨提示: 为了防止后续执行 skopeo 命令操作镜像时出错, 建议忽略 policy 策略和证书校验参数如下:

1
2
3
--insecure-policy \
--src-tls-verify=false \
--dest-tls-verify=false \

3.2.2 Skopeo inspect - 检查存储库中的镜像

描述: skopeo 能够检查容器 Registry 上的存储库并获取图像层。检查命令获取存储库的清单,它能够向您显示有关整个存储库或标签的类似 docker inspect 的 json 输出。与 docker inspect 相比,此工具可帮助您在拉取存储库或标签之前收集有用的信息(使用磁盘空间), 检查命令可以向您显示给定存储库可用的标签、映像具有的标签、映像的创建日期和操作系统等。

支持传输的类型 : containers-storage, dir, docker, docker-archive, docker-daemon, oci, oci-archive, ostree, tarball

  • 步骤 01. 显示 busybox:latest 镜像的属性相关信息。
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
$ skopeo inspect docker://docker.io/busybox:latest
{
"Name": "docker.io/library/busybox",
"Digest": "sha256:5acba83a746c7608ed544dc1533b87c737a0b0fb730301639a0179f9344b1678",
"RepoTags": [
"1",
"1-glibc",
"1-musl",
"1-ubuntu",
"1-uclibc",
"1.21-ubuntu",
"1.21.0-ubuntu",
....... # 镜像历史tags
"unstable-uclibc"
],
"Created": "2021-12-30T19:19:41.006954958Z",
"DockerVersion": "20.10.7",
"Labels": null,
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa"
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]
}
  • 步骤 02. 显示 busybox:latest 镜像的容器配置相关信息。
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
$ skopeo inspect --config docker://docker.io/busybox:latest  | jq
{
"created": "2021-12-30T19:19:41.006954958Z",
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"sh"
]
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:01fd6df81c8ec7dd24bbbd72342671f41813f992999a3471b9d9cbc44ad88374"
]
},
"history": [
{
"created": "2021-12-30T19:19:40.833034683Z",
"created_by": "/bin/sh -c #(nop) ADD file:6db446a57cbd2b7f4cfde1f280177b458390ed5a6d1b54c6169522bc2c4d838e in / "
},
{
"created": "2021-12-30T19:19:41.006954958Z",
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
"empty_layer": true
}
]
}
  • 步骤 03. 显示未经验证的图像 Digest(摘要)
1
$ skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://docker.io/busybox:latest

3.2.3 Skopeo copy - 仓库镜像拷贝

描述: skopeo 可以在各种存储机制之间复制容器镜像,支持包括容器仓库(The Quay, Docker Hub, OpenShift, GCR, ,Artifactory ...)以及容器存储后端 (Podman, CRI-O, Docker) 等、本地目录、本地 OCI-layout 目录。

例如,此处我从 hub 仓库复制 busybox:latest 镜像到私有 harbor 仓库中,在从私有 harbor 仓库中拷贝到本地指定目录中。

  • 步骤 01. 从 regsitry A 到 registry B 复制 busybox:latest 镜像。
1
2
3
4
5
6
$ skopeo copy --insecure-policy --src-tls-verify=false --dest-tls-verify=false --dest-authfile /root/.docker/config.json docker://docker.io/busybox:latest docker://harbor.weiyigeek.top/devops/busybox:latest
# Getting image source signatures
# Copying blob 5cc84ad355aa done
# Copying config beae173cca done
# Writing manifest to image destination
# Storing signatures

Tips: 由上述日志可以看到 skopeo 是直接从 registry 中 copy 镜像 layer 的 blob 文件,传输是镜像在 registry 中存储的原始格式。

  • 步骤 02. 从 registry B 复制 busybox:latest 镜像到本地 busybox:latest 目录中。
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
$ skopeo copy --insecure-policy --src-tls-verify=false docker://harbor.weiyigeek.top/devops/busybox:latest dir:busybox:latest
# Getting image source signatures
# Copying blob 5cc84ad355aa done
# Copying config beae173cca done
# Writing manifest to image destination
# Storing signatures

$ ls && tree busybox\:latest/
busybox:latest/
├── 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa # blob 块文件 -> vnd.docker.image.rootfs.diff.tar.gzip
├── beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a # 镜像配置信息文件 -> vnd.docker.container.image.v1+json
├── manifest.json
└── version
0 directories, 4 files

# 查看镜像的 manifest 文件
$ cat busybox\:latest/manifest.json
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1456,
"digest": "sha256:beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 772788,
"digest": "sha256:5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa"
}
]
}

# 根据 manifest 文件查看镜像的 image config 文件 (存放镜像Build指令与镜像相关配置信息)
$ jq '.' busybox\:latest/beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"sh"
],
"Image": "sha256:da658412c37aa24e561eb7e16c61bc82a9711340d8fb5cf1a8f39d8e96d7f723",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
........
"history": [
{
"created": "2021-12-30T19:19:40.833034683Z",
"created_by": "/bin/sh -c #(nop) ADD file:6db446a57cbd2b7f4cfde1f280177b458390ed5a6d1b54c6169522bc2c4d838e in / "
},
{
"created": "2021-12-30T19:19:41.006954958Z",
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:01fd6df81c8ec7dd24bbbd72342671f41813f992999a3471b9d9cbc44ad88374"
]
}

}
  • 步骤 03. 将 busybox:latest 镜像从 registry B 复制到本地目录,并以 OCI 格式保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ skopeo copy --insecure-policy --src-tls-verify=false docker://harbor.weiyigeek.top/devops/busybox:latest oci:busybox-latest
# Getting image source signatures
# Copying blob 5cc84ad355aa done
# Copying config 48edd9298a done
# Writing manifest to image destination
# Storing signatures

$ tree -h busybox-latest/
# busybox-latest/
# ├── [4.0K] blobs
# │ └── [4.0K] sha256
# │ ├── [ 347] 1612e16ff3f6b0d09eefdc4e9d5c5c0624f63032743e016585b095b958778016
# │ ├── [ 575] 48edd9298a25de2c97cd574a5523026f87576c6b7202330a2b60ce7d304ec307
# │ └── [755K] 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa # Blob 块 -
# ├── [ 186] index.json
# └── [ 31] oci-layout
# 2 directories, 5 files
  • 步骤 04. 将 alpine:3.13.1 镜像从 docker 本地存储( /var/lib/docker/image) push 到 registry B中(实际上替代 docker push 功能)
1
2
3
4
5
6
7
8
9
10
11
# 在  /var/lib/docker/ 目录中此处主要关心 image (主要存放镜像中layer层的元数据) 和 overlay2 (各层的具体信息)
$ docker images alpine:3.13.1
# REPOSITORY TAG IMAGE ID CREATED SIZE
# alpine 3.13.1 e50c909a8df2 11 months ago 5.61MB

$ skopeo copy --insecure-policy --dest-tls-verify=false --dest-authfile /root/.docker/config.json docker-daemon:alpine:3.13.1 docker://harbor.weiyigeek.top/devops/alpine:3.13.1
# Getting image source signatures
# Copying blob 1119ff37d4a9 done
# Copying config e50c909a8d done
# Writing manifest to image destination
# Storing signatures

3.2.4 Skopeo sync - 镜像同步命令

描述: Skopeo sync可以在容器仓库和本地目录之间同步镜像,其功能类似于阿里云的 image-syncer (https://github.com/AliyunContainerService/image-syncer) 工具, 实际上其比 image-syncer 更强大、灵活性更强一些,废话不多说实践为王。

skopeo sync 镜像同步文件示例:

  • 步骤 01. 将仓库中所有 busybox 镜像版本同步到本地目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ skopeo sync --insecure-policy --src-tls-verify=false --src docker --dest dir harbor.weiyigeek.top/devops/busybox /tmp
# INFO[0000] Tag presence check imagename=harbor.weiyigeek.top/devops/busybox tagged=false
# INFO[0000] Getting tags image=harbor.weiyigeek.top/devops/busybox
# INFO[0000] Copying image ref 1/1 from="docker://harbor.weiyigeek.top/devops/busybox:latest" to="dir:/tmp/busybox:latest"
# Getting image source signatures
# Copying blob 5cc84ad355aa done
# Copying config beae173cca done
# Writing manifest to image destination
# Storing signatures
# INFO[0000] Synced 1 images from 1 sources

$ tree -h /tmp/busybox:latest
/tmp/busybox:latest
├── [755K] 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa
├── [1.4K] beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a
├── [ 527] manifest.json
└── [ 33] version
0 directories, 4 files
  • 步骤 02. 从本地目录 /tmp/ 同步到 docker 的 hub 容器仓库中,此外我们可以通过浏览器看到 weiyigeek 用户下的 busybox 镜像 (https://hub.docker.com/u/weiyigeek)。
1
2
3
4
5
6
7
8
$ skopeo sync --insecure-policy --dest-tls-verify=false --src dir --dest docker /tmp weiyigeek
# INFO[0000] Copying image ref 1/1 from="dir:/tmp/busybox:latest" to="docker://weiyigeek/busybox:latest"
# Getting image source signatures
# Copying blob 5cc84ad355aa skipped: already exists
# Copying config beae173cca done
# Writing manifest to image destination
# Storing signatures
# INFO[0021] Synced 1 images from 1 sources

  • 步骤 03. 从 hub 容器仓库中同步 alpine-jenkins-jnlp:v2.285 镜像到本地临时容器仓库中。
1
2
3
4
5
6
$ skopeo sync --insecure-policy --src-tls-verify=false --dest-tls-verify=false --src docker --dest docker weiyigeek/alpine-jenkins-jnlp:v2.285 192.168.12.111:5000/
# INFO[0000] Tag presence check imagename="weiyigeek/alpine-jenkins-jnlp:v2.285" tagged=true
# INFO[0000] Copying image ref 1/1 from="docker://weiyigeek/alpine-jenkins-jnlp:v2.285" to="docker://192.168.12.111:5000/alpine-jenkins-jnlp:v2.285"
# Getting image source signatures
# Copying blob 68517a8c32d3 [======>-------------------------------] 45.0MiB / 255.7MiB
# Copying blob 4c0d98bf9879 done
  • 步骤 04. 以配置文件方式进行同步, 首先我们需要准备一个需要同步的资源清单。
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
# YAML 文件内容(用于 **--src yaml** 的源)
$ cat <<'EOF' > skopeo-sync.yml
registry.example.com:
images:
busybox: []
redis:
- "1.0"
- "2.0"
- "sha256:111111"
images-by-tag-regex:
nginx: ^1\.13\.[12]-alpine-perl$
credentials:
username: john
password: this is a secret
tls-verify: true
cert-dir: /home/john/certs
quay.io:
tls-verify: false
images:
coreos/etcd:
- latest
EOF

# 以yaml文件方式进行同步镜像到 my-registry.local.lan/repo/ 仓库中
$ skopeo sync --src yaml --dest docker skopeo-sync.yml my-registry.local.lan/repo/

skopeo-sync.yml 文件中镜像匹配复制镜像说明:

3.2.5 Skopeo list-tags - 仓库中镜像 Tag 查看

描述: 利用该命令我们可以列出 registry 上的某个镜像的所有 tag ,它是使用标准的 registry API 来获取镜像 tag。

简单示例:

1
$ skopeo list-tags docker://harbor.weiyigeek.top/devops/busybox:latest

3.2.6 Skopeo delete - 删除仓库中镜像 Tag

描述: 使用该命令我们可以删除镜像 Tag,注意此处仅仅只是通过 registry API 来删除镜像的 tag(即删除了 tag 对 manifests 文件的引用)并非真正将镜像删除掉,如果想要删除镜像的 layer 还是需要通过 registry GC 的方式。

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
# 方式1. 利用 skopeo delete
$ skopeo delete docker://harbor.weiyigeek.top/devops/busybox:latest --debug
# DEBU[0000] Loading registries configuration "/etc/containers/registries.conf"
# DEBU[0000] Found credentials for harbor.weiyigeek.top in credential helper containers-auth.json in file /home/weiyigeek/.docker/config.json
# DEBU[0000] Using registries.d directory /etc/containers/registries.d for sigstore configuration
# DEBU[0000] No signature storage configuration found for harbor.weiyigeek.top/devops/busybox:latest, using built-in default file:///home/weiyigeek/.local/share/containers/sigstore
# DEBU[0000] Looking for TLS certificates and private keys in /etc/docker/certs.d/harbor.weiyigeek.top
# DEBU[0000] crt: /etc/docker/certs.d/harbor.weiyigeek.top/harbor.crt
# DEBU[0000] GET https://harbor.weiyigeek.top/v2/
# DEBU[0000] Ping https://harbor.weiyigeek.top/v2/ status 401
# DEBU[0000] GET https://harbor.weiyigeek.top/service/token?account=WeiyiGeek&scope=repository%3Adevops%2Fbusybox%3A%2A&service=harbor-registry
# DEBU[0000] GET https://harbor.weiyigeek.top/v2/devops/busybox/manifests/latest
# DEBU[0000] DELETE https://harbor.weiyigeek.top/v2/devops/busybox/manifests/sha256:62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee
# DEBU[0000] Deleting /home/weiyigeek/.local/share/containers/sigstore/devops/busybox@sha256=62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee/signature-1

# 方式2.利用 curl 命令进行 registery 进行删除 Tag。
$ curl --header "Accept: application/vnd.docker.distribution.manifest.v2+json" -I -X GET http://192.168.12.111:5000/v2/busybox/manifests/latest
# HTTP/1.1 200 OK
# Content-Length: 527
# Content-Type: application/vnd.docker.distribution.manifest.v2+json
# Docker-Content-Digest: sha256:62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee
# Docker-Distribution-Api-Version: registry/2.0
# Etag: "sha256:62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee"
# X-Content-Type-Options: nosniff
# Date: Thu, 20 Jan 2022 13:18:28 GMT

# 一把梭织搞定
$ Docker-Content-Digest=$(curl -s --header "Accept: application/vnd.docker.distribution.manifest.v2+json" -I -X GET http://192.168.12.111:5000/v2/busybox/manifests/latest | grep "Docker-Content-Digest" | cut -d ' ' -f 2)
$ curl -I -X DELETE http://192.168.12.111:5000/v2/busybox/manifests/${Docker-Content-Digest}

4. 镜像同步最佳实践

本节,主要参考我前同事木子.其博客地址(https://blog.k8s.li)。

4.1 指定文本中镜像同步

假如,给你一个镜像列表 images-list.txt, 其格式如下, 我们可以直接采用 shell 脚本调用 skopeo 进行执行。

1
2
3
4
5
6
7
8
# images-list.txt
cat <<'EOF' > images-list.txt
kubesphere/kube-apiserver:v1.20.6
kubesphere/kube-scheduler:v1.20.6
kubesphere/kube-proxy:v1.20.6
kubesphere/kube-controller-manager:v1.20.6
kubesphere/kube-apiserver:v1.19.8
EOF
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
#!/bin/bash
GREEN_COL="\\033[32;1m"
RED_COL="\\033[1;31m"
NORMAL_COL="\\033[0;39m"

SOURCE_REGISTRY=$1
TARGET_REGISTRY=$2

# shell 变量赋值,当没有从命令行中传递值给SOURCE_REGISTRY和TARGET_REGISTRY变量时,便采用下述值进行覆盖。
: ${IMAGES_LIST_FILE:="images-list.txt"}
: ${TARGET_REGISTRY:="hub.k8s.li"}
: ${SOURCE_REGISTRY:="docker.io"}

BLOBS_PATH="docker/registry/v2/blobs/sha256"
REPO_PATH="docker/registry/v2/repositories"

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)

# shopeo 拷贝函数,注意其传递的参数,此处值得学习记录。
skopeo_copy() {
if skopeo copy --insecure-policy --src-tls-verify=false --dest-tls-verify=false \
--override-arch amd64 --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_NUM=${CURRENT_NUM}+1
skopeo_copy ${SOURCE_REGISTRY}/${image} ${TARGET_REGISTRY}/${image}
done
  • 执行命令和结果:
1
2
3
4
5
6
$ bash sync.sh docker.io localhost:5000
Progress: 1/143 sync docker.io/alpine:3.14 to localhost:5000/alpine:3.14 successful
Progress: 2/143 sync docker.io/busybox:1.31.1 to localhost:5000/busybox:1.31.1 successful
....
Progress: 142/143 sync docker.io/weaveworks/scope:1.13.0 to localhost:5000/weaveworks/scope:1.13.0 successful
Progress: 143/143 sync docker.io/wordpress:4.8-apache to localhost:5000/wordpress:4.8-apache successful

4.2 使用 registry 存储特性同步

描述: 将镜像从 registry 中同步到本地目录,使用 registry 存储的特性,将本地目录中的镜像转换成 registry 存储的格式, 这样的好处就是可以去除一些 skopeo dir 中重复的 layers,减少镜像的总大小。

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
#!/bin/bash
set -eo pipefail

GREEN_COL="\\033[32;1m"
RED_COL="\\033[1;31m"
NORMAL_COL="\\033[0;39m"

# 命令行参数
SOURCE_REGISTRY=$1
TARGET_REGISTRY=$2
IMAGES_DIR=$2

: ${IMAGES_DIR:="images"}
: ${IMAGES_LIST_FILE:="images-list.txt"}
: ${SOURCE_REGISTRY:="docker.io"}
: ${TARGET_REGISTRY:="hub.k8s.li"}

# hub.k8s.li 仓库服务器中的目录
BLOBS_PATH="docker/registry/v2/blobs/sha256"
REPO_PATH="docker/registry/v2/repositories"

# 记录当前数和总镜像数
CURRENT_NUM=0
ALL_IMAGES="$(sed -n '/#/d;s/:/:/p' ${IMAGES_LIST_FILE} | sort -u)"
TOTAL_NUMS=$(echo "${ALL_IMAGES}" | wc -l)

# 从远程仓库同步指定镜像到本地目录中。
skopeo_sync() {
if skopeo sync --insecure-policy --src-tls-verify=false --dest-tls-verify=false \
--override-arch amd64 --override-os linux --src docker --dest dir $1 $2 > /dev/null; 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
}

convert_images() {
rm -rf ${IMAGES_DIR}; mkdir -p ${IMAGES_DIR}
for image in ${ALL_IMAGES}; do
let CURRENT_NUM=${CURRENT_NUM}+1

# 取 images-list.txt 文本中的每一行,并分隔存储。
image_name=${image%%:*}
image_tag=${image##*:}
image_repo=${image%%/*}

# 函数调用 从仓库同步镜像到本地images目录
skopeo_sync ${SOURCE_REGISTRY}/${image} ${IMAGES_DIR}/${image_repo}

# 在本地images目录中,取得get image manifest sha256sum 信息
manifest="${IMAGES_DIR}/${image}/manifest.json"
manifest_sha256=$(sha256sum ${manifest} | awk '{print $1}') # 62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee
mkdir -p ${BLOBS_PATH}/${manifest_sha256:0:2}/${manifest_sha256} # docker/registry/v2/blobs/sha256/62/62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732dee
ln -f ${manifest} ${BLOBS_PATH}/${manifest_sha256:0:2}/${manifest_sha256}/data # 该 data 文件实际上是镜像的 manifest.json 文件。

# make image repositories dir
mkdir -p ${REPO_PATH}/${image_name}/{_uploads,_layers,_manifests}
mkdir -p ${REPO_PATH}/${image_name}/_manifests/revisions/sha256/${manifest_sha256}
mkdir -p ${REPO_PATH}/${image_name}/_manifests/tags/${image_tag}/{current,index/sha256}
mkdir -p ${REPO_PATH}/${image_name}/_manifests/tags/${image_tag}/index/sha256/${manifest_sha256}

# create image tag manifest link file
echo -n "sha256:${manifest_sha256}" > ${REPO_PATH}/${image_name}/_manifests/revisions/sha256/${manifest_sha256}/link # sha256:62ffc2ed7554e4c6d360bce40bbcf196573dd27c4ce080641a2c59867e732deer
echo -n "sha256:${manifest_sha256}" > ${REPO_PATH}/${image_name}/_manifests/tags/${image_tag}/current/link
echo -n "sha256:${manifest_sha256}" > ${REPO_PATH}/${image_name}/_manifests/tags/${image_tag}/index/sha256/${manifest_sha256}/link

# link image layers file to registry blobs dir
for layer in $(sed '/v1Compatibility/d' ${manifest} | grep -Eo "\b[a-f0-9]{64}\b"); do # 匹配 manifest.json 中"digest"两个不带sha256的值
mkdir -p ${BLOBS_PATH}/${layer:0:2}/${layer} # 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa 、 beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a
mkdir -p ${REPO_PATH}/${image_name}/_layers/sha256/${layer} # 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa 、 beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a
echo -n "sha256:${layer}" > ${REPO_PATH}/${image_name}/_layers/sha256/${layer}/link # sha256:5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa
ln -f ${IMAGES_DIR}/${image}/${layer} ${BLOBS_PATH}/${layer:0:2}/${layer}/data # 复制images目录中 "application/vnd.docker.container.image.v1+json" 容器配置 config 与 多个 "application/vnd.docker.image.rootfs.diff.tar.gzip" layer
done
done
}

convert_images
  • install.sh : 使用这个脚本将 registry 存储中的镜像转换成 skopeo dir 的方式,然后再将镜像同步到 registry 中。
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
#!/bin/bash
REGISTRY_DOMAIN="harbor.k8s.li"
REGISTRY_PATH="/var/lib/registry"

# 切换到 registry 存储主目录下
cd ${REGISTRY_PATH}
gen_skopeo_dir() {
# 定义 registry 存储的 blob 目录 和 repositories 目录,方便后面使用
BLOB_DIR="docker/registry/v2/blobs/sha256"
REPO_DIR="docker/registry/v2/repositories"
# 定义生成 skopeo 目录
SKOPEO_DIR="docker/skopeo"
# 通过 find 出 current 文件夹可以得到所有带 tag 的镜像,因为一个 tag 对应一个 current 目录
for image in $(find ${REPO_DIR} -type d -name "current"); do
# 根据镜像的 tag 提取镜像的名字
name=$(echo ${image} | awk -F '/' '{print $5"/"$6":"$9}')
link=$(cat ${image}/link | sed 's/sha256://')
mfs="${BLOB_DIR}/${link:0:2}/${link}/data"
# 创建镜像的硬链接需要的目录
mkdir -p "${SKOPEO_DIR}/${name}"
# 硬链接镜像的 manifests 文件到目录的 manifest 文件
ln ${mfs} ${SKOPEO_DIR}/${name}/manifest.json
# 使用正则匹配出所有的 sha256 值,然后排序去重
layers=$(grep -Eo "\b[a-f0-9]{64}\b" ${mfs} | sort -n | uniq)
for layer in ${layers}; do
# 硬链接 registry 存储目录里的镜像 layer 和 images config 到镜像的 dir 目录
ln ${BLOB_DIR}/${layer:0:2}/${layer}/data ${SKOPEO_DIR}/${name}/${layer}
done
done
}
sync_image() {
# 使用 skopeo sync 将 dir 格式的镜像同步到 harbor
for project in $(ls ${SKOPEO_DIR}); do
skopeo sync --insecure-policy --src-tls-verify=false --dest-tls-verify=false \
--src dir --dest docker ${SKOPEO_DIR}/${project} ${REGISTRY_DOMAIN}/${project}
done
}
gen_skopeo_dir

温馨提示: 此种方式是有些复杂对于大镜像的复制是推荐的, 而对于一些小镜像且显得多余。

4.3 从 registry 存储中 select 出镜像进行同步

描述: 先将镜像同步到一个 registry 中,再将镜像从 registry 存储中捞出来,该 registry 可以当作一个镜像存储的池子,我们使用 Linux 中硬链接的特性将镜像"复制"一份出来,然后再打一个 tar 包, 这样做的好处就是每次打包镜像的时候都能复用历史的镜像数据,而且性能极快。

  • 步骤 01. 先将镜像同步到一个固定的 registry 中。
1
$ bash skopeo-copy.sh docker.io localhost:5000
  • 步骤 02. 使用该脚本将镜像从 registry 存储中捞出来
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
#!/bin/bash
set -eo pipefail
# 命令行变量
IMAGES_LIST="$1"
REGISTRY_PATH="$2"
OUTPUT_DIR="$3"

# Registry 仓库数据目录
BLOB_DIR="docker/registry/v2/blobs/sha256"
REPO_DIR="docker/registry/v2/repositories"

# 判断输出目录是否存在如不存在则移除。
if [ -d ${OUTPUT_DIR} ];then
rm -rf ${OUTPUT_DIR};
fi
mkdir -p ${OUTPUT_DIR}

for image in $(find ${IMAGES_LIST} -type f -name "*.list" | xargs grep -Ev '^#|^/' | grep ':'); do
# 镜像名称和Tag
image_name=${image%%:*}
image_tag=${image##*:}

# link 路径获取
tag_link=${REGISTRY_PATH}/${REPO_DIR}/${image_name}/_manifests/tags/${image_tag}/current/link
manifest_sha256=$(sed 's/sha256://' ${tag_link})
manifest=${REGISTRY_PATH}/${BLOB_DIR}/${manifest_sha256:0:2}/${manifest_sha256}/data
mkdir -p ${OUTPUT_DIR}/${BLOB_DIR}/${manifest_sha256:0:2}/${manifest_sha256}

# 强制硬链接到指定目录
ln -f ${manifest} ${OUTPUT_DIR}/${BLOB_DIR}/${manifest_sha256:0:2}/${manifest_sha256}/data

# make image repositories dir
mkdir -p ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/{_uploads,_layers,_manifests}
mkdir -p ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/revisions/sha256/${manifest_sha256}
mkdir -p ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/tags/${image_tag}/{current,index/sha256}
mkdir -p ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/tags/${image_tag}/index/sha256/${manifest_sha256}

# create image tag manifest link file
echo -n "sha256:${manifest_sha256}" > ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/tags/${image_tag}/current/link
echo -n "sha256:${manifest_sha256}" > ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/revisions/sha256/${manifest_sha256}/link
echo -n "sha256:${manifest_sha256}" > ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_manifests/tags/${image_tag}/index/sha256/${manifest_sha256}/link

# 强制创建 /docker/registry/v2/blobs/ 各 layer data 文件到指定目录之中
for layer in $(sed '/v1Compatibility/d' ${manifest} | grep -Eo '\b[a-f0-9]{64}\b' | sort -u); do
mkdir -p ${OUTPUT_DIR}/${BLOB_DIR}/${layer:0:2}/${layer}
mkdir -p ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_layers/sha256/${layer}
ln -f ${BLOB_DIR}/${layer:0:2}/${layer}/data ${OUTPUT_DIR}/${BLOB_DIR}/${layer:0:2}/${layer}/data
echo -n "sha256:${layer}" > ${OUTPUT_DIR}/${REPO_DIR}/${image_name}/_layers/sha256/${layer}/link
done
done

至此完毕!

本文转载自:「 WeiyiGeek 」,原文:https://url.hi-linux.com/xCmXo/ ,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com