K3s 集群中使用 Keel 自动更新容器镜像

11次阅读
没有评论

共计 6807 个字符,预计需要花费 18 分钟才能阅读完成。

用过 Docker Compose 的朋友大概都知道 [[Watchtower]] 这个工具,只要把它往 Docker 环境里一扔,它就会定时帮你检查容器镜像有没有新版本,发现更新之后自动拉取并重启容器,整个过程几乎不需要人工干预。我之前在单机 Docker 环境下用了很久的 Watchtower,一直觉得这种"设置一次就不用管"的体验非常舒服。但自从把服务迁移到 [[K3s]] 集群之后,这个问题就变得复杂了起来,因为 Kubernetes 的设计哲学和 Docker 完全不同,它更强调声明式管理,你不能简单地"拉取最新镜像然后重启",而是需要通过修改 Deployment 的 spec 来触发滚动更新。于是我花了一些时间研究在 K3s 集群里实现类似 Watchtower 自动更新效果的方案,最终选定了 [[Keel]] 这个工具,这里把整个调研和实践过程分享出来。

为什么 Kubernetes 里不能直接用 Watchtower

在 Docker 环境中,Watchtower 的工作原理非常直观:它连接到 Docker daemon,遍历所有运行中的容器,检查每个容器对应的镜像是否有更新,如果发现新版本就拉取下来并用相同的参数重新创建容器。这个逻辑简单粗暴但有效,因为 Docker 的容器管理模型就是"启动一个进程",没有太多抽象层。值得一提的是,Watchtower 的 GitHub 仓库已经在 2025 年 12 月被归档(archived),不再活跃维护了,所以即便是纯 Docker 用户,未来可能也需要寻找替代方案。

但在 Kubernetes 的世界里,事情就完全不一样了。K8s 通过 Deployment、StatefulSet、DaemonSet 这些控制器来管理 Pod,而 Pod 的 spec 一旦创建就是不可变的(immutable)。当你在 Deployment 里写了 image: myapp:1.0.0,即使你往 Registry 推送了一个新的 1.0.0 镜像(比如覆盖了同一个 tag),K8s 也不会自动去拉取新的镜像来替换正在运行的 Pod,因为从 K8s 的视角来看,spec 并没有发生变化。即便你把 imagePullPolicy 设成 Always,也只有在 Pod 被重新调度的时候才会拉取最新镜像,而不会主动触发滚动更新。这就是为什么在 K8s 环境下,你需要一个专门的工具来监控镜像仓库的变化,然后以 K8s 原生的方式(修改 Deployment spec 或触发 rollout)来完成更新。

几种方案的横向对比

在研究这个问题的过程中,我大致梳理了四种主流方案,每种都有自己适合的场景。

第一种是 Rancher 内置的 Fleet ImageScan。由于我的 K3s 集群是通过 [[Rancher]] 管理的,我最先想到的就是看看 Rancher 自己有没有原生方案。Fleet 确实提供了一个 imageScan 功能,可以在 fleet.yaml 中配置镜像扫描策略,检测到新 tag 之后自动提交到 Git 仓库,再由 Fleet 同步部署到集群。但经过调研之后我发现这个功能一直标记为 experimental,而且在最近的版本中已经开始走向弃用(deprecation),计划在 Fleet 1.0 中彻底移除。既然官方自己都要放弃了,那就没有必要在上面投入时间了。

第二种是 [[ArgoCD]] + ArgoCD Image Updater。这是目前社区里公认最成熟的 [[GitOps]] 方案,ArgoCD Image Updater 会定期检查镜像仓库,发现新版本后通过 Git 提交或 ArgoCD API 来更新应用。这个方案非常适合已经在用 ArgoCD 做 GitOps 的团队,但它有一个限制让我暂时搁置了它:ArgoCD Image Updater 主要支持通过 Helm values 或 Kustomize overlay 来管理的应用,对于我这种直接用纯 YAML manifest 部署的场景,配置起来会比较绕。而且引入 ArgoCD 本身也意味着一套相对重量级的基础设施,对于 homelab 级别的 K3s 集群来说有些大材小用。

第三种是在 CI/CD Pipeline 中直接更新。这个方案的思路很简单:在 GitHub Actions 或 GitLab CI 构建完新镜像之后,直接在 pipeline 里执行 kubectl set image 或者修改 manifest 文件中的镜像 tag 然后 kubectl apply。这种方式最可控,但它依赖你有一个完整的 CI/CD 流程,而且不是所有的镜像都是自己构建的,很多时候我们用的是第三方的开源项目镜像,它们的更新不在我的 CI/CD 控制范围内。

第四种就是今天的主角 Keel。它是一个专为 Kubernetes 设计的镜像自动更新 Operator,轻量、无状态、不需要数据库,通过 Deployment 上的 annotation 来控制更新策略。对于纯 YAML 部署的场景,Keel 几乎是开箱即用的,annotation 加上去就能工作,不需要改变你现有的部署方式。

把这几种方案放在一起看:

方案 复杂度 适合部署方式 自动化程度 维护状态
Fleet ImageScan Fleet 管理的应用 全自动 弃用中
ArgoCD Image Updater Helm / Kustomize 全自动 活跃
CI/CD Pipeline 任意 推送触发 取决于 CI 平台
Keel 纯 YAML / Helm 全自动 活跃

对于我这种在 K3s 上跑着一堆用纯 YAML 部署的自托管服务的场景,Keel 无疑是最合适的选择。

Keel 的工作原理

Keel 的核心逻辑非常简洁:它在集群里运行一个 Deployment,持续监控你指定的镜像仓库,当发现有符合更新策略的新版本时,自动修改对应的 Deployment(或 StatefulSet、DaemonSet)的镜像 tag,从而触发 Kubernetes 原生的滚动更新。整个过程中,Keel 本身是无状态的,不需要任何数据库或持久化存储,重启之后也能正常工作。

Keel 支持两种方式来检测新镜像。第一种是 Webhook 模式,也就是让镜像仓库主动通知 Keel,Docker Hub、Quay、Azure Container Registry、Google Container Registry 都支持配置 Webhook,当你推送新镜像时会自动触发。第二种是 Poll 轮询模式,Keel 定期去镜像仓库查询有没有新的 tag,默认间隔是 10 分钟,可以通过 annotation 自定义。对于 homelab 场景,轮询模式通常更简单,不需要额外配置 Webhook 回调地址和网络穿透。

Keel 提供了基于 Semantic Versioning 的更新策略,你可以精确控制它在什么级别的版本变化时才触发更新。patch 策略只接受补丁版本更新(比如 1.0.0 到 1.0.1),minor 策略接受次要版本更新(比如 1.0.0 到 1.1.0),major 则���受所有版本更新。还有一个 force 策略比较特殊,它不看版本号,只要镜像的 digest 发生了变化就触发更新,这对于使用 latest 标签的场景特别有用。此外还有 glob 策略支持通配符匹配,比如只跟踪 v1.* 开头的 tag。

安装和配置 Keel

Keel 的安装方式有两种,通过 Helm chart 或者直接 apply 原始 YAML。考虑到我们讨论的就是纯 YAML 部署场景,这里先介绍直接安装的方式:

kubectl apply -f https://sunstone.dev/keel?namespace=keel&username=admin&password=admin&tag=latest

当然你也可以用 Helm 来安装,这样配置项会更清晰:

helm repo add keel https://charts.keel.sh
helm repo update
helm upgrade --install keel --namespace keel --create-namespace keel/keel \
  --set helmProvider.enabled="false" \
  --set polling.enabled="true"

这里有两个参数值得注意。helmProvider.enabled 如果你不用 Helm 管理应用,可以设成 false 来减少不必要的资源消耗。polling.enabled 建议设成 true,这样 Keel 就会主动去轮询镜像仓库,不需要配置 Webhook。

安装完成之后,Keel 会在指定的 namespace 里创建一个 Deployment,你可以通过 kubectl get pods -n keel 确认它已经正常运行。

给 Deployment 添加更新策略

Keel 的配置完全通过 Kubernetes 原生的 annotation 来完成,不需要额外的 CRD 或配置文件。假设你有一个这样的 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: freshrss
  namespace: default
  annotations:
    keel.sh/policy: minor
    keel.sh/trigger: poll
    keel.sh/pollSchedule: "@every 5m"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: freshrss
  template:
    spec:
      containers:
        - name: freshrss
          image: freshrss/freshrss:1.24.1
          imagePullPolicy: Always

这里用到了三个核心 annotation。keel.sh/policy: minor 表示当 FreshRSS 发布了 1.25.0 或 1.24.2 这样的版本时自动更新,但不会跳到 2.0.0。keel.sh/trigger: poll 告诉 Keel 用轮询方式检查新版本,而不是等待 Webhook 通知。keel.sh/pollSchedule: "@every 5m" 设定了每 5 分钟检查一次,这个值可以根据你对实时性的要求来调整,检查频率越高对镜像仓库的请求就越多,Docker Hub 对免费用户有 API 速率限制,所以不建议设得太短。

对于那些使用 latest 标签的镜像,你需要把策略改成 force

annotations:
  keel.sh/policy: force
  keel.sh/trigger: poll
  keel.sh/pollSchedule: "@every 30m"
  keel.sh/match-tag: "true"

force 策略会比较镜像的 digest 而不是 tag 名称,只要 latest 指向的实际镜像内容发生了变化就会触发更新。keel.sh/match-tag: "true" 这个 annotation 确保 Keel 只关注和当前 tag 完全匹配的更新,避免意外拉取到其他 tag。

审批机制和通知

Keel 还有一个让我觉得设计得很用心的功能:审批机制(Approval)。如果你不想让 Keel 完全自动地更新某些关键服务,可以配置为需要人工审批:

annotations:
  keel.sh/policy: major
  keel.sh/approvals: "1"
  keel.sh/approvalDeadline: "24"

这表示 major 版本更新需要至少 1 个人审批通过才会执行,审批的有效期是 24 小时,超时后自动取消。审批可以通过 Keel 自带的 Web 管理界面、Slack 集成或者 HTTP API 来完成。对于 homelab 场景,我觉得 minor 策略配合自动更新就够了,不需要审批流程。但如果你在生产环境用 Keel,审批功能可以作为一道安全防线。

通知方面,Keel 支持 Slack、Email、Webhook 等多种渠道。我个人配置了 Slack 通知,每当 Keel 更新了一个镜像,我都会在 Slack 频道里收到一条消息,告诉我哪个 Deployment 的镜像从什么版本更新到了什么版本。这样即使是全自动更新,我也能保持对集群变化的感知。

实际使用中的一些坑

用了一段时间 Keel 之后,我踩过几个坑,这里提前和大家说一下。

第一个坑是关于 latest 标签的。前面提到过 K8s 对镜像 tag 没变的情况不会重新拉取,即使你设了 imagePullPolicy: Always,也只有在 Pod 重建的时候才会生效。Keel 的 force 策略解决的是"检测到 digest 变化后修改 Deployment 触发 rollout"这一步,但你仍然需要确保 imagePullPolicy 设成 Always,否则即使 Keel 触发了滚动更新,新的 Pod 也可能从节点本地缓存拉取到旧的镜像。所以最佳实践是尽量使用明确的版本号 tag,而不是 latest

第二个坑是私有镜像仓库的认证。如果你的镜像不在 Docker Hub 公开仓库,而是放在私有 Registry 或者 GitHub Container Registry 里,Keel 需要有对应的认证凭据才能轮询镜像信息。你需要在 Keel 运行的 namespace 里创建对应的 imagePullSecret,并在 Keel 的配置中引用它。这一步的文档写得不太清楚,我当时折腾了一会儿才搞定。

第三个坑是 Docker Hub 的 API 速率限制。Docker Hub 对匿名用户限制每 6 小时 100 次请求,对免费认证用户限制每 6 小时 200 次。如果你在集群里跑了很多服务,每个都配了 5 分钟轮询,很快就会触发限制。建议要么适当拉长轮询间隔(比如 30 分钟),要么给 Keel 配上 Docker Hub 的认证信息来获得更高的配额,或者考虑搭建一个镜像代理缓存。

关于 ArgoCD Image Updater

虽然我最终选择了 Keel,但 ArgoCD + ArgoCD Image Updater 这套方案确实值得单独拿出来说说。如果你的集群已经在用 [[ArgoCD]] 做 GitOps,那 ArgoCD Image Updater 绝对是更自然的选择,因为它和 ArgoCD 的集成是无缝的。

ArgoCD Image Updater 的配置方式是在 ArgoCD 的 Application 资源上添加 annotation:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  annotations:
    argocd-image-updater.argoproj.io/image-list: myapp=registry.example.com/myapp
    argocd-image-updater.argoproj.io/myapp.update-strategy: semver
    argocd-image-updater.argoproj.io/write-back-method: git

它支持两种 write-back 方式。git 方式会在你的 Git 仓库里自动创建 commit 来更新镜像 tag,完全符合 GitOps 的理念,所有的变更都有 Git 记录可追溯。argocd 方式则是直接通过 ArgoCD 的 API 修改 Application 的参数,立即生效但不会留下 Git 记录。从 GitOps 的最佳实践来说,git 方式更好,但它需要你给 Image Updater 配置 Git 仓库的写权限(SSH key 或 token),配置起来会多几步。

ArgoCD Image Updater 目前的限制是它主要支持 Helm 和 Kustomize 管理的应用。虽然技术上也可以通过 git write-back 方式来更新纯 YAML manifest 中的镜像引用,但官方文档对这种用法的描述不多,社区里也有一些关于边界情况的讨论。如果你的应用全部是通过 Helm chart 或 Kustomize 来管理的,ArgoCD Image Updater 是更优雅的选择;但如果像我一样大量使用纯 YAML,Keel 目前的体验更好。关于 ArgoCD 的完整实践,我打算后续单独写一篇文章来详细介绍。

最后

从 Docker 单机迁移到 Kubernetes 集群之后,很多在 Docker 时代习以为常的东西都需要重新找到对应的解决方案,镜像自动更新就是其中之一。Keel 给我的最大感受就是它把这件事做得足够简单,annotation 加上去就能工作,不需要引入复杂的 GitOps 工具链,也不需要改变现有的部署方式,这对于 homelab 和中小规模的自托管场景来说恰到好处。

当然,工具的选择永远取决于你的具体场景。如果你已经在用 ArgoCD,那就直接上 ArgoCD Image Updater;如果你有完善的 CI/CD Pipeline,那在 Pipeline 里直接更新镜像 tag 可能是最可控的;如果你像我一样用纯 YAML 部署一堆自托管服务,Keel 大概是目前最省心的选择。不管选哪个,核心思路都是一样的:在 Kubernetes 的声明式管理框架下,通过工具自动检测镜像变化并触发原生的滚动更新,让你从手动 kubectl apply 的重复劳动中解放出来。

正文完
 0
评论(没有评论)