Menci's Blog
念念不忘,必有回响
Kubernetes 是如何合并 YAML 配置的?
  1. 1. 复盘
  2. 2. 原因
  3. 3. 解决
  4. 4. 参考

今天在更新一个以 StatefulSet 形式部署的服务时出现了一个诡异的问题:一个从 YAML 文件中被删掉的 env 环境变量,在应用 YAML 后的 StatefulSet 对象中仍然存在,导致 Pod 中的应用程序没有正常运行。

而出现问题的 StatefulSet 资源,唯一的不寻常之处是 —— 上次更新(apply -f)后进行了回滚(rollout undo)……

复盘

首先,我们有一个 StatefulSet,它运行得很正常:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-wonderhoy-app
  labels:
    app: my-wonderhoy-app
spec:
  serviceName: my-wonderhoy-app
  replicas: 3
  selector:
    matchLabels:
      app: my-wonderhoy-app
  template:
    metadata:
      labels:
        app: my-wonderhoy-app
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        env:
        - name: TO_BE_DELETED
          value: needs to be deleted!
        - name: ANOTHER_ENV
          value: who cares?

我们想要部署一个新版,进行了一些代码改动,发了一个新的 Docker 映像,并且不再需要其中的某一个 env

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-wonderhoy-app
  labels:
    app: my-wonderhoy-app
spec:
  serviceName: my-wonderhoy-app
  replicas: 3
  selector:
    matchLabels:
      app: my-wonderhoy-app
  template:
    metadata:
      labels:
        app: my-wonderhoy-app
    spec:
      containers:
      - name: nginx
        # 总之这个 image 不工作
        image: nginx:a-broken-version
        env:
        # 这个被删掉了!
        # - name: TO_BE_DELETED
        #   value: needs to be deleted!
        - name: ANOTHER_ENV
          value: who cares?

然后,我们发现这个新版的 Docker 映像不工作,于是回滚到上一个版本:

# 列出历史版本,找到上一个工作的版本
kubectl rollout history statefulset.apps/my-wonderhoy-app
REVISION  CHANGE-CAUSE
1         kubectl apply -f my-wonderhoy-app-v1.yaml
2         kubectl apply -f my-wonderhoy-app-v2.yaml
# 回滚到所需的版本
kubectl rollout undo statefulset.apps/my-wonderhoy-app --to-revision=1
statefulset.apps/my-wonderhoy-app rolled back

回滚后,我们的服务回到了正常工作的状态,它使用上一个版本的 Docker 映像,并且存在着被上一个版本所需要的 TO_BE_DELETED 变量。

而当我们修复了 Docker 映像的问题,再次部署新版时,我们发现 TO_BE_DELETED 这个变量并没有被删除:

kubectl apply -f my-wonderhoy-app-v3.yaml
statefulset.apps/my-wonderhoy-app configured
kubectl get statefulset.apps/my-wonderhoy-app -o jsonpath='{.spec.template.spec.containers[0].env}'
[map[name:TO_BE_DELETED value:needs to be deleted!] map[name:ANOTHER_ENV value:who cares?]]

原因

这个问题是由于 kubectl apply 的合并策略导致的。kubectl apply 并不是简单地用新的 YAML 文件来替换掉集群中现有资源的配置,而是会遵循一个策略来进行合并。对于 env 的成员,Kubernetes 认为它是一种 Key-Value 结构,会对每个 Key 计算合并后的 Value。为了进行 Kubernetes 所认为的「正确」的合并,它对每个资源维护了一个 last-applied-configuration 字段,记录了上次使用 kubectl apply 时为它应用的 YAML 配置,而在本次 kubectl apply 中,它的合并策略是,对于一个 Key:

  • 如果该 Key 存在于新的 YAML 配置中,那么直接使用新的 Value;
  • 如果该 Key 不存在于新的 YAML 配置中:
    • 如果该 Key 存在于上次应用的 YAML 配置中,Kubernetes 会认为这个 Key 应当被删除;
    • 如果该 Key 不存在于上次应用的 YAML 配置中,Kubernetes 会认为这个 Key 是被通过 YAML 文件之外的方式被添加的,不应当被删除。

这个策略在大多数情况下是没有问题的,我们(在非调试用途下)一般不会通过 YAML 文件之外的方式来修改资源,所以 last-applied-configuration 一般会运行中的配置一致,使得 Kubernetes 能够正确地判断出哪些 Key 需要被删除。但这次巧合的是:

  1. 首先,我们应用了一次(有 Bug 的)新 YAML 文件,这时 last-applied-configuration 中已经没有要删除的 TO_BE_DELETED 这个 Key 了;
  2. 然后,我们发现 Bug 之后回滚到了上一个版本,但 last-applied-configuration 没有被回滚,它之中仍然没有 TO_BE_DELETED 这个 Key;
    • 此时,工作中的该资源 statefulset.apps/my-wonderhoy-app 的配置就与 last-applied-configuration 不一致了;
  3. 最后,重新应用(修复了 Bug 的)新 YAML 文件后,Kubernetes 的合并策略会认为 TO_BE_DELETED 是被通过 YAML 文件之外的方式添加的,不应当被删除。

解决

搞明白整个问题的流程之后,它就不可怕了。要解决它,只需要手动删掉这个没有被自动删掉的 env 即可:

kubectl edit statefulset.apps/my-wonderhoy-app # 编辑 YAML 文件,删除掉不需要的 env
statefulset.apps/my-wonderhoy-app edited

这个问题在一直使用 kubectl apply 的环境中不会出现,所以我们会误认为 kubectl apply 是一个简单的替换操作,但在手动修改了资源之后,就必须要考虑这个合并策略的问题了。

参考