本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

与 Kluctl 和服务器端应用共存

这篇博文的灵感来自之前的一篇关于高级服务器端应用(Advanced Server Side Apply)的 Kubernetes 博客文章。该博文的作者列出了应用程序和控制器在切换到服务器端应用(下文简称 SSA)时的多个好处。特别是关于 CI/CD 系统的章节,激励我写下我的想法和经验作为回应。

这些想法和经验是我过去两年在 Kluctl 项目上工作的结果。我将 Kluctl 描述为“将由多个较小部分(Helm/Kustomize/...)组成的大型 Kubernetes 部署以可管理和统一的方式组合在一起的缺失的粘合剂”。

为了对 Kluctl 有一个基本的了解,我建议访问 kluctl.io 网站并阅读文档和教程,例如微服务演示教程。作为替代方案,你可以观看 Rawkode Academy YouTube 频道的《Kluctl 实战介绍》,其中展示了一个动手演示环节。

还有一个针对 podtato-head 演示项目的 Kluctl 交付场景

互不干扰

Kluctl 遵循的主要哲学之一是“互不干扰”,这意味着它会尽最大努力与在集群内外运行的任何其他工具或控制器协同工作。Kluctl 不会覆盖任何它已失去所有权的字段,除非你明确指示它这样做。

如果不使用 SSA,实现这一点是不可能的(或者至少要困难几个数量级)。服务器端应用允许 Kluctl 检测字段的所有权何时丢失,例如当另一个控制器或 Operator 将该字段更新为另一个值时。然后,Kluctl 可以在字段级别上决定是否需要在基于这些决策重试之前强制应用。

SSA 之前的日子

Kluctl 的最初版本是基于调用 kubectl 来实现的,因此隐式地依赖于客户端应用。那时,SSA 仍处于 alpha 阶段且相当不稳定。老实说,我当时甚至不知道有这么个东西。

客户端应用的工作方式有一些严重的缺点。最明显的一个(如果你有足够的时间,你肯定会自己遇到这个问题)是它依赖于一个被添加到对象上的注解(kubectl.kubernetes.io/last-applied-configuration),这带来了所有与巨大注解值相关的限制和问题。这类问题的一个很好的例子是CRD 太大,以至于它们无法再放入注解的值中。

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

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

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

发现 SSA apply

我一直对上述解决方案不满意,然后不知何故偶然发现了服务器端应用,当时它仍处于 beta 阶段。通过 kubectl apply --server-side 对其进行实验后,我立即发现,通过调用 kubectl 无法轻易利用 SSA 的真正威力。

kubectl 中 SSA 的实现方式不允许对冲突解决进行足够的控制,因为它只能在“不强制应用任何东西并报错”和“不留情面地强制应用所有东西!”之间切换。

然而,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 覆盖。

如果你将 Kluctl 与 kubectl 一起使用,且不希望 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),看看是否有某个字段被意外声明了所有权。