事情的起因是使用 kubectl apply
时遇到的一个偶发的错误提示.
Warning: resource deployments/busybox-demo is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
为了快速规避可能的不良影响, 我转到了 kubectl patch
.
在事后翻阅文档时, 可以明显的感受到 k8s 的 patch 基本考虑并处理了所有可能的场景, 极具借鉴意义.
部分更新, partial modification, 是一个常见而复杂的问题, HTTP PATCH 就是典型的部分更新语义, 一些实现会将 HTTP PUT 也实现成部分更新. 虽然业界对部分更新有充分而详细的讨论, 但很多实现者依然会忽略这些现成的结论而自行设计, 导致重复的问题重复出现.
我们假设已经存在一个资源 busybox-demo, 来讨论部分更新的典型问题.
apiVersion: apps/v1
kind: Deployment
metadata:
name: busybox-demo
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- name: busybox
image: busybox:1.28
args:
- sleep
- "1000000"
部分更新的语义下, 我们只需要提供资源的标识和想要更新的字段. 所以如果我们希望把 replicas 更新为 2, 只需要提供如下信息.
apiVersion: apps/v1
kind: Deployment
metadata:
name: busybox-demo
namespace: default
spec:
replicas: 2
而困难的点, 一方面在于如何把 replicas 更新为 0. 是仅需要把 replicas 设置为 0, 还是需要把 replicas 设置 null. server 会不会错误的把 key 不存在理解为更新为 0? 很多实现没有明确而清晰的考虑零值的问题, 导致实现使用中出现歧义.
另一方面在于是否允许对数组进行部分更新? 怎么标识同一元素? 怎么标识某一元素的删除? 在 JSON Merge PATCH 中是无法对数组做部分更新, 这需要一些额外的工作.
kubectl patch
提供了三种部分更新的策略, strategic 是默认选项.
你可以通过 patch
将 deploy 的 replicas 从 1 更新到 2.
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-92x58 1/1 Running 0 3m15s
➜ k patch -n default deploy busybox-demo -p '{"spec": {"replicas": 2}}'
deployment.apps/busybox-demo patched
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-92x58 1/1 Running 0 3m25s
busybox-demo-7b8b5c46db-j7grc 1/1 Running 0 4s
你可以通过指定 replicas 为 0, 将 pod 的数量减少到 0.
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-96q2d 1/1 Running 0 60s
➜ k patch -n default deploy busybox-demo -p '{"spec": {"replicas": 0}}'
deployment.apps/busybox-demo patched
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-96q2d 1/1 Terminating 0 79s
当你将 replicas 指定为 null 时, 有趣的事情发生了, replicas 变为了 1, 这是 replicas 的默认值. 原因是 strategic 会将 null 视为要删除对应的 key, 进而导致其取默认值.
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-kfhbw 1/1 Running 0 4s
busybox-demo-7b8b5c46db-r7znn 1/1 Running 0 4s
➜ k patch -n default deploy busybox-demo -p '{"spec": {"replicas": null}}'
deployment.apps/busybox-demo patched
➜ k get -n default pods
NAME READY STATUS RESTARTS AGE
busybox-demo-7b8b5c46db-kfhbw 1/1 Running 0 21s
busybox-demo-7b8b5c46db-r7znn 1/1 Terminating 0 21s
➜ k get -n default deploy busybox-demo -o json | jq '.spec.replicas'
1
k8s 提供 strategic 的主要目的是为了实现数组的部分更新. k8s 在文档中定义了两个注解: patchStrategy 和 patchMergeKey, 以 PodSpec.Containers 为例. patchStrategy 为 merge, 代表 k8s 会尝试将已有的数组和请求中的数组进行合并. 而 patchMergeKey 则指定了数组合并时用来判断数组元素是否相同的字段, 在此为 name.
所以, 我们可以通过如下的方式去修改和新增容器.
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[]'
{
"args": [
"sleep",
"1000000"
],
"image": "busybox:1.28",
"imagePullPolicy": "IfNotPresent",
"name": "busybox",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
➜ cat patch-containers.yaml
spec:
template:
spec:
containers:
- name: busybox
resources:
requests:
cpu: 500m
- name: busybox-add
image: busybox:1.28
args:
- sleep
- "3600"
➜ k patch -n default deploy busybox-demo --patch-file patch-containers.yaml
deployment.apps/busybox-demo patched
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[]'
{
"args": [
"sleep",
"1000000"
],
"image": "busybox:1.28",
"imagePullPolicy": "IfNotPresent",
"name": "busybox",
"resources": {
"requests": {
"cpu": "500m"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
{
"args": [
"sleep",
"3600"
],
"image": "busybox:1.28",
"imagePullPolicy": "IfNotPresent",
"name": "busybox-add",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
我们也可以通过 patch 删除某个容器, 虽然好像 k8s 的文档里没有说这种做法.
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[].name'
"busybox"
"busybox-add"
➜ k patch -n default deploy busybox-demo -p '{"spec": {"template": {"spec": {"containers": [{"name": "busybox-add", "$patch": "delete"}]}}}}'
deployment.apps/busybox-demo patched
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[].name'
"busybox"
k8s 也支持 JSON Merge Patch 和 JSON Patch.
相较于 strategic, JSON Merge Patch 的主要区别在于其并不支持数组层面的部分更新. JSON Merge Patch 会直接使用请求中的数组替换现有的数组.
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[]'
{
"args": [
"sleep",
"1000000"
],
"image": "busybox:1.28",
"imagePullPolicy": "IfNotPresent",
"name": "busybox",
"resources": {
"requests": {
"cpu": "500m"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
➜ cat patch-json.yaml
spec:
template:
spec:
containers:
- name: busybox-add
image: busybox:1.28
args:
- sleep
- "3600"
➜ k patch -n default deploy busybox-demo --patch-file patch-json.yaml --type merge
deployment.apps/busybox-demo patched
➜ k get -n default deploy busybox-demo -o json | jq '.spec.template.spec.containers[]'
{
"args": [
"sleep",
"3600"
],
"image": "busybox:1.28",
"imagePullPolicy": "IfNotPresent",
"name": "busybox-add",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
JSON Patch 则是完全的另外一个思路, 并不是完全的增量更新. 它允许对资源的任意字段单独进行复杂的操作, 更灵活强大的同时, 也更复杂.
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
当使用 kubectl 去操作资源时, 我们更推荐使用 apply 而不是 patch. apply 做了一些额外工作, 极大的降低了使用成本.
当你使用 apply 去创建或者修改资源时, k8s 会通过特定的注解来记录这次请求.
➜ k get -n default deploy busybox-demo -o json | jq '.metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"]' -r | jq
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"annotations": {},
"name": "busybox-demo",
"namespace": "default"
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "busybox"
}
},
"template": {
"metadata": {
"labels": {
"app": "busybox"
}
},
"spec": {
"containers": [
{
"args": [
"sleep",
"1000000"
],
"image": "busybox:1.28",
"name": "busybox"
}
]
}
}
}
}
当你随后再通过 apply 去更新资源时, kubectl 会根据上次的请求, 当前的资源现状和这次的请求计算出具体应该如何修改. 而体而言:
此时如果面临上文中需要删除某个容器的场景时, 直接在配置文件中删除对应的配置即可, 不再需要使用 $patch 这样的字段了.
需要注意, patch
等命令都不会更新这个注解, 所以如果最好不要把 apply 和其他命令混用在一个字段上.