镜像搬运工 skopeo 初体验

搬砖工具

上周末的时候更新完一篇《木子的搬砖工具 😂》,最近因为项目需求又发现了个搬砖工具,所以来水篇博客分享给大家。

我司项目组中的一个需求就是:在一台机器上去 pull 一个镜像列表中的镜像,这些镜像存储在 registry A Harbor 上,pull 完这些镜像之后重新打上 tag 然后再 push 到另一个 registry B Harbor 上去。相当于一个同步镜像操作,但和 harbor 里在带的那个镜像同步还有很大的不同,我们仅仅需要同步特定 tag 的镜像,而不是整个 harbor 或者 project 里的全部镜像。目前我们的做法还是最简单的方式,使用 docker 命令行的方式来 pull 镜像,然后打 tag 接着 push 到 B harbor。但是啊,当同步二三百个的镜像,或者镜像的总大小几十 GB 的时候这种原始的方法速度还是太慢了,于是就思考有没有另一个工具可以直接将 registry A 中的某个镜像同步到 registry B 中去。

之前我看到过 漠然大佬 写的博客《如何不通过 docker 下载 docker image》 ,于是咱也就上手试一下这个工具看看能不能帮咱搬点砖 😂。结合这个工具的使用,又一次加深了对容器镜像分发存储的了解,收获颇丰 😋

image

关于镜像的详细分析可以参考 浅谈 docker 中镜像和容器在本地的存储

registry

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

  • 通过 Registry API 获得的两个镜像仓库中相同镜像的 manifest 信息完全相同。
  • 两个镜像仓库中相同镜像的 manifest 信息的存储路径和内容完全相同。
  • 两个镜像仓库中相同镜像的 blob 信息的存储路径和内容完全相同。

docker pull 和 docker push

之所以想使用 skopeo 替代原有使用 docker pull –> docker tag –> docker push 的操作,是因为 docker pull 镜像的时候,registry 中存储的镜像 layer 格式是 vnd.docker.image.rootfs.diff.tar.gzip ,这是一个 tar.gz 类型的文件。我们可以在本地搭建一个 harbor ,并向 harbor 推送一个 alpine:latest 镜像,来分析一下镜像是如何在 registry 中存储的。

  • harbor 的存储目录
tree
`-- registry
    `-- v2      # registry V2 版本
        |-- blobs # blobs 目录下存储镜像的 raw 数据,存储的最小单元为 layer
        |   `-- sha256
        |       |-- 39
        |       |-- cb
        |       `-- f7
        `-- repositories # 镜像的元数据信息
            `-- library
                `-- alpine
  • 镜像的 manifest 是针对 registry 服务端的配置信息
sh-4.2# skopeo inspect docker://index.docker.io/library/alpine:latest --raw
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "config": {
      "mediaType": "application/vnd.docker.container.image.v1+json",
      "size": 1507,
      "digest": "sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a"
   },
   "layers": [
      {
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": 2813316,
         "digest": "sha256:cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08"
      }
   ]
}
  • 仔细看一下 digest 和下面文件夹的名称,他们是一一对应的,因为 manifest 信息就是镜像在 registry 中存储的信息。
tree
|-- [  20]  blobs
|   `-- [  36]  sha256
|       |-- [  78]  39
|       |   `-- [  18]  39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
|       |       `-- [ 528]  data
|       |-- [  78]  cb
|       |   `-- [  18]  cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08
|       |       `-- [2.7M]  data
|       `-- [  78]  f7
|           `-- [  18]  f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a
|               `-- [1.5K]  data
`-- [  21]  repositories
    `-- [  20]  library
        `-- [  55]  alpine
            |-- [  20]  _layers
            |   `-- [ 150]  sha256
            |       |-- [  18]  cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08
            |       |   `-- [  71]  link
            |       `-- [  18]  f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a
            |           `-- [  71]  link
            |-- [  35]  _manifests
            |   |-- [  20]  revisions
            |   |   `-- [  78]  sha256
            |   |       `-- [  18]  39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
            |   |           `-- [  71]  link
            |   `-- [  20]  tags
            |       `-- [  34]  latest
            |           |-- [  18]  current
            |           |   `-- [  71]  link
            |           `-- [  20]  index
            |               `-- [  78]  sha256
            |                   `-- [  18]  39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
            |                       `-- [  71]  link
            `-- [   6]  _uploads

26 directories, 8 files
  • 我们去看一下 [2.7M] data 这个文件,其他文件估计是一些 json 文本用于保存元数据信息。使用 file 命令查看 blobs/sha256/cb/cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08 目录下的 data 文件。镜像的每一层都是存放在一个 64 位长度名称的文件夹下,文件名就是 data 。而且这个文件还是个 gzip 压缩后的文件。我么可以使用 tar 命令将其解压开来。
cd registry/v2/blobs/sha256/cb/cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08
sh-4.2# file data
data: gzip compressed data
sh-4.2# mkdir layer
sh-4.2# tar -xvf data -C layer/
  • 将其解压到 layer 目录下,使用 tree 命令看一下文件夹下的内容就会明白,这不就是我们的 alpine 镜像真实的内容嘛 😂。
sh-4.2# tree -L 1 -d layer
layer
|-- bin
|-- dev
|-- etc
|-- home
|-- lib
|-- media
|-- mnt
|-- opt
|-- proc
|-- root
|-- run
|-- sbin
|-- srv
|-- sys
|-- tmp
|-- usr
`-- var

知道了镜像在 registry 中是如何存储的,我们也就能够明白在当前仅仅为了同步两个 registry 上的镜像使用 docker pull –> docker tag –> docker push 操作的弊端。因为 docker pull 镜像时会对 registry 上的 layer 进行解压缩,这一点和我们的浏览器解压缩一些 gzip 压缩的资源一样道理,为了减少网络传输的流量。当我们 pull 镜像的时候,docker 会有一个单独的进程对镜像进行解压缩,在使用 docker pull 拉取镜像的时候使用 ps 查看一下进程就会找到 docker-untar 这个进程。

docker-untar /var/lib/docker/overlay2/a076db6567c7306f3cdab6040cd7d083ef6a39d125171353eedbb8bde7f203b4/diff

对于一些很大的镜像比如 2GB 以上,有时候镜像 layer 已经 download 完了,但是还在进行镜像的解压缩,性能的瓶颈也就在了解压镜像这一块。对于 docker push 来讲,也是如此。

有没有一种办法可以直接将 registry 上的 blob 复制到另一个 registry,中间过程不涉及对镜像 layer 的解压缩,这岂不美哉 😂。

skopeo install

yum/dnf/zypper/brew

安装方式很简单,对于常见的发相伴直接 install 一把梭就行,从官方文档偷来的安装方式 😂

$ sudo dnf install skopeo

on RHEL/CentOS ≤ 7.x:

$ sudo yum install skopeo

for openSUSE:

$ sudo zypper install skopeo

on alpine:

$ sudo apk add skopeo

on macOS:

$ brew install skopeo

build

由于我的 VPS 机器是 Ubuntu 1804 的 OS ,配置 apt 源并没成功,当场翻车。为了能够快速体验一把还是本地起一个 alpine 容器,在 alpine 里通过 apk add 的方式安装 skopeo。但 alpine 里的 skopeo 版本 还是 0.14.0 😥,GitHub 上的 master 分支已经 1.0.0 了,而且并没有 sync 的选项。

/ # skopeo --help
NAME:
   skopeo - Various operations with container images and container image registries
USAGE:
   skopeo [global options] command [command options] [arguments...]
VERSION:
   0.1.40
COMMANDS:
     copy               Copy an IMAGE-NAME from one location to another
     inspect            Inspect image IMAGE-NAME
     delete             Delete image IMAGE-NAME
     manifest-digest    Compute a manifest digest of a file
     standalone-sign    Create a signature using local files
     standalone-verify  Verify a signature using local files
     help, h            Shows a list of commands or help for one command

真是一波三折啊,绕了一圈最终还是亲自指挥 build 一份吧,不过这个 build 过程也很简单。

git clone https://github.com/containers/skopeo skopeo
cd !$
git checkout v1.0.0
make binary-static DISABLE_CGO=1
cp skopeo /usr/bin/
  • 在这里需要注意一点,如果汝想构建一个在各 Linux 发行版通用的二进制可执行文件,一定要使用 make binary-static DISABLE_CGO=1 ,之前我没有仔细看文档直接 make 一把梭,然后在 Ubuntu 上构建出来的二进制执行文件拿到 CentOS 上去用,当场翻车提示以下错误:
skopeo: error while loading shared libraries: libdevmapper.so.1.02.1: cannot open shared object file: No such file or directory
  • 然后我傻乎乎地去安装 CentOS 上的这个库,但还是提示 libdevmapper.so.1.02.1 不存在。因为 Ubuntu 上的这个库和 CentOS 上的这个库是不一样名称的 😑。所以说要在编译的时候加上 DISABLE_CGO=1 这个参数进行静态链接编译,这样编译出来的二进制可执行文件就可以在 Linux 发行版之间通用了。
ldd skopeo_d #
        linux-vdso.so.1 (0x00007ffed9e66000)
        libgpgme.so.11 => /usr/lib/x86_64-linux-gnu/libgpgme.so.11 (0x00007f94aed2e000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f94aeb0f000)
        libdevmapper.so.1.02.1 => /lib/x86_64-linux-gnu/libdevmapper.so.1.02.1 (0x00007f94ae8a4000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f94ae4b3000)
        libassuan.so.0 => /usr/lib/x86_64-linux-gnu/libassuan.so.0 (0x00007f94ae2a0000)
        libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007f94ae08b000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f94b0ac4000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f94ade63000)
        libudev.so.1 => /lib/x86_64-linux-gnu/libudev.so.1 (0x00007f94adc45000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f94ad8a7000)
        libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f94ad635000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f94ad431000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f94ad229000)

# 加上 DISABLE_CGO=1 编译后的二进制可执行文件
ldd skopeo_s #
        not a dynamic executable

usage

Various operations with container images and container image registries

Usage:
  skopeo [command]

Available Commands:
  copy                      Copy an IMAGE-NAME from one location to another
  delete                    Delete image IMAGE-NAME
  help                      Help about any command
  inspect                   Inspect image IMAGE-NAME
  list-tags                 List tags in the transport/repository specified by the
  login                     Login to a container registry
  logout                    Logout of a container registry
  manifest-digest           Compute a manifest digest of a file
  standalone-sign           Create a signature using local files
  standalone-verify         Verify a signature using local files
  sync                      Synchronize one or more images from one location to another

skopeo version 1.0.0 commit: bd162028cd83ceecd8915736f2d66d5ca73ee54a

可以看到 skopeo 的功能很简单:

  • copy:复制一个镜像从 A 到 B,这里的 A 和 B 可以为本地 docker 镜像或者 registry 上的镜像。
  • inspect:查看一个镜像的 manifest 火车 image config 详细信息
  • delete:删除一个镜像,可以是本地 docker 镜像或者 registry 上的镜像
  • list-tags:列出一个 registry 上某个镜像的所有 tag
  • login:登录到某个 registry,和 docker login 类似
  • logout: 退出已经登录到某个 registry 的 auth 信息,和 docker logout 类似
  • manifest-digest、standalone-sign、standalone-verify 这三个用的不多
  • sync:同步一个镜像从 A 到 B,感觉和 copy 一样,但 sync 支持的参数更多,功能更强大。在 0.14.0 版本的时候是没有 sync 选项的,到了 0.14.2 才有,现在是 1.0.0

IMAGE NAMES

在使用 skopeo 之前,我们首先要知道在命令行中镜像的格式,下面是官方详细的文档格式。无论我们的 src 镜像还是 desc 镜像都要满足以下格式才可以。

Most commands refer to container images, using a transport : details format. The following formats are supported:

**containers-storage:**docker-reference An image located in a local containers/storage image store. Both the location and image store are specified in /etc/containers/storage.conf. (Backend for Podman, CRI-O, Buildah and friends)

**dir:**path An existing local directory path storing the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.

**docker://**docker-reference An image in a registry implementing the “Docker Registry HTTP API V2”. By default, uses the authorization state in either $XDG_RUNTIME_DIR/containers/auth.json, which is set using (skopeo login). If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using (docker login).

**docker-archive:path[:**docker-reference] An image is stored in the docker save formatted file. docker-reference is only used when creating such a file, and it must not contain a digest.

**docker-daemon:**docker-reference An image docker-reference stored in the docker daemon internal storage. docker-reference must contain either a tag or a digest. Alternatively, when reading images, the format can be docker-daemon:algo:digest (an image ID).

**oci:path:**tag An image tag in a directory compliant with “Open Container Image Layout Specification” at path.

需要注意的是,这几种镜像的名字,对应着镜像存在的方式,不同存在的方式对镜像的 layer 处理的方式也不一样,比如 docker:// 这种方式是存在 registry 上的,docker-daemon: 是存在本地 docker pull 下来的,再比如 docker-archive 是通过 docker save 出来的镜像。同一个镜像有这几种存在的方式就像水有气体、液体、固体一样。可以这样去理解,他们表述的都是同一个镜像,只不过是存在的方式不一样而已。

IMAGE NAMES example
containers-storage: containers-storage:
dir: dir:/PATH
docker:// docker://k8s.gcr.io/kube-apiserver:v1.17.5
docker-daemon: docker-daemon:alpine:latest
docker-archive: docker-archive:alpine.tar (docker save)
oci: oci:alpine:latest

skopeo copy

Copy an IMAGE-NAME from one location to another

注意一下,这里的 location 就是指的上面提到的 IMAGE NAMES ,也就是说 skopeo copy src dest 可以有 6*6=36 种组合!比如我可以将一个镜像从一个 registry 复制到另一个 registry,skopeo copy docker://IMAGE_NAME docker://IMAGE_NAME,再强调一遍,一定要注意 IMAGE_NAME 的命名的格式。

skopeo 的详细使用可以参考官方的文档,在使用之前先创建一个

在使用 skopeo 之前如果镜像是存放在 registry 上的话,需要先登录到 registry。使用 skopeo login 或者 docker login 都可以。成功登录之后会在本地保存一个为 config.json 的文件,里面保存了登录需要的验证信息,skopeo 拿到这个验证信息才有权限往 registry push 镜像。

╭─root@sg-02 /home/ubuntu/skopeo ‹master*›
╰─# jq "." ~/.docker/config.json
{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "d2sddaqWM7bSVlJFpmQE43Sw=="
    }
  },
  "HttpHeaders": {
    "User-Agent": "Docker-Client/19.03.5 (linux)"
  },
  "experimental": "enabled"
}
  • k8s.gcr.io/kube-apiserver:v1.17.5 复制镜像到 index.docker.io/webpsh/kube-apiserver:v1.17.5
╭─root@sg-02 ~/skopeo ‹master›
╰─# skopeo copy docker://k8s.gcr.io/kube-apiserver:v1.17.5 docker://index.docker.io/webpsh/kube-apiserver:v1.17.5 --dest-authfile /root/.docker/config.json
Getting image source signatures
Copying blob 597de8ba0c30 done
Copying blob e13a88fa950c done
Copying config f640481f6d done
Writing manifest to image destination
Storing signatures
  • skopeo 输出的日志显示是 Copying blob 597de8ba0c30 done ,可以看到 skopeo 是直接 copy 镜像 layer 的 blob,而 blob 是在 registry 进行压缩存储的格式。
# 然后从重新 pul 下来刚刚 push 到 docker hub 上的镜像,验证是否正确
╭─root@sg-02 ~/skopeo ‹master›
╰─# docker pull webpsh/kube-apiserver:v1.17.5
v1.17.5: Pulling from webpsh/kube-apiserver
Digest: sha256:5ddc5c77f52767f2f225a531a257259228d74b32d8aac9cfe087251f998c42f3
Status: Downloaded newer image for webpsh/kube-apiserver:v1.17.5
docker.io/webpsh/kube-apiserver:v1.17.5
  • copy 镜像到本地
╭─root@sg-02 /home/ubuntu
╰─# skopeo copy docker-daemon:alpine:latest oci:alpine
Getting image source signatures
Copying blob 3e207b409db3 done
Copying config af88fdb253 done
Writing manifest to image destination
Storing signatures
╭─root@sg-02 /home/ubuntu
╰─# tree -h alpine
alpine
├── [4.0K]  blobs
│   └── [4.0K]  sha256
│       ├── [ 348]  1c6f747c933450c5169f349f2a57b9d31e833c0452e1ec712b8aab0cbfea4d2c
│       ├── [2.8M]  3eee30c545e47333e6fe551863f6f29c3dcd850187ae3f37c606adb991444886
│       └── [ 583]  af88fdb253aac46693de7883c9c55244327908c77248d7654841503f744aae8b
├── [ 186]  index.json
└── [  31]  oci-layout

有点好奇这个镜像格式,所以我们来分析一下 copy 出来的镜像,可以看到在导出来的.

这个应该是镜像的 mainfaet 文件

╭─root@sg-02 /home/ubuntu/alpine/blobs/sha256
╰─# jq "." 1c6f747c933450c5169f349f2a57b9d31e833c0452e1ec712b8aab0cbfea4d2c
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:af88fdb253aac46693de7883c9c55244327908c77248d7654841503f744aae8b",
    "size": 583
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:3eee30c545e47333e6fe551863f6f29c3dcd850187ae3f37c606adb991444886",
      "size": 2898973
    }
  ]
}
  • 这个就是镜像的 image config 文件
╭─root@sg-02 /home/ubuntu/alpine/blobs/sha256
╰─# jq "." af88fdb253aac46693de7883c9c55244327908c77248d7654841503f744aae8b
{
  "created": "2020-04-24T01:05:03.92860976Z",
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "/bin/sh"
    ]
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a"
    ]
  },
  "history": [
    {
      "created": "2020-04-24T01:05:03.608058404Z",
      "created_by": "/bin/sh -c #(nop) ADD file:b91adb67b670d3a6ff9463e48b7def903ed516be66fc4282d22c53e41512be49 in / "
    },
    {
      "created": "2020-04-24T01:05:03.92860976Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
      "empty_layer": true
    }
  ]
}

skopeo inspect

这个命令可以查看一个镜像的 image config 和 mainf 文件,和 docker inspect 命令差不多


╭─root@sg-02 /home/ubuntu/alpine/blobs/sha256
╰─# skopeo inspect docker-daemon:alpine:latest --raw | jq "."
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 1507,
    "digest": "sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 5878784,
      "digest": "sha256:3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a"
    }
  ]
}

skopeo delete

使用这个命令可以删除镜像,对于删除 registry 上的镜像很有帮助,因为目前想要删除 registry 上的镜像常规的做法还是登录到 registry 在 WEB 上手动删除。skopeo delete 也是调用 registry 的 API 来进行删除镜像。

skopeo list-tags

这个命令常用来列出 registry 上的某个镜像的所有 tag ,在一些 shell 脚本中可能会又用得到。