本文发表已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已失效。

与 Kluctl 和 Server Side Apply 和睦共存

这篇博文的灵感来源于之前一篇关于高级服务端应用 (Advanced Server Side Apply) 的 Kubernetes 博文。该博文作者列出了切换到服务端应用(以下简称 SSA)对应用和控制器的诸多好处。特别是关于CI/CD 系统 的章节,激发了我回应并写下我的思考和经验。

这些思考和经验来自于我过去两年从事 Kluctl 项目的成果。我将 Kluctl 描述为“将由多个小部分(Helm/Kustomize/...)组成的大型 Kubernetes 部署以可管理且统一的方式整合起来的缺失的粘合剂。”

要对 Kluctl 有基本了解,建议访问 kluctl.io 网站并通读文档和教程,例如 微服务演示教程。或者,您可以观看 Rawkode Academy YouTube 频道上的 kluctl 动手入门 视频,其中展示了一个动手演示会话。

还有一个适用于 podtato-head 演示项目的 Kluctl 交付场景

和睦共存 (Live and let live)

Kluctl 遵循的主要理念之一是“和睦共存”(live and let live),这意味着它将尽力与集群外部或内部运行的任何其他工具或控制器协同工作。除非您明确告知,否则 Kluctl 不会覆盖它已失去所有权的字段。

没有 SSA 的使用,实现这一点将是不可能(或至少困难数倍)的。服务端应用允许 Kluctl 检测何时丢失了字段的所有权,例如当另一个控制器或 operator 将该字段更新为另一个值时。然后,Kluctl 可以根据这些判断逐个字段地决定是否需要强制应用后再重试。

SSA 之前的日子

Kluctl 的早期版本是基于调用 kubectl 的,因此隐式依赖于客户端应用 (client-side apply)。当时,SSA 仍然处于 Alpha 阶段,并且 Bug 很多。老实说,当时我甚至不知道它是什么。

客户端应用的工作方式存在一些严重的缺点。最明显的一点(如果时间够长,你肯定会遇到这个问题)是它依赖于对象上添加一个注解 (kubectl.kubernetes.io/last-applied-configuration),这带来了巨大注解值的所有限制和问题。此类问题的一个很好的例子是 CRD 变得如此之大,以至于它们不再适合注解的值。

另一个缺点仅从名字 (客户端应用) 就可以看出。作为客户端应用意味着每个客户端都必须自己提供应用逻辑,而当时这只在 kubectl 内部得到了正确实现,这使得它很难在控制器内部复制。

这使得所有希望利用应用逻辑的控制器都将 kubectl 添加为依赖项(无论是作为可执行文件还是 Go 包的形式)。

然而,即使有人设法在控制器内部运行客户端应用,最终得到的解决方案也无法控制其内部工作方式。例如,无法在外部更改发生时单独决定要覆盖哪些字段以及要放弃哪些字段。

发现 SSA 应用

我对上面描述的解决方案一直不满意,然后偶然发现了服务端应用,当时它还在 Beta 阶段。通过 kubectl apply --server-side 进行实验后立即发现,仅仅通过调用 kubectl 无法轻松利用 SSA 的真正强大之处。

SSA 在 kubectl 中的实现方式未能对冲突解决提供足够的控制,因为它只能在“不强制应用任何内容并报错”和“毫不留情地强制应用所有内容”之间切换。

然而,API 文档清楚地表明,SSA 能够通过简单地选择要在提供的对象中包含哪些字段和省略哪些字段来在字段级别控制冲突解决。

放弃 kubectl

这意味着 Kluctl 首先必须放弃调用 kubectl。只有完成这一步,我才能正确实现带有强大冲突解决能力的 SSA。

为了实现这一点,我首先通过 Kubernetes 客户端库实现了对目标集群的访问。这样做的一个额外好处是极大地提高了 Kluctl 的速度。通过确保正在运行的 Kluctl 命令不会因在运行时外部修改 kubeconfig 而受到干扰,这也提高了 Kluctl 的安全性和可用性。

实现 SSA

切换到 Kubernetes 客户端库后,利用 SSA 感觉很容易。Kluctl 现在必须将每个清单作为 PATCH 请求的一部分发送到 API 服务器,这表明 Kluctl 想要执行 SSA 操作。然后 API 服务器会返回一个 OK 响应(HTTP 状态码 200),或者一个 Conflict 响应(HTTP 状态码 409)。

在收到 Conflict 响应时,响应体中包含了关于冲突的机器可读详细信息。Kluctl 可以使用这些详细信息来找出哪些字段存在冲突以及哪些参与者(字段管理者)已拥有冲突字段的所有权。

然后,对于每个字段,Kluctl 将决定是否应该忽略冲突或者是否应该强制应用。如果任何字段需要被强制应用,Kluctl 将省略被忽略的字段,并在 API 调用中设置 force 标志后重试应用操作。

如果冲突被忽略,Kluctl 会向用户发出警告,以便用户能够做出适当的反应(或者永远忽略它...)。

基本上就是这样。这就是利用 SSA 所需的一切。非常感谢并为实现这一功能的 Kubernetes 开发者点赞!

冲突解决

Kluctl 有几个简单的规则来判断冲突是应该被忽略还是应该被强制应用。

它首先对照一个已知字段管理者字符串列表来检查字段的参与者(字段管理者),该列表包含常用手动修改工具的名称。例如 kubectlk9s。使用这些工具进行的任何修改都被视为“临时”的,并将被 Kluctl 覆盖。

如果您在与 kubectl 一起使用 Kluctl 时,不希望 kubectl 的更改被覆盖(例如,在脚本中使用),那么您可以在调用 kubectl 的命令行中指定 --field-manager=<manager-name>,这样 Kluctl 就不会应用其特殊的启发式规则。

如果 Kluctl 不认识该字段管理者,它将检查是否对该字段请求了强制应用。强制应用可以通过以下不同方式请求:

  1. 通过向 Kluctl 传递 --force-apply 参数。这将导致所有字段在冲突时都被强制应用。
  2. 通过在相关对象中添加 kluctl.io/force-apply=true 注解。这将导致该对象的所有字段在冲突时都被强制应用。
  3. 通过在相关对象中添加 kluctl.io/force-apply-field=my.json.path 注解。这将导致只有与 JSON 路径匹配的字段在冲突时被强制应用。

每当已知其他参与者错误地声称某些字段的所有权(例如 ECK operator 对 nodeSets 字段这样做)时,就需要将该字段标记为强制应用,以确保 Kluctl 始终将这些字段覆盖为原始值或新值。

未来,Kluctl 将允许对冲突解决进行更多控制。例如,CLI 将允许在字段级别控制强制应用。

DevOps 与控制器

那么 Kluctl 中的 SSA 如何实现“和睦共存”呢?

它允许经典流水线(例如 Github Actions 或 Gitlab CI)、控制器(例如 HPA 控制器或 GitOps 风格的控制器),甚至从本地机器运行部署的管理员共存。

无论您在基础设施自动化之旅的哪个阶段,Kluctl 都有适合您的位置。从使用 PC 上的脚本运行部署,到完全自动化的 CI/CD(流水线本身在代码中定义),Kluctl 旨在补充最适合您的工作流程。

即使一切都完全自动化后,如果需要,您仍然可以使用管理员权限进行干预,运行 kubectl 命令修改某个字段,从而阻止 Kluctl 覆盖它。您只需切换到一个 Kluctl 不会覆盖的字段管理者(例如 "admin-override")。

几点总结

服务端应用是一项很棒的特性,对于 Kubernetes 中控制器和工具的未来至关重要。涉及的控制器数量只会越来越多,因此必须具备合适的协同工作模式。

我认为与 CI/CD 相关的控制器和工具应该利用 SSA 进行适当的冲突解决。我也相信其他控制器(例如 Flux 和 ArgoCD)将受益于同样的字段级冲突解决控制能力。

甚至可以考虑联合起来,为 CI/CD 相关工具制定一套标准化的注解来控制冲突解决。

另一方面,非 CI/CD 相关的控制器在修改对象时应确保不会引起不必要的冲突。根据服务端应用文档,强烈建议控制器始终执行强制应用。在遵循此建议时,控制器应确实确保应用的对象中只包含与该控制器相关的字段。否则,必然会产生不必要的冲突。

在很多情况下,控制器旨在仅修改其管理对象的 status 子资源。在这种情况下,控制器应仅修补 status 子资源,而不触碰实际对象。如果遵循这一点,冲突将不可能发生。

如果你是这类控制器的开发者,并且不确定你的控制器是否遵循了上述原则,只需尝试检索你的控制器管理的一个对象,并查看 managedFields(你需要向 kubectl get 命令传递 --show-managed-fields -oyaml 参数),查看是否有某些字段被意外地声明了所有权。