把镜像拆开往里看
本实验在真正的 VM 中运行
这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行,systemd 真正管理服务,docker 也不是模拟工具,而是真正的 Docker 引擎。通过 docker run 启动的容器会成为实际进程,docker exec 和 docker logs 也都能照常工作。
过去,这个实验在 Pod 中运行。由于该环境放弃了所有内核权限,启动容器的步骤无法进行,因此只能学习直接解开镜像归档的变通方法。现在不再需要绕路了。
有两点需要了解。
- **首次启动大约需要 1 分钟。**因为 VM 需要启动并安装 Docker,比 Pod 实验(通常 40 秒)更慢。
- **没有浏览器预览。**进入 VM 的连接只开放评分端口。如果启动了 Web 服务器,请在 VM 内使用
curl检查。
目标
将镜像导出为归档,亲自打开其中的清单和 config blob,确认 RootFS.Layers(diff_id)与清单中的层摘要属于不同层次的值。最后,通过比较摘要证明由同一基础镜像构建的两个镜像共享第一层。
为什么重要
镜像事故大多源于“相信了标签”。标签是人为赋予的名称,随时可以改为指向其他摘要;摘要则是内容的哈希,相同哈希保证内容相同。亲手确认这一区别后,就会清楚部署流水线中应固定什么。此外,清单中的层摘要是压缩内容的哈希,config 的 rootfs.diff_ids 则是解压后 tar 的哈希。不了解两者不同,就会因“同一个镜像为何哈希不同”的误解浪费时间。因此,本实验会分别从不同文件中取出这两个值。
步骤
- 创建
/root/int3目录,将alpine:3.20镜像的 ID(内容哈希)保存到/root/int3/image-id.txt。它必须是 64 位十六进制数,可以带有sha256:前缀。 - 将
nginx:1.27-alpine的 rootfs 层摘要每行一个保存到/root/int3/layers.txt。每一行都必须以sha256:开头,后接 64 位字符,行数必须等于实际层数。 - 将
alpine:3.20镜像导出为归档并保存到/root/int3/alpine.tar。由于docker save在这个环境中无法运行,请使用skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20——作用相同。归档中必须包含manifest.json(或index.json/oci-layout)以及以哈希命名的 blob。 - 从该归档中取出
manifest.json,保存为/root/int3/manifest.json。它必须是有效 JSON,并至少包含一个层。 - 从归档中取出第 4 步清单的
Config字段所指向的 config blob 文件,保存为/root/int3/config.json。文件中必须有architecture,rootfs.type必须为layers,rootfs.diff_ids至少包含一项。 - 使用
skopeo查询本地镜像存储中的nginx:1.27-alpine信息,并保存到/root/int3/skopeo.json。存储起初为空,因此请先使用skopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpine导入——podman load在这个环境中受阻,但 skopeo 不创建命名空间,因此可以直接工作。Name中必须包含 nginx,Digest必须采用sha256:格式,Layers至少包含一项。 - 以
alpine:3.20为基础,分别叠加一层不同内容,构建并标记为labhub/oci:a和labhub/oci:b。两个镜像的第一层摘要必须相同,最后一层摘要必须不同。 - 在
/root/int3/oci.md中严格按以下格式写四行,全部以nginx:1.27-alpine为准。layer_count=<레이어 개수>architecture=<아키텍처>os=<운영체제>first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>
参考
- 使用
docker image inspect <이미지> | jq -r '.[0].Id'读取镜像 ID。 - 将数组展开为逐行输出时,使用
jq -r '.[0].RootFS.Layers[]'形式。 - 从归档中只取出一个文件时,使用
tar xf <아카이브> -C <디렉터리> <파일이름>。 - skopeo 使用
skopeo inspect <전송>:<이미지>格式,本地存储对应的传输是containers-storage:。 - 常见错误 1:第 3 步使用
docker export时,只会得到一份容器文件系统,不包含清单。 - 常见错误 2:第 5 步直接复制清单时,其中没有
architecture,因此会失败。必须取出Config指向的另一个文件。 - 常见错误 3:第 7 步若向两个镜像写入相同内容,所有层都会相同,无法比较。
镜像 ID 就是内容哈希
创建 /root/int3 目录,将 alpine:3.20 镜像的 ID(内容哈希)保存到 /root/int3/image-id.txt。它必须是 64 位十六进制数,可以带有 sha256: 前缀。
该值位于 docker image inspect 的 Id 字段。可以有或没有 sha256: 前缀,但后面的十六进制数必须正好为 64 位。本步骤保存的是哈希,而不是标签名称。
提取层摘要列表
将 nginx:1.27-alpine 的 rootfs 层摘要每行一个保存到 /root/int3/layers.txt。每一行都必须以 sha256: 开头,后接 64 位字符,行数必须等于实际层数。
将 RootFS.Layers 数组展开为每行一项。用 jq 展开数组时不会带引号。行数必须与实际层数完全相同,不得混入空行或表头。
将镜像导出为归档
将 alpine:3.20 镜像导出为归档并保存到 /root/int3/alpine.tar。由于 docker save 在这个环境中无法运行,请使用 skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20——作用相同。归档中必须包含 manifest.json(或 index.json/oci-layout)以及以哈希命名的 blob。
导出一份容器文件系统的命令,与导出包含层、标签和历史记录的镜像的命令不同。必须使用后者,归档中才会包含清单。
打开归档中的清单
从该归档中取出 manifest.json,保存为 /root/int3/manifest.json。它必须是有效 JSON,并至少包含一个层。
可以从 tar 归档中只提取某个文件。不要手工编造 JSON,请原样取出归档中的文件。提取的文件必须是有效 JSON,并包含层列表。
config blob 与 diff_ids
从归档中取出第 4 步清单的 Config 字段所指向的 config blob 文件,保存为 /root/int3/config.json。文件中必须有 architecture,rootfs.type 必须为 layers,rootfs.diff_ids 至少包含一项。
清单中的 Config 字段是归档内另一个文件的名称。必须再次取出该文件,才能得到含有 architecture 和 rootfs.diff_ids 的真正 config。复制清单本身会失败。
使用注册表工具查询清单
使用 skopeo 查询本地镜像存储中的 nginx:1.27-alpine 信息,并保存到 /root/int3/skopeo.json。存储起初为空,因此请先使用 skopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpine 导入——podman load 在这个环境中受阻,但 skopeo 不创建命名空间,因此可以直接工作。Name 中必须包含 nginx,Digest 必须采用 sha256: 格式,Layers 至少包含一项。
此环境离线,无法访问远程注册表。skopeo 通过传输(transport)前缀选择目标;使用指向本地镜像存储的传输,即可在无网络时查询。输出必须同时包含 Name、Digest、Layers。
证明基础层被共享
以 alpine:3.20 为基础,分别叠加一层不同内容,构建并标记为 labhub/oci:a 和 labhub/oci:b。两个镜像的第一层摘要必须相同,最后一层摘要必须不同。
两个镜像必须从相同基础镜像开始,并各自叠加一层不同内容。基础镜像不同会导致第一层不同,内容相同又会让最后一层也相同,两种情况都会失败。
汇总 OCI 元数据
在 /root/int3/oci.md 中严格按以下格式写四行,全部以 nginx:1.27-alpine 为准。
layer_count=<레이어 개수>architecture=<아키텍처>os=<운영체제>first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>
四行都采用 키=값 格式,不得混入空格或引号。first_layer12 是从第一层摘要中去掉 sha256: 后的前 12 个字符。全部以 nginx 镜像为准。