容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。


一、skopeo:不依赖 Daemon 的镜像瑞士军刀

skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。

1.1 安装

# Ubuntu / Debian
sudo apt install -y skopeo

skopeo --version
# skopeo version 1.15.1

1.2 inspect:免拉取检查镜像元数据

docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry 查询,不占本地磁盘:

# 查看镜像基本信息(架构、操作系统、环境变量、入口命令等)
$ skopeo inspect docker://nginx:1.27-alpine
{
    "Name": "docker.io/library/nginx",
    "Digest": "sha256:c15da6c9...",
    "RepoTags": ["1.27-alpine", "stable-alpine", ...],
    "Created": "2025-03-15T10:23:44.123456789Z",
    "DockerVersion": "",
    "Labels": { "maintainer": "NGINX Docker Maintainers" },
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:1a7bfe56...",
        "sha256:92a4e81b...",
        "sha256:d3f9a04e..."
    ],
    "Env": [
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        "NGINX_VERSION=1.27.0"
    ]
}

# 查看原始 Manifest JSON(用于调试或脚本解析)
$ skopeo inspect --raw docker://nginx:1.27-alpine | python3 -m json.tool

# 查看私有 Registry 的镜像(同样免拉取)
$ skopeo inspect docker://registry.internal/myapp:v1.0

1.3 list-tags:列出仓库所有标签

$ skopeo list-tags docker://library/nginx
{
    "Repository": "docker.io/library/nginx",
    "Tags": [
        "1.27",
        "1.27-alpine",
        "1.27.0",
        "1.27.0-alpine",
        "latest",
        "stable",
        "stable-alpine",
        ...
    ]
}

# 配合 jq 筛选特定标签
$ skopeo list-tags docker://library/golang \
  | python3 -c "
import sys, json
tags = json.load(sys.stdin)['Tags']
print('\n'.join(t for t in tags if '1.22' in t and 'alpine' in t))
"
1.22-alpine
1.22.9-alpine
1.22.8-alpine

1.4 delete:删除 Registry 中的镜像

# 删除指定标签(需要 Registry 开启删除权限)
skopeo delete docker://registry.internal/myapp:old-tag

# 删除多架构镜像需要加 --all,否则只删除对应平台的 Manifest
skopeo delete --all docker://registry.internal/myapp:v0.9

注意:大多数 Registry(包括 Docker Hub)不允许通过 API 删除公共镜像标签;私有 Registry 通常需要在配置中显式开启删除功能(Harbor、Distribution Registry 均支持)。

1.5 sync:批量同步整个仓库

skopeo synccopy 的批量版,支持把整个仓库或一批指定版本一次性同步过去:

# 把 Docker Hub 上 nginx 仓库的所有标签同步到私有 Registry
skopeo sync \
  --src  docker \
  --dest docker \
  library/nginx \
  registry.internal/

# 更常用:用 YAML 文件精确控制要同步哪些镜像和版本
$ cat sync-list.yaml
docker.io:
  images:
    golang:
      - "1.22-alpine"
      - "1.22.9-alpine"
    alpine:
      - "3.21"
      - "3.20"

$ skopeo sync \
    --src  yaml \
    --dest docker \
    --dest-creds user:password \
    sync-list.yaml \
    registry.internal/

sync 有增量能力:如果目标端已经存在相同 digest 的镜像,会跳过,不重复传输。


二、crane:以 Registry 为操作对象的轻量工具

crane 由 Google 出品,定位是"直接操作 Registry 的工具",很多操作(打标签、查摘要、展平层)在 Registry 侧完成,不需要把镜像内容下载到本地。

2.1 安装

# 下载二进制
curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz \
  | tar xz -C /usr/local/bin crane

crane version
# v0.20.1

2.2 ls:列出仓库标签

$ crane ls nginx
1.27
1.27-alpine
1.27.0
latest
stable
...

# 支持私有 Registry(先 crane auth login)
$ crane auth login registry.internal -u user -p password
$ crane ls registry.internal/myapp
v1.0
v1.1
latest

2.3 digest:获取镜像摘要

# 获取当前平台的 Manifest 摘要
$ crane digest nginx:1.27-alpine
sha256:c15da6c91de8b17bc4e4e7b733b12c28b1a2a7b0fb14c97e67b7eac08d3f5e77

# 获取 Image Index 的摘要(多架构镜像)
$ crane digest --platform all nginx:1.27-alpine
sha256:a8b3f20d...

# 在 CI 中固定镜像版本,避免 latest 漂移
IMAGE_DIGEST=$(crane digest registry.internal/myapp:latest)
docker run registry.internal/myapp@${IMAGE_DIGEST}

2.4 tag:在 Registry 侧直接打标签

docker tag + docker push 需要把镜像拉到本地再推上去;crane tag 直接在 Registry 侧操作,纯 API 调用,几乎瞬间完成:

# 把 v1.0 在 Registry 里直接打上 latest 标签,无需下载任何数据
$ crane tag registry.internal/myapp:v1.0 registry.internal/myapp:latest

# 在 CI 中发布时常见用法:构建完推 Git commit SHA 标签,再打 latest
crane tag registry.internal/myapp:${GIT_SHA} registry.internal/myapp:latest

2.5 config:查看镜像配置

$ crane config nginx:1.27-alpine | python3 -m json.tool
{
    "architecture": "amd64",
    "config": {
        "Env": ["PATH=...", "NGINX_VERSION=1.27.0"],
        "Cmd": ["nginx", "-g", "daemon off;"],
        "ExposedPorts": { "80/tcp": {} }
    },
    "history": [
        { "created_by": "ADD alpine-minirootfs... # buildkit", ... },
        { "created_by": "RUN /bin/sh -c apk add --no-cache nginx", ... }
    ],
    "rootfs": {
        "type": "layers",
        "diff_ids": ["sha256:adb6...", "sha256:cc4d...", ...]
    }
}

2.6 mutate:修改镜像元数据

在不重新构建镜像的前提下,给已有镜像添加 label、修改环境变量或入口命令,所有修改在 Registry 侧完成:

# 添加 label(常用于标记构建信息)
crane mutate \
  --label [email protected] \
  --label build-date=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --label git-commit=${GIT_SHA} \
  registry.internal/myapp:v1.0

# 修改入口命令
crane mutate \
  --entrypoint /usr/local/bin/app \
  --cmd "--config=/etc/app/config.yaml" \
  registry.internal/myapp:v1.0

# 添加环境变量
crane mutate \
  --env APP_ENV=production \
  registry.internal/myapp:v1.0

2.7 flatten:展平镜像层

把镜像的所有层合并成单层,减小层数开销,有时可以降低镜像体积(合并后 whiteout 文件会被真正删除):

$ crane flatten \
    registry.internal/myapp:v1.0 \
    --tag registry.internal/myapp:v1.0-flat

# 对比展平前后的层数
$ docker image inspect registry.internal/myapp:v1.0 \
    --format '层数: {{len .RootFS.Layers}}'
层数: 6

$ docker image inspect registry.internal/myapp:v1.0-flat \
    --format '层数: {{len .RootFS.Layers}}'
层数: 1

2.8 export:导出镜像文件系统

# 导出镜像根文件系统为 tar,用于检查内容
$ crane export nginx:1.27-alpine - | tar t | grep "^etc/nginx"
etc/nginx/
etc/nginx/conf.d/
etc/nginx/conf.d/default.conf
etc/nginx/nginx.conf
...

# 提取单个文件
$ crane export nginx:1.27-alpine - | tar xO etc/nginx/nginx.conf

三、regctl:更强大的 Registry 客户端

regctl 来自 regclient 项目,在 skopeo 和 crane 的基础上提供了更丰富的功能,尤其在多架构镜像操作和 Tag 管理方面更为完善。

3.1 安装

curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 \
  -o /usr/local/bin/regctl && chmod +x /usr/local/bin/regctl

regctl version
# v0.7.1

3.2 基础操作

# 列出标签
$ regctl tag ls registry.internal/myapp
v1.0
v1.1
latest

# 删除标签(比 skopeo 更直接)
$ regctl tag delete registry.internal/myapp:old-tag

# 获取镜像摘要
$ regctl image digest registry.internal/myapp:v1.0
sha256:c7a2b31...

3.3 manifest:查看 Manifest 结构

regctl manifest get 对多架构镜像有很好的可视化支持:

# 查看 Image Index(自动格式化)
$ regctl manifest get registry.internal/myapp:v1.0
Name:      registry.internal/myapp:v1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:c7a2b31...
Manifests:
  Digest:    sha256:a1b2c3...
  MediaType: application/vnd.oci.image.manifest.v1+json
  Platform:  linux/amd64
  Digest:    sha256:d4e5f6...
  MediaType: application/vnd.oci.image.manifest.v1+json
  Platform:  linux/arm64

# 以树形显示(直观展示 Index → Manifest → Layer 层级)
$ regctl manifest get --format tree registry.internal/myapp:v1.0
registry.internal/myapp:v1.0
├── linux/amd64 sha256:a1b2c3...
│   ├── config sha256:e7f8a9...
│   ├── layer  sha256:b1c2d3...
│   └── layer  sha256:f4a5b6...
└── linux/arm64 sha256:d4e5f6...
    ├── config sha256:a2b3c4...
    ├── layer  sha256:c5d6e7...
    └── layer  sha256:f8a9b0...

3.4 image copy:平台过滤复制

regctl 的 image copy 支持在一条命令里过滤平台,比 skopeo 两步走更简洁:

# 只复制 amd64 和 arm64,生成新的 Image Index
$ regctl image copy \
    --platform linux/amd64,linux/arm64 \
    docker.io/library/golang:1.22-alpine \
    registry.internal/golang:1.22-alpine

# 复制单平台镜像(跳过 Image Index,只复制指定平台)
$ regctl image copy \
    --platform linux/amd64 \
    registry.internal/myapp:v1.0 \
    registry.internal/myapp:v1.0-amd64

3.5 速率限制检查

拉取 Docker Hub 镜像时经常遇到速率限制,regctl 提供了直接查询当前账号剩余配额的命令:

$ regctl registry ratelimit docker.io
Limit:     100
Remaining: 87
Reset:     2025-04-15T11:00:00Z

四、dive:可视化镜像层内容

第一篇已介绍过 dive 的基本用法(交互式查看每层的文件变化),这里补充它在 CI 环境中的自动化检查能力。

4.1 CI 模式:自动检查镜像效率

在构建流水线中,可以用 dive 的 CI 模式为镜像质量把关,一旦镜像效率低于阈值就让流水线失败:

# CI 模式:设置 CI=true 后 dive 不启动交互界面,直接输出报告并返回退出码
$ CI=true dive registry.internal/myapp:v1.0
  Using default CI config
Image Source: docker://registry.internal/myapp:v1.0
Fetching image... (this takes a while for large images)

  Results:
  PASS: highestUserWastedPercent   (threshold: 10%, actual: 2%)
  PASS: highestWastedBytes         (threshold: 20 MB, actual: 1.2 MB)
  PASS: lowestEfficiency           (threshold: 90%, actual: 97%)
  Result: PASS [Total test(s): 3]

可以通过 .dive-ci 文件调整阈值:

# .dive-ci
rules:
  lowestEfficiency: 0.95          # 镜像层利用率不低于 95%
  highestWastedBytes: "10MB"      # 浪费的空间不超过 10 MB
  highestUserWastedPercent: 0.05  # 用户层浪费比例不超过 5%
# 在 Makefile 或 CI 脚本中调用
$ CI=true dive --ci-config .dive-ci registry.internal/myapp:v1.0

4.2 常见浪费来源

dive 经常能发现这几类问题:

在同一个 RUN 指令之后没有清理包管理器缓存:

# ❌ 缓存留在镜像层里
RUN apt-get update && apt-get install -y curl

# ✓ 同一层清理
RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

在一个层里添加了文件,在另一个层里又删除:

# ❌ 第二层的 rm 只产生 whiteout 文件,第一层的数据依然存在于镜像里
COPY large-build-artifact.tar .
RUN tar xf large-build-artifact.tar && rm large-build-artifact.tar

多阶段构建(COPY --from=build)是解决这类问题的根本方案,避免把构建产物带入最终镜像。


五、cosign:镜像签名与验证

cosign 是 sigstore 项目的核心工具,用于对容器镜像进行加密签名,确保镜像从构建到部署的完整性——任何人都无法在不被发现的情况下篡改已签名的镜像内容。

5.1 安装

curl -Lo /usr/local/bin/cosign \
  https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x /usr/local/bin/cosign

cosign version
# GitVersion: v2.4.0

5.2 生成密钥对

# 生成非对称密钥对
$ cosign generate-key-pair
Enter password for private key:
Enter password for private key again:
Private key written to cosign.key
Public key written to cosign.pub

cosign.key 是私钥,用于签名(保存在 CI/CD 系统的 secret 里);cosign.pub 是公钥,用于验证(可以公开分发给所有消费方)。

5.3 对镜像签名

# 对已推送到 Registry 的镜像进行签名
$ cosign sign --key cosign.key registry.internal/myapp:v1.0

# cosign 会把签名作为一个独立的对象推送到同一个 Registry
# 签名引用格式:<registry>/<image>:<sha256-摘要>.sig

签名以独立的 artifact 存储在 Registry 里,不修改原始镜像的任何内容和摘要,可以用 crane ls 看到:

$ crane ls registry.internal/myapp | grep sha256
sha256-c7a2b31df4e8...cosign   # ← 这就是签名 artifact

5.4 验证签名

# 验证:确认镜像自签名后未被篡改
$ cosign verify --key cosign.pub registry.internal/myapp:v1.0

Verification for registry.internal/myapp:v1.0 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key

[{"critical":{"identity":{"docker-reference":"registry.internal/myapp"},
  "image":{"docker-manifest-digest":"sha256:c7a2b31..."},
  "type":"cosign container image signature"},
  "optional":null}]

验证失败(镜像内容被篡改或缺少签名)时,cosign 返回非零退出码,可以直接集成进部署流水线作为门禁。

5.5 在 CI 中的典型用法

# 构建阶段(CI 签名私钥从环境变量读取)
docker buildx build --push --tag registry.internal/myapp:${GIT_SHA} .
echo "${COSIGN_PRIVATE_KEY}" > /tmp/cosign.key
cosign sign --key /tmp/cosign.key registry.internal/myapp:${GIT_SHA}

# 部署阶段(用公钥验证,失败则阻止部署)
cosign verify --key /etc/deploy/cosign.pub registry.internal/myapp:${GIT_SHA} \
  || { echo "Image verification failed, aborting deploy"; exit 1; }

六、trivy:镜像漏洞扫描

trivy 是 Aqua Security 出品的开源漏洞扫描工具,可以扫描镜像里的操作系统包和应用依赖(npm、pip、Go modules 等)中的已知 CVE。

6.1 安装

sudo apt install -y trivy
# 或
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
  | sudo sh -s -- -b /usr/local/bin

trivy --version
# Version: 0.58.1

6.2 扫描镜像

# 扫描公共镜像(首次运行会下载漏洞数据库)
$ trivy image nginx:1.27-alpine

nginx:1.27-alpine (alpine 3.21.3)
Total: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

┌────────────────────┬───────────────┬──────────┬──────────┬────────────────────────┐
│      Library       │ Vulnerability │ Severity │  Status  │     Fixed Version      │
├────────────────────┼───────────────┼──────────┼──────────┼────────────────────────┤
│ libssl3            │ CVE-2024-xxxx │ MEDIUM   │ fixed    │ 3.3.2-r1               │
│ busybox            │ CVE-2023-xxxx │ LOW      │ affected │                        │
└────────────────────┴───────────────┴──────────┴──────────┴────────────────────────┘

# 只显示 HIGH 和 CRITICAL 级别(减少噪音)
$ trivy image --severity HIGH,CRITICAL nginx:1.27-alpine

# 扫描私有 Registry 的镜像
$ trivy image registry.internal/myapp:v1.0

6.3 扫描本地 tar 文件

# 配合第三篇的离线传输,扫描传输前的镜像
trivy image --input myapp.tar

6.4 集成进 CI 流水线

# 发现 HIGH 或 CRITICAL 时以非零退出码失败,阻断流水线
$ trivy image \
    --exit-code 1 \
    --severity HIGH,CRITICAL \
    --ignore-unfixed \       # 忽略尚无修复版本的漏洞(减少误报)
    registry.internal/myapp:v1.0

# 输出 JSON 格式供其他工具消费(如上传到安全平台)
$ trivy image \
    --format json \
    --output trivy-report.json \
    registry.internal/myapp:v1.0

七、工具选型速查

需求 推荐工具
免 daemon 复制镜像(含多架构) skopeo copy --all
批量同步多个镜像到私有 Registry skopeo sync
离线打包传输(保留 Image Index) skopeo copy oci-archive:
在 Registry 侧直接打标签 crane tag
列出仓库所有标签 crane ls / skopeo list-tags
查看镜像 Manifest 树形结构 regctl manifest get --format tree
按平台过滤复制多架构镜像 regctl image copy --platform / crane copy --platform
可视化分析镜像层浪费 dive
CI 中自动检查镜像体积效率 dive(CI 模式)
为镜像签名 / 验证签名 cosign
扫描镜像 CVE 漏洞 trivy

八、小结

这几个工具覆盖了镜像日常运维的几个维度:

搬运与同步——skopeo 和 crane 各有所长,skopeo 的 sync 适合批量运维,crane 的 tag/mutate 适合在 Registry 侧直接操作元数据;regctl 在多架构 Manifest 的可视化和精细控制上更胜一筹。

质量把关——dive 的 CI 模式可以自动拦截"层浪费严重"的镜像;trivy 扫描 CVE 漏洞,结合 --exit-code 1 可以直接作为流水线门禁。

供应链安全——cosign 对镜像做密码学签名,签名以独立 artifact 存储在同一 Registry,部署时验证签名可以确保镜像自构建以来未被篡改,是构建可信软件供应链的基础环节。

这四篇文章从 Docker 镜像的存储原理,到 containerd 的内部机制,再到多架构镜像的构建与分发,最后是日常工具的使用,构成了一套较为完整的镜像知识体系。

Read more

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd