为什么镜像可以 pull 下来但是在 manifest inspect 的时候提示 no such manifest?—— Docker Buildx Attestations 检修记
你有没有遇到过这种情况,对于一个镜像来说,它可以正常 pull 下来:
pad ~ # docker pull knatnetwork/github-runner-amd64:focal-2.301.1
focal-2.301.1: Pulling from knatnetwork/github-runner-amd64
846c0b181fff: Pull complete
588b3eef3b63: Pull complete
189ea0ac146f: Pull complete
4f4fb700ef54: Pull complete
546945707c6e: Pull complete
71464c2d54c9: Pull complete
1c4efc443e6a: Pull complete
21bbc223ea9a: Pull complete
Digest: sha256:6b5b4aa94f8c1e781785e831d18d7ccc1a0de7d70d63b1afd4df3cce27ddd53f
Status: Downloaded newer image for knatnetwork/github-runner-amd64:focal-2.301.1
docker.io/knatnetwork/github-runner-amd64:focal-2.301.1
但是如果你想 inspect 它的 manifest 会发现 no such manifest
。
pad ~ # docker manifest inspect knatnetwork/github-runner-amd64:focal-2.301.1
no such manifest: docker.io/knatnetwork/github-runner-amd64:focal-2.301.1
我怎么会遇到这么个鬼问题呢?
在 2022 年 4 月,我开源了 GitHub Runner, 相关的文章是: 开源 Github Actions Self-Hosted Runner,由于这个 Runner 的 Image 就是在 GitHub Actions 上面构建的,且为了提供多架构的支持(ARM64 和 AMD64) 并为了保证构建速度,整个构建工作分为了以下几步:
- 第一阶段同时开两个 Runner 分别构建
knatnetwork/github-runner-amd64:focal-2.301.1
和knatnetwork/github-runner-arm64:focal-2.301.1
的镜像 - 在上面两个 Runner 完成之后通过操作 manifest 的方式合并为一个叫
knatnetwork/github-runner:focal-2.301.1
的 Multi-Arch 镜像
这么做一直没有问题,直到几天前在最后一步合并镜像的时候遇到了第一个报错: https://github.com/knatnetwork/github-runner/actions/runs/3954481625/jobs/6776296661
failed to put manifest docker.io/knatnetwork/github-runner:focal-2.301.1: errors:
manifest blob unknown: blob unknown to registry
奇怪,难道是因为 GitHub 有一些 Step 没有升级么?
想到之前看到过一堆 The set-output command is deprecated and will be disabled soon.
,于是尝试升级了一下 docker/login-action
和 docker/build-push-action
等,然后重新触发任务,结果依然是在合并镜像的时候报错,不过这一次报错内容还不太一样,是:
Run docker manifest create knatnetwork/github-runner:focal-2.301.1 --amend knatnetwork/github-runner-amd64:focal-2.301.1 --amend knatnetwork/github-runner-arm64:focal-2.301.1
docker.io/knatnetwork/github-runner-amd64:focal-2.301.1 is a manifest list
基于个人的经验,如果同一段代码之前能跑,现在突然不能跑了,在这个情况下,一般有如下可能:
- GitHub Runner 环境的 Docker 版本变了
docker/login-action
和docker/build-push-action
中有什么变更,或者这些 step 使用的组件(比如 buildx)有啥变更- DockerHub/GHCR 出问题了
我们先排除最后一个可能,因为过了两天之后再重试发现问题依旧,且没有看到有大量对于这两个服务不可用的反馈,所以只剩下前两个可能。
GitHub Runner Docker
先看看是不是 Docker 有啥 Breaking change 导致的问题,最后一次成功的 Action 是: https://github.com/knatnetwork/github-runner/actions/runs/3736662591,调试信息中:
Client:
Version: 20.10.21+azure-2
API version: 1.41
Go version: go1.18.9
Git commit: baeda1f82a10204ec5708d5fbba130ad76cfee49
Built: Tue Oct 25 17:53:02 UTC 2022
OS/Arch: linux/amd64
Context: default
Experimental: true
Server:
Engine:
Version: 20.10.21+azure-2
API version: 1.41 (minimum version 1.12)
Go version: go1.18.9
Git commit: 3056208812eb5e792fa99736c9167d1e10f4ab49
Built: Tue Oct 25 11:44:15 2022
OS/Arch: linux/amd64
Experimental: false
第一次失败开始的 Action: https://github.com/knatnetwork/github-runner/actions/runs/3954481625/jobs/6776269393 ,调试信息中:
Client:
Version: 20.10.22+azure-1
API version: 1.41
Go version: go1.18.9
Git commit: 3a2c30b63ab20acfcc3f3550ea756a0561655a77
Built: Thu Dec 15 15:37:38 UTC 2022
OS/Arch: linux/amd64
Context: default
Experimental: true
Server:
Engine:
Version: 20.10.22+azure-1
API version: 1.41 (minimum version 1.12)
Go version: go1.18.9
Git commit: 42c8b314993e5eb3cc2776da0bbe41d5eb4b707b
Built: Thu Dec 15 22:17:04 2022
OS/Arch: linux/amd64
Experimental: false
看上去确实有一些版本升级,不过阅读了 https://docs.docker.com/engine/release-notes/#201022 之后发现基本只有点 Patch ,没有什么足以引起这种问题的更新。
那么现在压力就来到了第二个,即 「docker/login-action
和 docker/build-push-action
中有什么变更,或者这些 step 使用的组件(比如 buildx)有啥变更」。
Manifest
在继续调查前我们先看一下上面的报错是个什么情况,为什么镜像能拉,但是 manifest 看不了,难道拉镜像之前不需要看 manifest 么?
Docker 用来查看 manifest 的指令是 docker manifest inspect
,但是这个指令没有类似用于调试的 -v
的选项,所以如果看到了 no such manifest
,那你也没法知道背后出了啥问题,不过考虑到 manifest 就一个 JSON 文件,所以肯定是有 Docker Hub 的 API 可以查询的,于是立即上网梭了一个脚本出来:
#!/bin/sh
ref="${1:-library/ubuntu:latest}"
sha="${ref#*@}"
if [ "$sha" = "$ref" ]; then
sha=""
fi
wosha="${ref%%@*}"
repo="${wosha%:*}"
tag="${wosha##*:}"
if [ "$tag" = "$wosha" ]; then
tag="latest"
fi
api="application/vnd.docker.distribution.manifest.v2+json"
apil="application/vnd.docker.distribution.manifest.list.v2+json"
token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
| jq -r '.token')
curl -H "Accept: ${api}" -H "Accept: ${apil}" \
-H "Authorization: Bearer $token" \
-s "https://registry-1.docker.io/v2/${repo}/manifests/${sha:-$tag}" | jq .
来源:https://stackoverflow.com/questions/57316115/get-manifest-of-a-public-docker-image-hosted-on-docker-hub-using-the-docker-regi
然后找了个正常的镜像试了一下,输出结果类似是这样的,和用 docker manifest inspect
结果一致:
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:19bf2d0d0a8aaf27988db772ff6ba4044405447535762bfc9ba451d0d84f0a18",
"size": 4995
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:846c0b181fff0c667d9444f8378e8fcfa13116da8d308bf21673f7e4bea8d580",
"size": 28576882
},
...
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:74b36662af5e651ae3390a6cf13fcaa8fca08fea5bd711ddbed60bf9e5924654",
"size": 932
}
]
}
于是立即看了一下有问题的镜像,结果是这样的:
{
"errors": [
{
"code": "MANIFEST_UNKNOWN",
"message": "OCI index found, but accept header does not support OCI indexes"
}
]
}
从 OCI Image Index Specification 文档中我们知道 manifest 有很多类型,大家一般在用的是 application/vnd.docker.distribution.manifest.v2+json
,如果是一个 multi-arch 的镜像的话可能输出结果是这样的:
{
"manifests": [
{
"digest": "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "amd64",
"os": "linux"
},
"size": 528
},
{
"digest": "sha256:176bc6c6e93528f4b729fae1f8dbd70b73861264dba3a3f64c49c92e1f42a5aa",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "s390x",
"os": "linux"
},
"size": 528
}
],
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"schemaVersion": 2
}
这里它的格式是 application/vnd.docker.distribution.manifest.list.v2+json
,也就是上面脚本中请求的时候同时带上了以下两个 header 的原因。
api="application/vnd.docker.distribution.manifest.v2+json"
apil="application/vnd.docker.distribution.manifest.list.v2+json"
但是这里根据提示 OCI index found
,我们猜测可能实际的 manifest 格式和上面两个都不匹配,于是加入了以下两个新的 Header 上去,显式定义一下我们还接受 application/vnd.oci.image.index.v1+json
这个格式:
api_old="application/vnd.oci.image.manifest.v1+json"
api_oldi="application/vnd.oci.image.index.v1+json"
很快,我们就看到有问题的镜像也能返回了,数据是这样的:
{
"mediaType": "application/vnd.oci.image.index.v1+json",
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:73809677ff2aff4bee611f1da7cdc9b8825c5729d2aab4c88b683cfa0e5fc7f0",
"size": 1817,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:f47cf60d8b8da4e0f5040071b78ddb41f0ae160da6b1be7ddcba03a5c0bf9b3d",
"size": 567,
"annotations": {
"vnd.docker.reference.digest": "sha256:73809677ff2aff4bee611f1da7cdc9b8825c5729d2aab4c88b683cfa0e5fc7f0",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}
这就很有意思了,我用:
- name: Build and push AMD64 Version
uses: docker/build-push-action@v2
with:
context: ./amd64/
file: ./amd64/Dockerfile
platforms: linux/amd64
push: true
tags: |
knatnetwork/github-runner-amd64:focal-${{ github.event.inputs.github-runner-version }}
构建出来的镜像为什么 manifests 是个数组(像是一个 multi-arch 的镜像),而且第二个 platform 还是 unknown?
所以应该也是这个原因导致了: docker.io/knatnetwork/github-runner-amd64:focal-2.301.1 is a manifest list
这个报错, 操作 manifest 合并镜像不能把两个多 Arch 镜像合并。
但为什么?
attestation manifest
在上文的输出中我们看到了一个关键信息: "vnd.docker.reference.type": "attestation-manifest"
,经过搜索看到了这个文档: Attestation storage | Docker Documentation
Buildkit supports creating and attaching attestations to build artifacts. These attestations can provide valuable information from the build process, including, but not limited to: SBOMs, SLSA Provenance, build logs, etc.
哦?是 Buildkit 搞的事情?
于是开始检查最后一次成功的 Buildx 版本,发现是:
github.com/docker/buildx 0.9.1+azure-2 ed00243a0ce2a0aee75311b06e32d33b44729689
再看看第一次失败的 Buildx 版本:
github.com/docker/buildx 0.10.0+azure-1 876462897612d36679153c3414f7689626251501
版本从 0.9.1 升级到了 0.10.0 ,这个时候回顾一下 docker/build-push-action
的 Release Note 中有这么一段话:
Buildx v0.10 enables support for a minimal SLSA Provenance attestation, which requires support for OCI-compliant multi-platform images. This may introduce issues with registry and runtime support (e.g. GCR and Lambda). You can optionally disable the default provenance attestation functionality using provenance: false.
很快我们就知道这里的问题在于 Buildx 从 0.10 开始就默认加入了这个叫做 SLSA Provenance attestation
的东西,也就是我们看到的 manifest 中底下那个 "vnd.docker.reference.type": "attestation-manifest"
的内容,这么做对于直接构建的 Multi-Arch 镜像没有影响,对于单架构镜像而言一般也没有影响(虽然会在 docker manifest inspect
的时候报错),但是一旦有了像我这样多个并行构建,后期操作 manifest 的合并的操作的时候,就会导致 docker.io/knatnetwork/github-runner-amd64:focal-2.301.1 is a manifest list
类似这样的错误。
如果你想了解更多关于 Build attestations 的事情,可以从 Docker 的文档: Build attestations | Docker Documentation 开始阅读,简单来说分为 SBOM 和 Provenance:
Build attestations describe how an image was built, and what it contains. The attestations are created at build-time by BuildKit, and become attached to the final image as metadata.
Two types of build annotations are available:
Software Bill of Material (SBOM): list of software artifacts that an image contains, or that were used to build the image.
Provenance: how an image was built.
既然问题很清晰了,那解决问题的思路也明确了,在 docker/build-push-action
加入以下两行即可:
provenance: false
sbom: false
构建后我们再次通过脚本确认,发现 manifest 已经正常,如下:
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:82da6a4f14803932bfece329e5d2592b74dbbb65a3c493bb6b459fb8b3a082ff",
"size": 4995
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:846c0b181fff0c667d9444f8378e8fcfa13116da8d308bf21673f7e4bea8d580",
"size": 28576882
},
...
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:8b5ad40966565f7a972b30cf9494aa3600645350952d99f1d442c143a03d2650",
"size": 932
}
]
}
而至于一开始遇到的 manifest blob unknown: blob unknown to registry
问题,猜测是由于合并镜像需要在一个 repo 下,逻辑应该是:
knatnetwork/github-runner-amd64:latest
和knatnetwork/github-runner-arm64:latest
不能合并knatnetwork/github-runner:latest-amd64
和knatnetwork/github-runner:latest-arm64
可以合并
不过这里似乎也没法解释为什么之前这么做是可以的,如果有读者有了解的话,欢迎在评论区中指出。
Summary
总结,为了解决上面两个问题,我分别做了以下调整:
- 在
docker/build-push-action
中显式禁用了 provenance 和 sbom 的输出 - 将 amd64 和 arm64 的镜像改变同 repo 的不同 tag 输出
同时得出一个结论就是:如果你和我一样想后期操作 manifest 来调整镜像的话,一定要注意 buildx 的这个新特性,要么显式禁用掉,要么考虑修改你的 Dockerfile 们尽量一次通过 buildx 构建成 Multi-Arch 的镜像。