你不知道的kubectl apply

我平时喜欢用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之后的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 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的工作原理。

本文使用的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工作原理

kubectl-apply

总体流程为:

  1. 获取openapi的schema
  2. 读取文件
  3. 验证字段
  4. 获得对象的meta.RESTMapping
  5. 获取apiserver中的对象
  6. 生成patch并进行patch请求
  7. 显示最终输出

从apiserver的/openapi/v2接口获取所有资源(内置的对象和crd对象和aggregate apiserver对象)的schema定义。

kubectl apply支持的内容格式为json或yaml。

如果-f后面的参数为文件,直接读取。

如果-f后面参数为目录,读取目录下面后缀为".json", ".yaml", ".yml"文件,如果设置-R, --recursive=true,递归读取目录下面文件后缀为".json", ".yaml", ".yml"的文件。

如果-f后面参数为"-",则从标准输入读取。

-f后面参数还支持"http://“和"https://“开头地址,从这些地址获取内容。

然后根据schema进行验证各个字段的类型是否未知不合法、是否缺少必须字段,这个是在客户端进行字段验证。

从1.24版本kubectl开始支持ServerSideFieldValidation,即–validate参数默认值为"strict”,即在apiserver端进行字段的验证,如果服务端不支持则使用客户端进行验证。具体实现是在 CreateOptions, PatchOptions, and UpdateOptions 增加fieldValidation字段。具体详情访问设计文档

比如字段类型错误:

1
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

不存在的字段错误:

1
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

缺少必须的字段错误:

1
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

验证字段成功后,利用discovery client获得对象的meta.RESTMapping,用于确定对象是否为namespaced和对象的Resource(这些信息确定访问apiserver的路径)

发起get请求从apiserver获得对象

根据对象是否存在,采取不同的操作。当对象存在时候,就会进行计算patch,当patch不为空(即发生了变化),则发起patch请求。如果对象不存在,则发起creat请求。

概念

上一次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中的对象

字段类型分类(这里只包含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

对于非内置类型对象(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"字段不能修改)

对于内置对象使用CreateThreeWayMergePatch来计算patch,patch类型为strategic merge patch

strategic merge patch为kubernetes自己定义的patch类型, 一般应用场景:字段类型为结构体的切片,进行合并时候进行元素的聚合,而不是字段值直接替换。这个时候就需要定义一个合并策略patchStrategy,而且需要一个字段来区分(类似数据库里的唯一键)是否同一元素,这个字段称为patchMergeKey

下面举个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    // 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 proposalPreserve Order in Strategic Merge Patch

如果apiserver有openapi的Schema,则从schema获取字段的patchMergeKeypatchStrategy。如果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请求。

如果apiserver中没有这个对象,则进行创建这个资源

创建成功会输出:

1
deployment.apps/nginx-deployment created

如果patch不为空且patch成功输出:

1
deployment.apps/nginx-deployment configured

如果patch为空,即没有任何修改,则输出

1
deployment.apps/nginx-deployment unchanged

可以指定-o或–output输出格式,比如-o name输出

1
deployment.apps/nginx-deployment

回到前面的问题,由于删除的字段是根据“上一次apply的内容”和“这次apply的内容”的差异计算的,而spec.template.metadata.annotations里的kubectl.kubernetes.io/restartedAt不在“上一次apply的内容”,所以不会移除这个字段。

使用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.annotationsnull,然后进行kubectl apply。

1
2
3
4
5
6
7
....
spec:
  template:
    metadata:
      annotations: null
      labels:
        app: nginx

通常情况“上一次apply的内容”的spec.template.metadata.annotations不为空:这个操作需要麻烦一点。

  1. 添加kubectl.kubernetes.io/restartedAt到yaml文件中,然后进行kubectl apply,让annotations里的kubectl.kubernetes.io/last-applied-configuration里记录这个annotation。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    ....
    spec:
      template:
        metadata:
          # add next two line
          annotations:
            a: b
            kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
          labels:
            app: nginx
    
  2. 从yaml文件里移除这个annotation,然后进行kubectl apply

使用update请求来移除

使用kubectl replace -f nginx-deployment.yaml --save-config=true来移除。

如果apiserver中的对象annotations里没有kubectl.kubernetes.io/last-applied-configuration,执行kubectl apply会输出类似警告。

1
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.

上面所有提到的机制都是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

server side apply会在managedFields字段里记录那个字段是谁管理的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 - 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进行移除,也是有办法的。

  1. 移除kubectl-rollout对annotation的控制权,即移除managedFields里的manager为kubectl-rollout元素

    1
    
    kubectl patch deployment nginx-deployment --type=json -p '[{ "op": "remove", "path": "/metadata/managedFields/3"}]'
    
  2. 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获得字段的控制权

    1
    2
    3
    4
    5
    
    spec:
      template:
        metadata:
          annotations:
            kubectl.kubernetes.io/restartedAt: "2022-07-26T11:44:32+08:00"
    
  3. nginx-deployment.yaml文件里移除这个annotation,然后再次执行 kubectl apply -f nginx-deployment.yaml --server-side

server side apply虽然解决了部分问题,但是存在无法兼容现在client side apply(多个manger同时控制一个字段,导致无法删除这个字段)的问题。

默认–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。

这是一个比较少用的功能,它的作用是删除满足下面条件的资源

  • 资源类型在–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操作对象

这个是危险命令谨慎使用。

client side apply进行patch请求时,如果apiserver返回Conflict错误,则进行5次重试。其他错误不进行重试。

重试次数用完后还未成功或其他错误(不重试),且apiserver返回是Conflict错误IsInvalid错误,且命令行设置了–force,则进行对象的删除然后执行创建。

首先对“这次apply的内容”执行创建,如果出现失败,则对“上一次apply的内容”执行创建。

使用strategic merge patch来计算patch时,如果修改的字段被非kubectl apply修改(跟"上一次apply内容"里的不一样),且–overwrite为false(默认为true),则会出现冲突错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:
  ....

kubectl apply包含三个子命令:

  • view-last-applied
  • edit-last-applied
  • set-last-applied

view-last-applied查看对象annotations里的kubectl.kubernetes.io/last-applied-configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 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方法还可以这样操作:

  1. nginx-deployment.yaml里添加kubectl.kubernetes.io/restartedAt

  2. kubectl apply set-last-applied -f nginx-deployment.yaml --create-annotation=true

  3. 然后从nginx-deployment.yaml里移除kubectl.kubernetes.io/restartedAt

  4. 执行kubectl apply

edit-last-applied编辑对象annotations里的kubectl.kubernetes.io/last-applied-configuration,类似kubectl edit。

server-side field validation

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

相关内容