你不知道的kubectl apply
1 kubectl apply无法移除annotation
我平时喜欢用yaml进行部署应用,最近使用kubectl apply发现一个问题。我使用kubectl rollout restart重启应用,kubectl会在spec.template.metadata.annotations
添加kubectl.kubernetes.io/restartedAt: <current time>
。然后我再更新yaml文件进行kubectl apply后,并没有将annotation里kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
删除掉。
kubectl rollout restart之后的对象
#kubectl get -f nginx-deployment.yaml -o yaml
....
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
labels:
app: nginx
...
文件里没有kubectl.kubernetes.io/restartedAt
# cat nginx-deployment.yaml
...
spec:
template:
metadata:
labels:
app: nginx
# kubectl apply -f nginx-deployment.yaml
deployments.apps/nginx configured
# kubectl get -f nginx-deployment.yaml -o yaml
....
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
labels:
app: nginx
...
为什么kubectl apply无法移除annotation呢?kubectl apply作为Declarative Management of Kubernetes Objects,理论上应该是我所定义即所得。为了找到问题的答案,先要了解kubectl apply的工作原理。
2 kubectl apply -f的工作原理
本文使用的kubectl和apiserver版本为1.23.17,相关的阅读源码注释可以访问这里
kubectl apply
命令有一些默认的命令行参数,这里比较重要的有--validate=true --override=true --dry-run=none --server-side=false
。 即kubectl apply -f file
相当于kubectl apply -f file --validate=true --override=true --dry-run=none --server-side=false
。
下面讲解kubectl apply -f file
工作原理
总体流程为:
- 获取openapi的schema
- 读取文件
- 验证字段
- 获得对象的meta.RESTMapping
- 获取apiserver中的对象
- 生成patch并进行patch请求
- 显示最终输出
2.1 获取openapi schema
从apiserver的/openapi/v2
接口获取所有资源(内置的对象和crd对象和aggregate apiserver对象)的schema定义。
2.2 读取文件
kubectl apply支持的内容格式为json或yaml。
如果-f后面的参数为文件,直接读取。
如果-f后面参数为目录,读取目录下面后缀为".json", ".yaml", ".yml"
文件,如果设置-R, --recursive=true
,递归读取目录下面文件后缀为".json", ".yaml", ".yml"
的文件。
如果-f后面参数为"-",则从标准输入读取。
-f后面参数还支持"http://“和"https://“开头地址,从这些地址获取内容。
2.3 验证字段
然后根据schema进行验证各个字段的类型是否未知不合法、是否缺少必须字段,这个是在客户端进行字段验证。
从1.24版本kubectl开始支持ServerSideFieldValidation
,即–validate参数默认值为"strict”,即在apiserver端进行字段的验证,如果服务端不支持则使用客户端进行验证。具体实现是在 CreateOptions, PatchOptions, and UpdateOptions
增加fieldValidation
字段。具体详情访问设计文档
比如字段类型错误:
error: error validating "nginx-deployment.yaml": error validating data: ValidationError(Deployment.spec.replicas): invalid type for io.k8s.api.apps.v1.DeploymentSpec.replicas: got "string", expected "integer"; if you choose to ignore these errors, turn validation off with --validate=false
不存在的字段错误:
error: error validating "nginx-deployment.yaml": error validating data: ValidationError(Deployment.spec): unknown field "notexist" in io.k8s.api.apps.v1.DeploymentSpec; if you choose to ignore these errors, turn validation off with --validate=false
缺少必须的字段错误:
error: error validating "nginx-deployment.yaml": error validating data: ValidationError(Deployment.spec): missing required field "selector" in io.k8s.api.apps.v1.DeploymentSpec; if you choose to ignore these errors, turn validation off with --validate=false
2.4 获得对象的meta.RESTMapping
验证字段成功后,利用discovery client获得对象的meta.RESTMapping,用于确定对象是否为namespaced和对象的Resource(这些信息确定访问apiserver的路径)
2.5 获取apiserver中的对象
发起get请求从apiserver获得对象
2.6 进行patch或create
根据对象是否存在,采取不同的操作。当对象存在时候,就会进行计算patch,当patch不为空(即发生了变化),则发起patch请求。如果对象不存在,则发起creat请求。
2.6.1 对象存在执行patch
概念
上一次apply的内容:kubectl apply会将上一次apply的内容保存到对象的annotations里kubectl.kubernetes.io/last-applied-configuration
,这里就是apiserver中的对象里的annotations里kubectl.kubernetes.io/last-applied-configuration
这次apply的内容:将上面读取的文件的内容annotations里添加kubectl.kubernetes.io/last-applied-configuration
,value为读取的文件的内容(移除原有annotations里的kubectl.kubernetes.io/last-applied-configuration
),进行json encode之后的byte。
当前对象内容:apiserver中的对象
2.6.2 字段diff算法
字段类型分类(这里只包含json解析成golang类型,并没有所有golang类型,比如struct就是map类型)
- primitive:string, float64, bool, int64, nil
- list:slice, array
- map:map, struct
type | diff action |
---|---|
primitive | not equal |
list | patch类型为json merge patch,则按照not equal区分不同 patch类型为strategic merge patch,patchStrategy为replace,则按照not equal区分不同。 patchStrategy为merge: - 元素类型为primitive,则按照集合计算diff - 元素类型为map,按照mergekey的值是否相等判断是否是同一个元素,按照集合计算diff |
map(key一定是string) | patch类型为json merge patch,则按照not equal区分不同 patch类型为strategic merge patch,patchStrategy为replace,则按照not equal区分不同 patchStrategy为merge: - value类型为primitive,则按照集合计算diff - value类型为list,递归的进行diff -value类型为map,递归的进行diff |
2.6.3 json merge patch
对于非内置类型对象(crd和aggregator资源),使用CreateThreeWayJSONMergePatch来计算patch,patch类型为json merge patch
,json merge patch为标准协议RFC7386。
计算步骤:
- 根据“当前对象内容”和“这次apply的内容”进行比较,计算出”增加和更新的字段“,过滤掉”增加和更新的字段“里为null值的字段
- 根据“上一次apply的内容”和“这次apply的内容”进行比较,计算出”删除的字段“(包括增加和更新为null的字段)
- 将”删除的字段“和”增加和更新的字段“进行合并为最后patch
- 检查"apiVersion"和"kind"和"metadata.name"不能在patch中(即"apiVersion"和"kind"和"metadata.name"字段不能修改)
2.6.4 strategic merge patch
对于内置对象使用CreateThreeWayMergePatch来计算patch,patch类型为strategic merge patch
。
strategic merge patch
为kubernetes自己定义的patch类型, 一般应用场景:字段类型为结构体的切片,进行合并时候进行元素的聚合,而不是字段值直接替换。这个时候就需要定义一个合并策略patchStrategy
,而且需要一个字段来区分(类似数据库里的唯一键)是否同一元素,这个字段称为patchMergeKey
。
下面举个例子
// Current Condition of persistent volume claim. If underlying persistent volume is being
// resized then the Condition will be set to 'ResizeStarted'.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
Conditions []PersistentVolumeClaimCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,4,rep,name=conditions"`
// PersistentVolumeClaimCondition contails details about state of pvc
type PersistentVolumeClaimCondition struct {
Type PersistentVolumeClaimConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=PersistentVolumeClaimConditionType"`
Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"`
// Last time we probed the condition.
// +optional
LastProbeTime metav1.Time `json:"lastProbeTime,omitempty" protobuf:"bytes,3,opt,name=lastProbeTime"`
// Last time the condition transitioned from one status to another.
// +optional
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"`
// Unique, this should be a short, machine understandable string that gives the reason
// for condition's last transition. If it reports "ResizeStarted" that means the underlying
// persistent volume is being resized.
// +optional
Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"`
// Human-readable message indicating details about last transition.
// +optional
Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
}
这个例子中Conditions
字段类型为[]PersistentVolumeClaimCondition
,在字段的上面有标记+patchMergeKey=type
和+patchStrategy=merge
,代表这个字段进行聚合时候使用merge策略,且以字段type的值来判别是否为相同元素。
strategic merge patch
还有其他特殊的key,"$patch" "$retainKeys" "$deleteFromPrimitiveList" "$setElementOrder"
,具体可以访问strategic-merge-patch proposal 和Preserve Order in Strategic Merge Patch
如果apiserver有openapi的Schema,则从schema获取字段的patchMergeKey
和patchStrategy
。如果apiserver没有openapi接口,则使用结构体里字段的json tag信息获取字段,并从tag获取"patchStrategy"和"patchMergeKey"值。然后根据这些信息生成patch。
patch生成流程:
根据“当前对象内容”和“这次apply的内容”进行比较,计算出”增加和更新的字段“
根据“上一次apply的内容”和“这次apply的内容”进行比较,计算出”删除的字段“
将”删除的字段“和”增加和更新的字段“进行合并为”最后patch“
如果–override为false,则检验“上一次apply的内容”和“当前对象内容”存在的差异和”最后patch“是否存在冲突(两个对同一个key同时进行修改)
比如“上一次apply的内容”为
"{k: a}"
, “当前对象内容”为"{k: b}"
, “这次apply的内容”为"{k: c}"
其中
"{k: b}"
是非kubectl apply修改的,而现在基于“上一次apply的内容”修改成”{k: c}",会存在冲突
如果上面计算出来的patch不为空,则发起patch请求。
2.6.5 对象不存在进行创建
如果apiserver中没有这个对象,则进行创建这个资源
3 显示最终输出
创建成功会输出:
deployment.apps/nginx-deployment created
如果patch不为空且patch成功输出:
deployment.apps/nginx-deployment configured
如果patch为空,即没有任何修改,则输出
deployment.apps/nginx-deployment unchanged
可以指定-o或–output输出格式,比如-o name
输出
deployment.apps/nginx-deployment
4 无法移除annotation的原因
回到前面的问题,由于删除的字段是根据“上一次apply的内容”和“这次apply的内容”的差异计算的,而spec.template.metadata.annotations
里的kubectl.kubernetes.io/restartedAt
不在“上一次apply的内容”,所以不会移除这个字段。
4.1 那么如何移除这个annotation
使用patch请求移除这个annotation
使用kubectl edit
进行移除,或kubectl patch deployment nginx-deployment -p '{"spec":{"template":{"metadata":{"annotations": {"kubectl.kubernetes.io/restartedAt": null}}}}}'
kubectl apply机制来移除
当“上一次apply的内容”的spec.template.metadata.annotations
为空:在yaml文件里设置spec.template.metadata.annotations
为null
,然后进行kubectl apply。
....
spec:
template:
metadata:
annotations: null
labels:
app: nginx
通常情况“上一次apply的内容”的spec.template.metadata.annotations
不为空:这个操作需要麻烦一点。
添加
kubectl.kubernetes.io/restartedAt
到yaml文件中,然后进行kubectl apply,让annotations里的kubectl.kubernetes.io/last-applied-configuration
里记录这个annotation。.... spec: template: metadata: # add next two line annotations: a: b kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00" labels: app: nginx
从yaml文件里移除这个annotation,然后进行kubectl apply
使用update请求来移除
使用kubectl replace -f nginx-deployment.yaml --save-config=true
来移除。
5 其他kubectl apply重要命令行参数
5.1 对非kubectl apply管理的对象进行apply出现的警告
如果apiserver中的对象annotations里没有kubectl.kubernetes.io/last-applied-configuration
,执行kubectl apply会输出类似警告。
Warning: resource deployments/nginx-deployment 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.
5.2 server side apply
上面所有提到的机制都是client side apply,它存在这几个问题:
- 当多个不同的客户端对同一个对象进行操作时候会出现无法管理非kubectl apply增加的字段
- 不同的客户端对同一字段进行操作时候会出现冲突,且无法知道是谁操作的
- strategic merge patch不是标准协议,任何修改都需要客户端和服务端进行修改,而且还要考虑版本兼容性
基于上面的问题(不止这些),所以现在又引入了server side apply这个机制,这里就不展开,想要知道设计初衷请访问设计文档
可以指定–server-side=true使用server side apply。使用server side apply时候,直接发起patch请求,body为读取的内容,patch类型为ApplyPatch
。而不用客户端计算patch(不用先get对象,然后再进行diff出patch),即apiserver端执行计算patch和merge。
最后执行成功输出deployment.app/nginx-deployment serverside-applied
。
5.2.1 冲突处理
server side apply会在managedFields字段里记录那个字段是谁管理的
- apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
f:kubectl.kubernetes.io/last-applied-configuration: {}
manager: kubectl
operation: Update
time: "2023-10-04T10:54:28Z"
- apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:template:
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/restartedAt: {}
manager: kubectl-rollout
operation: Update
time: "2023-10-05T03:11:36Z"
当两个不同的manager对同一个字段进行修改,就会出现冲突。这里的manager是客户端进行Patch、Update、Create请求时候指定的FieldManager
参数。
三种冲突解决方式:
抢夺控制权,kubectl apply里的–force-conflicts设置为true,则会将字段的控制权变成自己。
放弃控制,从文件内容里移除冲突字段
共享控制,文件内容里添加现在apiserver的里的字段的值,然后进行kubectl apply –server-side,获得字段的共享控制权
怎么用server side apply移除annotation kubectl.kubernetes.io/restartedAt
答案是目前没有简单的办法去移除这个annotation,相关issuehttps://github.com/kubernetes/kubernetes/issues/99003
但是非要使用server side apply进行移除,也是有办法的。
移除kubectl-rollout对annotation的控制权,即移除managedFields里的manager为kubectl-rollout元素
kubectl patch deployment nginx-deployment --type=json -p '[{ "op": "remove", "path": "/metadata/managedFields/3"}]'
nginx-deployment.yaml
文件里spec.template.metadata.annotations
添加kubectl.kubernetes.io/restartedAt: "2023-10-05T11:11:36+08:00"
,执行kubectl apply -f nginx-deployment.yaml --server-side
获得字段的控制权spec: template: metadata: annotations: kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
nginx-deployment.yaml
文件里移除这个annotation,然后再次执行kubectl apply -f nginx-deployment.yaml --server-side
server side apply
虽然解决了部分问题,但是存在无法兼容现在client side apply
(多个manger同时控制一个字段,导致无法删除这个字段)的问题。
5.3 dry run
默认–dry-run为none,即命令真正执行生效。
如果–dry-run为client,不执行Create和Patch请求、包括计算Patch。执行输出类似deployment.apps/nginx-deployment configured (dry run)
或deployment.apps/nginx-deployment created (dry run)
。
如果–dry-run为server,首先会验证对象的Patch请求是否支持dry run。如果不支持dry run则报错xxx doesn't support dry-run
。如果支持,则apiserver端只会执行请求,不会将修改对象保存到etcd。
5.4 –prune
这是一个比较少用的功能,它的作用是删除满足下面条件的资源
资源类型在–prune-whitelist列表里,如果–prune-whitelist为空,则使用默认的资源类型列表(ConfigMap、Endpoints、Namespace、PersistentVolumeClaim、PersistentVolume、Pod、ReplicationController、Secret、Service、Job、CronJob、Ingress、DaemonSet、Deployment、ReplicaSet、StatefulSet)
资源在这次kubectl apply操作的对象的命名空间下
资源是kubectl apply管理的,即annotations里有
kubectl.kubernetes.io/last-applied-configuration
资源不是这次kubectl apply操作对象
这个是危险命令谨慎使用。
5.5 –force
client side apply
进行patch请求时,如果apiserver返回Conflict错误,则进行5次重试。其他错误不进行重试。
重试次数用完后还未成功或其他错误(不重试),且apiserver返回是Conflict错误或IsInvalid错误,且命令行设置了–force,则进行对象的删除然后执行创建。
首先对“这次apply的内容”执行创建,如果出现失败,则对“上一次apply的内容”执行创建。
5.6 –overwrite
使用strategic merge patch来计算patch时,如果修改的字段被非kubectl apply修改(跟"上一次apply内容"里的不一样),且–overwrite为false(默认为true),则会出现冲突错误。
warning: error calculating patch from openapi spec: patch:
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
.....
spec:
template:
metadata:
annotations: null
conflicts with changes made from original to current:
metadata:
annotations:
deployment.kubernetes.io/revision: "10"
...
error: error when applying patch:
to:
Resource: "apps/v1, Resource=deployments", GroupVersionKind: "apps/v1, Kind=Deployment"
Name: "nginx-deployment", Namespace: "default"
for: "nginx-deployment.yaml": error when creating patch with:
original:
....
modified:
....
current:
....
for: "nginx-deployment.yaml": patch:
metadata:
annotations:
....
5.7 子命令
kubectl apply包含三个子命令:
- view-last-applied
- edit-last-applied
- set-last-applied
view-last-applied查看对象annotations里的kubectl.kubernetes.io/last-applied-configuration
值
# kubectl apply view-last-applied deployments.apps nginx-deployment
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
name: nginx-deployment
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx:1.18
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
set-last-applied设置对象annotations里的kubectl.kubernetes.io/last-applied-configuration
值为文件里的内容,而不修改其他字段。
所以上面去除annotation方法还可以这样操作:
nginx-deployment.yaml里添加
kubectl.kubernetes.io/restartedAt
kubectl apply set-last-applied -f nginx-deployment.yaml --create-annotation=true
然后从nginx-deployment.yaml里移除
kubectl.kubernetes.io/restartedAt
执行kubectl apply
edit-last-applied编辑对象annotations里的kubectl.kubernetes.io/last-applied-configuration
,类似kubectl edit。
6 Reference
openapi-v3-field-validation-ga
[Remove unexisting annotations in kubectl apply -f](https://stackoverflow.com/questions/60988558/remove-unexisting-annotations-in-kubectl-apply-f)
how-apply-calculates-differences-and-merges-changes
How to clear server-defaulted fields or fields set by other writers