1. 项目概述:一个“无爪”的容器镜像仓库清理工具

在容器化部署成为主流的今天,镜像仓库的管理与维护是每个运维和开发团队都无法绕开的日常。镜像越积越多,存储空间告急,安全漏洞镜像需要清理,手动操作不仅繁琐,还极易出错。最近在社区里看到一个名为 cduf938-alt/clawless 的项目,这个名字很有意思,“clawless”直译是“无爪的”,听起来像是一个温和、不会造成破坏的工具。深入探究后,我发现它确实是一个专注于容器镜像仓库清理的实用工具,旨在用更精准、更安全的方式,帮助我们从庞大的镜像仓库中“修剪”掉不需要的旧镜像、测试镜像或特定标签,而不会“抓伤”你的生产环境。

简单来说, clawless 就是一个命令行工具,它允许你根据一系列灵活的规则(比如保留最近N个版本、匹配特定名称模式、排除某些关键标签等),对私有或公共的容器镜像仓库执行清理操作。它支持 Docker Registry V2 API,这意味着它可以与 Harbor、GitLab Container Registry、Google Container Registry (GCR)、Amazon ECR 以及标准的私有 Docker Registry 等主流仓库协同工作。它的核心价值在于将清理策略代码化、自动化,避免了人工在界面上点点点可能带来的误删风险,特别适合集成到 CI/CD 流水线中,作为资源治理的一环。

2. 核心设计思路:策略驱动与安全优先

2.1 为何需要专门的清理工具?

你可能觉得,清理镜像不就是用 API 列出来再删掉吗?写个脚本也能搞定。确实如此,但一个健壮的清理工具需要考虑的远不止于此。首先, 安全性 是重中之重。误删了生产环境正在使用的镜像,可能导致服务无法重启或回滚,这是灾难性的。其次, 灵活性 ,不同项目、不同环境的清理策略可能完全不同。有的需要按时间保留,有的需要按版本号(如保留所有 v1.x 但删除 v2.x 的测试版),有的则需要排除像 latest stable 这样的特殊标签。最后, 可观测性与可逆性 ,在执行删除操作前,必须清晰地知道哪些镜像会被影响,最好能有“模拟运行”(dry-run)模式,并且操作应该有清晰的日志记录。

clawless 的设计正是围绕这些痛点展开。它采用“策略驱动”的模式,你将清理规则定义在一个配置文件(如 YAML)中,工具根据配置进行计算和操作。这种声明式的方式,使得策略可以版本化、可评审、可复用。其“无爪”的理念体现在:默认情况下,它应该是安全且非破坏性的,通过详尽的预览和确认步骤来防止误操作。

2.2 核心工作流程解析

clawless 的工作流程可以清晰地分为几个阶段,理解这个流程对正确使用它至关重要:

  1. 认证与连接 :工具首先需要获取目标镜像仓库的访问凭证。这通常通过 Docker 的认证机制(如 ~/.docker/config.json )或直接提供用户名、密码来完成。它支持 Bearer Token、Basic Auth 等多种方式,以适应不同的仓库配置。

  2. 策略加载与解析 :用户提供一个策略文件。这个文件定义了“在哪里清理”(仓库地址、项目名)、“清理什么”(镜像名称模式)以及“如何清理”(保留规则、排除规则)。工具会解析这个文件,形成内部的可执行策略对象。

  3. 清单获取与候选集生成 :工具调用仓库的 Catalog API 和 Manifest API,列出所有符合“清理什么”条件的镜像及其标签。此时,它获取到的是一个完整的候选镜像列表。

  4. 策略应用与过滤 :这是核心逻辑所在。工具将上一步得到的候选集,逐条应用策略中定义的规则。例如,“保留最近7天推送的镜像”、“保留标签匹配 ^prod-\d+\.\d+\.\d+$ 的镜像”、“排除名为 latest main 的标签”。经过层层过滤,最终会得出一份“待删除镜像/标签清单”。

  5. 模拟运行与确认 :在真正执行删除前, clawless 会进入“模拟运行”模式。在此模式下,它会输出完整的分析报告:总共扫描了多少镜像,哪些符合策略将被保留,哪些被标记为删除。用户必须审阅这份报告。通常,工具会要求用户进行二次确认(如交互式输入 yes 或提供 --confirm 参数)才会继续。

  6. 执行清理与后处理 :确认后,工具开始调用仓库的删除 API(Docker Registry V2 的 DELETE 接口)来移除镜像的 Manifest 和关联的 Blob 层。删除过程中,它会记录日志。删除完成后,一些高级仓库(如 Harbor)可能还需要调用垃圾回收(GC)接口才能真正释放磁盘空间, clawless 也可能集成或提示这一步骤。

注意 :镜像仓库的删除操作通常是异步且需要垃圾回收的。删除一个标签只是删除了对该镜像 Manifest 的引用。只有当该 Manifest 及其下的所有 Blob 层没有任何其他标签引用时,它们在垃圾回收后才会被物理删除。这意味着,即使你删除了一个标签,如果同一个镜像的另一个标签还在引用这些层,磁盘空间并不会立即释放。

3. 策略文件深度解析与配置实战

clawless 的强大和灵活,几乎完全体现在其策略文件的配置上。一份典型的策略文件(例如 policy.yaml )结构如下,我们来逐部分拆解:

# policy.yaml 示例
version: v1
rules:
  - name: "cleanup-old-feature-branches"
    registry: "harbor.mycompany.com"
    repository: "myproject/myapp"
    tagFilter: # 标签过滤器,定义“清理什么”
      pattern: "^feature/.*" # 匹配所有以 feature/ 开头的标签
    keep: # 保留规则,定义“如何清理”
      mostRecentlyPushed: 5 # 保留最近推送的5个匹配标签
    dryRun: true # 首次运行设为true进行模拟

  - name: "keep-major-versions"
    registry: "gcr.io"
    repository: "my-gcp-project/backend-service"
    tagFilter:
      pattern: "^v\d+\.\d+\.\d+$" # 匹配语义化版本标签,如 v1.2.3
    keep:
      tags: # 使用复杂的标签匹配规则进行保留
        - pattern: "^v[1-9]\.\d+\.\d+$" # 保留所有主版本为1-9的标签
        - pattern: "^v10\.\d+\.\d+$" # 保留所有主版本为10的标签
    # 不设置 mostRecentlyPushed 或 count,则匹配上述pattern的标签全部保留,其余删除

3.1 核心字段详解

  1. registry repository :这两个字段定位了你要操作的目标。 registry 是仓库服务器地址, repository 是项目/命名空间下的具体镜像库。它们共同构成了镜像的完整前缀,如 harbor.mycompany.com/myproject/myapp

  2. tagFilter.pattern :这是一个正则表达式,用于从仓库中筛选出你关心的那部分标签。这是第一层过滤。例如, ^feature/.* 只处理特性分支构建的镜像; ^.*-test$ 只处理以 -test 结尾的标签。 务必小心正则表达式的编写 ,过于宽泛的模式(如 .* )可能会匹配到所有标签,包括你不希望触碰的生产标签。

  3. keep 规则 :这是策略的核心,定义了保留逻辑。 clawless 通常提供多种保留规则,它们会按顺序或优先级应用:

    • mostRecentlyPushed: N :按镜像推送时间排序,保留最新的 N 个。这是最常用、最直观的基于时间的清理策略。
    • mostRecentlyPulled: N :按镜像拉取时间排序,保留最近被拉取过的 N 个。这更适合清理那些“无人使用”的冷镜像,但需要仓库支持拉取日志,并非所有仓库都提供此API。
    • tags: [ {pattern: “...”}, … ] :提供一个模式列表,任何匹配其中任一模式的标签都会被 保留 。这用于“保护”特定标签,如 latest stable v1.* 等。
    • excludeTags: [ {pattern: “...”}, … ] :提供一个模式列表,任何匹配其中任一模式的标签都会被 排除 (即不删除)。它通常与更宽泛的 tagFilter 结合使用,进行细粒度控制。

    实操心得 keep.tags keep.excludeTags 的逻辑容易混淆。我的经验是: tags 是“白名单”,明确告诉工具“这些我要留”; excludeTags 是“黑名单中的例外”,告诉工具“虽然它符合被删除的条件,但这个除外”。在复杂策略中,建议先用 dryRun 模式验证结果。

3.2 多规则执行与优先级

一个策略文件可以包含多个 rules clawless 会按顺序执行这些规则。这意味着你可以为同一个仓库的不同标签组设置不同的策略。例如,第一条规则清理 feature/* 标签,保留最近5个;第二条规则清理 release/* 标签,保留最近10个;第三条规则保护所有 prod-* 标签不被删除。

规则优先级需要特别注意 :如果两个规则的 tagFilter 重叠了,后执行的规则可能会对已经被前一条规则处理过的镜像再次进行操作。通常,更具体、保护性的规则应该放在前面,更宽泛、清理性的规则放在后面。例如,先写规则保护 latest prod ,再写规则去清理旧的 test 标签。

4. 完整实操流程:从零开始使用 Clawless

假设我们有一个 Harbor 仓库 ( harbor.example.com ),里面有一个项目 devops/nginx ,积累了大量的每日构建 ( daily-2024-* ) 和特性分支 ( feature/* ) 标签,我们需要定期自动化清理。

4.1 环境准备与工具安装

首先,你需要有访问目标仓库的权限。确保你的机器上可以通过 docker login harbor.example.com 成功登录。凭证会保存在 ~/.docker/config.json 中, clawless 默认会使用这个凭证。

clawless 通常以单文件二进制发布。我们前往项目的 GitHub Releases 页面,找到适合你操作系统(Linux/macOS/Windows)的版本下载并安装。

# 以 Linux amd64 为例
wget https://github.com/cduf938-alt/clawless/releases/download/v0.1.0/clawless-linux-amd64
chmod +x clawless-linux-amd64
sudo mv clawless-linux-amd64 /usr/local/bin/clawless
# 验证安装
clawless --version

4.2 编写清理策略

创建文件 cleanup_policy.yaml

version: v1
rules:
  # 规则1:保护关键标签,绝对不允许删除
  - name: "protect-critical-tags"
    registry: "harbor.example.com"
    repository: "devops/nginx"
    tagFilter:
      pattern: "^(latest|stable|v1\\.0\\.0)$" # 保护 latest, stable, v1.0.0
    keep:
      tags:
        - pattern: ".*" # 匹配所有,即全部保留。也可以使用 `mostRecentlyPushed: all`
    dryRun: false # 实际执行时,此规则用于保护,dryRun不影响其保护意图

  # 规则2:清理旧的每日构建镜像,保留最近7天
  - name: "cleanup-old-daily-builds"
    registry: "harbor.example.com"
    repository: "devops/nginx"
    tagFilter:
      pattern: "^daily-2024-\\d{2}-\\d{2}$" # 匹配 daily-2024-01-01 格式
    keep:
      mostRecentlyPushed: 7 # 保留最近7个推送的每日构建
    dryRun: true # 首次先模拟运行!

  # 规则3:清理特性分支镜像,保留最近3个
  - name: "cleanup-old-feature-branches"
    registry: "harbor.example.com"
    repository: "devops/nginx"
    tagFilter:
      pattern: "^feature/.*"
    keep:
      mostRecentlyPushed: 3
    dryRun: true

4.3 执行模拟运行与人工审核

在真正删除前, 必须进行模拟运行 。这能让你清晰看到工具将要执行的操作。

clawless apply -f cleanup_policy.yaml

由于我们在策略文件中将后两条规则的 dryRun 设为了 true ,工具会输出详细的报告,而不会执行删除。报告通常会包含:

  • 扫描到的总标签数。
  • 每条规则匹配到的标签列表。
  • 根据 keep 规则计算后,计划保留的标签。
  • 计划删除的标签 。这是你需要重点审查的部分!

仔细检查这份列表,确认没有误包含重要标签。特别是检查 tagFilter.pattern 的正则表达式是否精确匹配了你的预期。

4.4 确认并执行清理

审核模拟运行报告无误后,修改策略文件,将 dryRun: true 改为 dryRun: false 。或者,更安全也更常见的做法是,在命令行中使用 --confirm --dry-run=false 参数来覆盖文件中的设置。

# 方法一:修改yaml文件后执行
clawless apply -f cleanup_policy.yaml

# 方法二:使用命令行参数强制确认(更推荐,避免误改文件)
clawless apply -f cleanup_policy.yaml --confirm

执行后,工具会开始调用仓库API进行删除。你会看到删除过程的日志。请注意,如前所述,删除操作在仓库侧可能是逻辑删除,需要等待垃圾回收才能释放物理空间。对于 Harbor,你可能需要手动或在脚本中调用 Harbor API 触发垃圾回收。

4.5 集成到 CI/CD 流水线

自动化是 clawless 的最大价值所在。你可以将其集成到 Jenkins、GitLab CI 或 GitHub Actions 中,定期(例如每周日凌晨)执行清理任务。

以下是一个 GitHub Actions 工作流示例 .github/workflows/cleanup-images.yml

name: Cleanup Container Images

on:
  schedule:
    - cron: '0 2 * * 0' # 每周日凌晨2点运行
  workflow_dispatch: # 允许手动触发

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Log in to Harbor
        run: |
          echo ${{ secrets.HARBOR_PASSWORD }} | docker login harbor.example.com -u ${{ secrets.HARBOR_USERNAME }} --password-stdin

      - name: Download Clawless
        run: |
          wget -q https://github.com/cduf938-alt/clawless/releases/download/v0.1.0/clawless-linux-amd64
          chmod +x clawless-linux-amd64
          sudo mv clawless-linux-amd64 /usr/local/bin/clawless

      - name: Run Dry-Run First
        run: |
          clawless apply -f ./cleanup_policy.yaml
        continue-on-error: true # 模拟运行失败也不终止工作流,便于查看报告

      - name: Confirm and Apply Cleanup
        if: github.event_name == 'workflow_dispatch' # 仅在手動触发时执行真实删除
        run: |
          clawless apply -f ./cleanup_policy.yaml --confirm

这个工作流做了几件关键事:1) 定时触发;2) 先进行模拟运行并输出报告(即使失败也继续);3) 只有手动触发工作流时,才执行真正的删除操作 。这是一种“审批门控”机制,自动化报告,人工决策执行,在安全与效率间取得平衡。

5. 常见问题、排查技巧与高级用法

5.1 权限问题:401 Unauthorized 或 403 Forbidden

这是最常见的问题。 clawless 需要足够的权限来列出和删除镜像。

  • 症状 :执行时提示 failed to fetch catalog: unauthorized denied: requested access to the resource is denied
  • 排查
    1. 确认 docker login 是否成功, ~/.docker/config.json 中是否有对应 registry 的 auth token。
    2. 确认使用的账号是否有目标 repository 的 推送和删除权限 。在 Harbor 中,至少需要“项目管理员”或拥有“删除镜像”权限的角色。
    3. 如果仓库使用 Robot Account(机器人账户),确保其权限范围正确。
  • 解决 :使用具备足够权限的账号登录。对于 CI/CD 环境,使用安全的 Secret 管理方式存储凭证。

5.2 模式匹配错误:误删或漏删

正则表达式编写错误会导致清理范围不符合预期。

  • 症状 :模拟运行报告显示,计划删除的列表里出现了本应保留的标签,或者该被清理的标签没出现。
  • 排查
    1. 使用在线正则表达式测试器 (如 regex101.com)反复验证你的 pattern 。注意 YAML 中需要对反斜杠 \ 进行转义( \\d )。
    2. 在策略中先使用 dryRun: true mostRecentlyPushed: 999 (一个很大的数)这样的配置,运行一次,查看你的 tagFilter.pattern 到底匹配到了哪些标签。这是一个非常有效的调试手段。
  • 解决 :修正正则表达式。对于复杂的匹配逻辑,考虑拆分成多条更简单的规则。

5.3 垃圾回收未运行:磁盘空间未释放

  • 症状 clawless 执行成功,日志显示标签已删除,但仓库管理界面显示磁盘使用率没有下降。
  • 原因 :这是正常现象。Docker Registry 的删除是引用计数式的。只有触发垃圾回收后,未被任何 Manifest 引用的 Blob 层才会被物理删除。
  • 解决
    1. 对于 Harbor :可以通过 Harbor UI(系统管理 -> 垃圾回收)手动触发,或调用 Harbor API POST /api/v2.0/system/gc 来触发。可以在 clawless 执行后,在 CI/CD 脚本中增加这一步。
    2. 对于自建 Registry :需要登录到 Registry 容器内执行 docker exec registry registry garbage-collect /etc/docker/registry/config.yml 注意 :垃圾回收期间,Registry 可能会变为只读或影响性能,请在低峰期操作。

5.4 处理大量镜像时的性能与超时

  • 症状 :仓库内镜像数量巨大(数万标签), clawless 执行缓慢甚至超时。
  • 排查与解决
    1. 优化策略 :使用更精确的 tagFilter.pattern 缩小每次处理的范围。不要用一个规则匹配所有标签,可以按项目、前缀分多次作业。
    2. 分页处理 :查看 clawless 是否支持分页参数,或者其使用的 Registry API 本身是否有分页限制。可以尝试调整。
    3. 网络与超时设置 :检查工具是否有连接超时、请求超时的配置项,适当调大。
    4. 增量清理 :改为更频繁地执行清理(如每天),每次只清理一小部分,而不是积累数月后一次性处理。

5.5 高级用法:基于 Manifest 的深度清理

默认情况下, clawless 基于标签进行清理。但多个标签可能指向同一个 Manifest(即同一个镜像ID)。如果你删除了一个标签,但该镜像的其他标签还存在,那么底层 Blob 并不会被清理。有些场景下,我们想清理的是“未被任何标签引用的镜像层”。

更高级的用法是结合仓库的垃圾回收机制和 clawless 的标签清理。首先,用 clawless 安全地删除所有不需要的标签。然后,运行仓库的垃圾回收。一些更专业的工具(如 Harbor 自带的功能)可以直接分析存储并列出“孤立的”Blob,但这通常超出了 clawless 这类标签管理工具的范围。 clawless 的核心定位是安全、策略化的 标签生命周期管理 ,而非底层的存储空间分析。

通过将 clawless 这样的工具纳入你的运维体系,容器镜像的治理就从一项高风险、随意的体力活,转变为了一个可预测、可审计、自动化的标准流程。它就像一位细心且可靠的园丁,定期为你修剪枝蔓,让镜像仓库这座花园始终保持整洁、高效,为你的容器化应用提供坚实的后勤保障。

更多推荐