LabHub
学习 学习路径 课程

容器内部原理

把镜像拆开往里看

在 LabHub 中继续学习

本实验在真正的 VM 中运行

这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行,systemd 真正管理服务,docker 也不是模拟工具,而是真正的 Docker 引擎。通过 docker run 启动的容器会成为实际进程,docker execdocker logs 也都能照常工作。

过去,这个实验在 Pod 中运行。由于该环境放弃了所有内核权限,启动容器的步骤无法进行,因此只能学习直接解开镜像归档的变通方法。现在不再需要绕路了。

有两点需要了解。

目标

将镜像导出为归档,亲自打开其中的清单和 config blob,确认 RootFS.Layers(diff_id)与清单中的层摘要属于不同层次的值。最后,通过比较摘要证明由同一基础镜像构建的两个镜像共享第一层。

为什么重要

镜像事故大多源于“相信了标签”。标签是人为赋予的名称,随时可以改为指向其他摘要;摘要则是内容的哈希,相同哈希保证内容相同。亲手确认这一区别后,就会清楚部署流水线中应固定什么。此外,清单中的层摘要是压缩内容的哈希,config 的 rootfs.diff_ids 则是解压后 tar 的哈希。不了解两者不同,就会因“同一个镜像为何哈希不同”的误解浪费时间。因此,本实验会分别从不同文件中取出这两个值。

步骤

  1. 创建 /root/int3 目录,将 alpine:3.20 镜像的 ID(内容哈希)保存到 /root/int3/image-id.txt。它必须是 64 位十六进制数,可以带有 sha256: 前缀。
  2. nginx:1.27-alpine 的 rootfs 层摘要每行一个保存到 /root/int3/layers.txt。每一行都必须以 sha256: 开头,后接 64 位字符,行数必须等于实际层数。
  3. 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。
  4. 从该归档中取出 manifest.json,保存为 /root/int3/manifest.json。它必须是有效 JSON,并至少包含一个层。
  5. 从归档中取出第 4 步清单的 Config 字段所指向的 config blob 文件,保存为 /root/int3/config.json。文件中必须有 architecturerootfs.type 必须为 layersrootfs.diff_ids 至少包含一项。
  6. 使用 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 至少包含一项。
  7. alpine:3.20 为基础,分别叠加一层不同内容,构建并标记为 labhub/oci:alabhub/oci:b。两个镜像的第一层摘要必须相同,最后一层摘要必须不同
  8. /root/int3/oci.md 中严格按以下格式写四行,全部以 nginx:1.27-alpine 为准。
    • layer_count=<레이어 개수>
    • architecture=<아키텍처>
    • os=<운영체제>
    • first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>

参考

镜像 ID 就是内容哈希

创建 /root/int3 目录,将 alpine:3.20 镜像的 ID(内容哈希)保存到 /root/int3/image-id.txt。它必须是 64 位十六进制数,可以带有 sha256: 前缀。

该值位于 docker image inspectId 字段。可以有或没有 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。文件中必须有 architecturerootfs.type 必须为 layersrootfs.diff_ids 至少包含一项。

清单中的 Config 字段是归档内另一个文件的名称。必须再次取出该文件,才能得到含有 architecturerootfs.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)前缀选择目标;使用指向本地镜像存储的传输,即可在无网络时查询。输出必须同时包含 NameDigestLayers

证明基础层被共享

alpine:3.20 为基础,分别叠加一层不同内容,构建并标记为 labhub/oci:alabhub/oci:b。两个镜像的第一层摘要必须相同,最后一层摘要必须不同

两个镜像必须从相同基础镜像开始,并各自叠加一层不同内容。基础镜像不同会导致第一层不同,内容相同又会让最后一层也相同,两种情况都会失败。

汇总 OCI 元数据

/root/int3/oci.md 中严格按以下格式写四行,全部以 nginx:1.27-alpine 为准。

四行都采用 키=값 格式,不得混入空格或引号。first_layer12 是从第一层摘要中去掉 sha256: 后的前 12 个字符。全部以 nginx 镜像为准。