apply 如何计算差异并合并更改

定义:patch 是一种更新操作,它的作用范围是对象的特定字段而不是整个对象。这允许只更新对象上的一组特定字段,而无需先读取对象。
当 kubectl apply 更新对象的实时配置时,它通过向 API 服务器发送 patch 程序请求来实现此目的。该 patch 将更新范围定义为活动对象配置的特定字段。kubectl apply 命令使用配置文件,实时配置和存储在实时配置中的 last-applied-configuration 注释来计算此 patch 请求。

合并 patch 计算

kubectl apply 命令将配置文件的内容写入 kubectl.kubernetes.io/last-applied-configuration 注释。这用于标识已从配置文件中删除并需要从实时配置中清除的字段。以下是用于计算应删除或设置哪些字段的步骤:

  1. 计算要删除的字段。这些是 last-applied-configuration中出现的字段,配置文件中缺少这些字段。
  2. 计算要添加或设置的字段。这些是配置文件中存在的字段,其值与实时配置不匹配。

这是一个例子。假设这是一个 Deployment 对象的配置文件:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: nginx-deployment
  5. spec:
  6. selector:
  7. matchLabels:
  8. app: nginx
  9. template:
  10. metadata:
  11. labels:
  12. app: nginx
  13. spec:
  14. containers:
  15. - name: nginx
  16. image: nginx:1.11.9 # update the image
  17. ports:
  18. - containerPort: 80

同样,假设这是用于同一个 Deployment 对象的实时配置:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. annotations:
  5. # ...
  6. # note that the annotation does not contain replicas
  7. # because it was not updated through apply
  8. kubectl.kubernetes.io/last-applied-configuration: |
  9. {"apiVersion":"apps/v1","kind":"Deployment",
  10. "metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
  11. "spec":{"minReadySeconds":5,"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
  12. "spec":{"containers":[{"image":"nginx:1.7.9","name":"nginx",
  13. "ports":[{"containerPort":80}]}]}}}}
  14. # ...
  15. spec:
  16. replicas: 2 # written by scale
  17. # ...
  18. minReadySeconds: 5
  19. selector:
  20. matchLabels:
  21. # ...
  22. app: nginx
  23. template:
  24. metadata:
  25. # ...
  26. labels:
  27. app: nginx
  28. spec:
  29. containers:
  30. - image: nginx:1.7.9
  31. # ...
  32. name: nginx
  33. ports:
  34. - containerPort: 80
  35. # ...

以下是 kubectl apply 将执行的合并计算:

  1. 通过读取 last-applied-configuration 中的值并将它们与配置文件中的值进行比较来计算要删除的字段。示例中,minReadySeconds 显示在 last-applied-configuration 中,但未出现在配置文件中。操作:清除实时配置中的 minReadySeconds。
  2. 通过读取配置文件中的值并将它们与实时配置中的值进行比较来计算要设置的字段。示例中,配置文件中 image 的值与实时配置中的值不匹配。操作:在实时配置中设置 image 的值。
  3. 设置 last-applied-configuration 注释以匹配配置文件的值。
  4. 将前面 3 个步骤的结果合并到 API 服务器的单个 patch 请求中。

以下是合并后的实时配置:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. annotations:
  5. # ...
  6. # The annotation contains the updated image to nginx 1.11.9,
  7. # but does not contain the updated replicas to 2
  8. kubectl.kubernetes.io/last-applied-configuration: |
  9. {"apiVersion":"apps/v1","kind":"Deployment",
  10. "metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
  11. "spec":{"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
  12. "spec":{"containers":[{"image":"nginx:1.11.9","name":"nginx",
  13. "ports":[{"containerPort":80}]}]}}}}
  14. # ...
  15. spec:
  16. selector:
  17. matchLabels:
  18. # ...
  19. app: nginx
  20. replicas: 2 # Set by `kubectl scale`. Ignored by `kubectl apply`.
  21. # minReadySeconds cleared by `kubectl apply`
  22. # ...
  23. template:
  24. metadata:
  25. # ...
  26. labels:
  27. app: nginx
  28. spec:
  29. containers:
  30. - image: nginx:1.11.9 # Set by `kubectl apply`
  31. # ...
  32. name: nginx
  33. ports:
  34. - containerPort: 80
  35. # ...
  36. # ...
  37. # ...
  38. # ...

如何合并不同类型的字段

配置文件中的特定字段如何与实时配置合并取决于字段的类型。有几种类型的字段:

  • primitive:字符串、整数或布尔类型的字段。例如,image 和 replicas 是 primitive 字段。行为:更换 Replace。
  • map,也称为 object:map 类型或包含子字段的复杂类型。例如,label、annotation、spec 和 metadata 都是 map。行为:合并元素或子字段。
  • list:包含 primitive 或 map 的条目列表的字段。例如,container、port 和 args 都是列表。行为:各不相同。

当 kubectl apply 更新 map 或 list 字段时,通常不会替换所有字段,而是更新单个子元素。例如,在合并 Deployment 的 spec 规范时,不会替换整个规范,而是比较并合并规范的子字段(例如 replicas)。

将更改合并到原始字段

原始字段被替换或清除。
注意:’ - ‘用于“不适用”,因为该值未被使用。

Field in object configuration file Field in live object configuration Field in last-applied-configuration Action
Yes Yes - Set live to configuration file value.
Yes No - Set live to local configuration.
No - Yes Clear from live configuration.
No - No Do nothing. Keep live value.

Merging changes to map fields

Fields that represent maps are merged by comparing each of the subfields or elements of the map:
Note: ‘-’ is used for “not applicable” because the value is not used.

Key in object configuration file Key in live object configuration Field in last-applied-configuration Action
Yes Yes - Compare sub fields values.
Yes No - Set live to local configuration.
No - Yes Delete from live configuration.
No - No Do nothing. Keep live value.

Merging changes for fields of type list

Merging changes to a list uses one of three strategies:

  • Replace the list.
  • Merge individual elements in a list of complex elements.
  • Merge a list of primitive elements.

The choice of strategy is made on a per-field basis.

替换列表 list

将列表视为 primitive 原始字段。替换或删除整个列表。这保留了 ordering。
示例:使用 kubectl apply 来更新 Pod 中容器的 args 字段。这会将实时配置中的 args 值设置为配置文件中的值。以前添加到实时配置的任何 args 元素都将丢失。配置文件中定义的 args 元素的顺序保留在实时配置中。

  1. # last-applied-configuration value
  2. args: ["a, b"]
  3. # configuration file value
  4. args: ["a", "c"]
  5. # live configuration
  6. args: ["a", "b", "d"]
  7. # result after merge
  8. args: ["a", "c"]

说明:合并使用配置文件值作为新的列表值。

合并一个复杂元素列表中的单个元素:

将列表视为 map,并将每个元素的特定字段视为 key。添加,删除或更新单个元素。不保留排序。
此合并策略在每个字段上使用一个名为 patchMergeKey 的特殊标记。 patchMergeKey 是为 Kubernetes 源代码中的每个字段定义的:types.go。合并 map 列表时,为给定元素指定为 patchMergeKey 的字段将用作该元素的 map key(the field specified as the patchMergeKey for a given element is used like a map key for that element)。
示例:使用 kubectl apply 来更新 PodSpec 的 containers 字段。这会合并列表,就好像它是一个 map,其中每个元素都是按名称键入的。

  1. # last-applied-configuration value
  2. containers:
  3. - name: nginx
  4. image: nginx:1.10
  5. - name: nginx-helper-a # key: nginx-helper-a; will be deleted in result
  6. image: helper:1.3
  7. - name: nginx-helper-b # key: nginx-helper-b; will be retained
  8. image: helper:1.3
  9. # configuration file value
  10. containers:
  11. - name: nginx
  12. image: nginx:1.10
  13. - name: nginx-helper-b
  14. image: helper:1.3
  15. - name: nginx-helper-c # key: nginx-helper-c; will be added in result
  16. image: helper:1.3
  17. # live configuration
  18. containers:
  19. - name: nginx
  20. image: nginx:1.10
  21. - name: nginx-helper-a
  22. image: helper:1.3
  23. - name: nginx-helper-b
  24. image: helper:1.3
  25. args: ["run"] # Field will be retained
  26. - name: nginx-helper-d # key: nginx-helper-d; will be retained
  27. image: helper:1.3
  28. # result after merge
  29. containers:
  30. - name: nginx
  31. image: nginx:1.10
  32. # Element nginx-helper-a was deleted
  33. - name: nginx-helper-b
  34. image: helper:1.3
  35. args: ["run"] # Field was retained
  36. - name: nginx-helper-c # Element was added
  37. image: helper:1.3
  38. - name: nginx-helper-d # Element was ignored
  39. image: helper:1.3

说明:

  • 名为“nginx-helper-a”的容器被删除,因为在配置文件中没有出现名为“nginx-helper-a”的容器。
  • 名为“nginx-helper-b”的容器在实时配置中保留了 args 的更改。即使其配置文件中的字段具有不同的值(配置文件中没有 args),kubectl apply 也能识别出实时配置中的“nginx-helper-b”与配置文件中的“nginx-helper-b”相同。这是因为 patchMergeKey 字段值(名称)在两者中都是相同的。
  • 添加了一个名为“nginx-helper-c”的容器,因为实时配置中没有带有该名称的容器出现,但具有该名称的容器出现在配置文件中。
  • 名为“nginx-helper-d”的容器被保留,因为 last-applied-configuration 中没有出现具有该名称的元素。

合并一系列 primitive 原始元素

合并 primitive 元素列表

从 Kubernetes 1.5 开始,不支持合并原始元素列表。
注意:为给定字段选择上述哪种策略是由 types.go 中的 patchStrategy 标记控制的。如果未为类型列表字段指定 patchStrategy,则会替换该列表。

默认的字段值

如果在创建对象时未指定字段值,则 API 服务器会将实时配置中的某些字段设置默认值。
这是 Deployment 的配置文件。该文件不指定策略或选择器:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: nginx-deployment
  5. spec:
  6. selector:
  7. matchLabels:
  8. app: nginx
  9. minReadySeconds: 5
  10. template:
  11. metadata:
  12. labels:
  13. app: nginx
  14. spec:
  15. containers:
  16. - name: nginx
  17. image: nginx:1.7.9
  18. ports:
  19. - containerPort: 80

使用 kubectl apply 创建对象:
kubectl apply -f https://k8s.io/docs/concepts/overview/object-management-kubectl/simple_deployment.yaml
使用 kubectl get 查看实时配置:
kubectl get -f https://k8s.io/docs/concepts/overview/object-management-kubectl/simple_deployment.yaml -o yaml
输出显示,API 服务器为实时配置中的几个字段设置了默认值。这些字段没有在配置文件中声明。

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. # ...
  4. spec:
  5. selector:
  6. matchLabels:
  7. app: nginx
  8. minReadySeconds: 5
  9. replicas: 1 # defaulted by apiserver
  10. selector:
  11. matchLabels: # defaulted by apiserver - derived from template.metadata.labels
  12. app: nginx
  13. strategy:
  14. rollingUpdate: # defaulted by apiserver - derived from strategy.type
  15. maxSurge: 1
  16. maxUnavailable: 1
  17. type: RollingUpdate # defaulted apiserver
  18. template:
  19. metadata:
  20. creationTimestamp: null
  21. labels:
  22. app: nginx
  23. spec:
  24. containers:
  25. - image: nginx:1.7.9
  26. imagePullPolicy: IfNotPresent # defaulted by apiserver
  27. name: nginx
  28. ports:
  29. - containerPort: 80
  30. protocol: TCP # defaulted by apiserver
  31. resources: {} # defaulted by apiserver
  32. terminationMessagePath: /dev/termination-log # defaulted by apiserver
  33. dnsPolicy: ClusterFirst # defaulted by apiserver
  34. restartPolicy: Always # defaulted by apiserver
  35. securityContext: {} # defaulted by apiserver
  36. terminationGracePeriodSeconds: 30 # defaulted by apiserver
  37. # ...

注意:某些字段的默认值是从配置文件中指定的其他字段的值派生的,例如 selector 字段。
在 patch 请求中,默认字段不会被重新默认,除非它们作为 patch 请求的一部分被明确地清除。这可能会导致某些依赖其他字段值的字段发生非预期的行为。当其他字段稍后更改时,除非明确清除,否则不会更新其默认值。
因此,建议在配置文件中明确定义服务器默认的字段,即使所需值与服务器默认值匹配。
示例:

  1. # last-applied-configuration
  2. spec:
  3. template:
  4. metadata:
  5. labels:
  6. app: nginx
  7. spec:
  8. containers:
  9. - name: nginx
  10. image: nginx:1.7.9
  11. ports:
  12. - containerPort: 80
  13. # configuration file
  14. spec:
  15. strategy:
  16. type: Recreate # updated value
  17. template:
  18. metadata:
  19. labels:
  20. app: nginx
  21. spec:
  22. containers:
  23. - name: nginx
  24. image: nginx:1.7.9
  25. ports:
  26. - containerPort: 80
  27. # live configuration
  28. spec:
  29. strategy:
  30. type: RollingUpdate # defaulted value
  31. rollingUpdate: # defaulted value derived from type
  32. maxSurge : 1
  33. maxUnavailable: 1
  34. template:
  35. metadata:
  36. labels:
  37. app: nginx
  38. spec:
  39. containers:
  40. - name: nginx
  41. image: nginx:1.7.9
  42. ports:
  43. - containerPort: 80
  44. # result after merge - ERROR!
  45. spec:
  46. strategy:
  47. type: Recreate # updated value: incompatible with rollingUpdate
  48. rollingUpdate: # defaulted value: incompatible with "type: Recreate"
  49. maxSurge : 1
  50. maxUnavailable: 1
  51. template:
  52. metadata:
  53. labels:
  54. app: nginx
  55. spec:
  56. containers:
  57. - name: nginx
  58. image: nginx:1.7.9
  59. ports:
  60. - containerPort: 80

说明:

  1. 用户创建一个没有定义 strategy.type 的 Deployment。
  2. 服务器将 strategy.type 默认为 RollingUpdate,并默认使用 strategy.rollingUpdate 值。
  3. 用户将 strategy.type 更改为 Recreate。尽管服务器期望它们被清除,但 strategy.rollingUpdate 值仍保持默认值。如果 strategy.rollingUpdate 值最初是在配置文件中定义的,那么它会更清楚地理解它们需要被删除。
  4. 由于 strategy.rollingUpdate 未清除,因此应用失败。strategy.rollingupdate 字段不能使用 Recreate 的 strategy.type 来定义。

建议:这些字段应该在对象配置文件中明确定义:

  • 工作负载上的选择器和 PodTemplate 标签,例如 Deployment,StatefulSet,Job,DaemonSet,ReplicaSet 和 ReplicationController
  • Deployment rollout strategy(推出策略)

    如何清除服务器默认字段或由其他 writer 设置的字段

    截至 Kubernetes 1.5,配置文件中的字段都能被合并操作清除。以下是一些解决方法:
    选项 1:通过直接修改活动对象来删除字段。
    注意:截至 Kubernetes 1.5,kubectl edit 不适用于 kubectl apply。一起使用会导致意想不到的行为。
    选项 2:通过配置文件删除字段。
  1. 将字段添加到配置文件以匹配活动对象。
  2. 应用配置文件,这会更新注释以包含该字段。
  3. 从配置文件中删除该字段。
  4. 应用配置文件,这会从活动对象和注释中删除该字段。

    如何在配置文件和直接命令式 writer 之间更改字段的所有权

    这些是用来更改单个对象字段的唯一方法:
  • 使用 kubectl apply。
  • 直接写入实时配置而不修改配置文件:例如,使用 kubectl scale。

    将所有权从直接命令式 writer 改为配置文件

    将该字段添加到配置文件。对于该字段,停止直接更新不经过 kubectl apply 的实时配置(discontinue direct updates to the live configuration that do not go through kubectl apply)。

    将所有权从配置文件改为直接命令式 writer

    截至 Kubernetes 1.5,需要手动步骤:

  • 从配置文件中删除该字段。

  • 从实时对象上的 kubectl.kubernetes.io/last-applied-configuration annotation 中删除该字段。