Kubernetes 基础体系 · 第 4/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 声明式配置:YAML、默认值、字段所有权、Apply 和 Diff

Kubernetes 配置经常以 YAML 文件出现,因此很多人会把以下几件事混为一谈:

  • YAML 文件是什么;
  • API Server 接受什么对象;
  • kubectl apply 如何把文件变成集群状态;
  • 默认字段是谁补上的;
  • 多个操作者修改同一个对象时,谁拥有字段;
  • kubectl diff 比较的到底是什么。

这些概念分别属于不同层次。YAML 只是输入表示法,apply 是一种 API 写入语义,字段所有权是 Server-Side Apply 的并发控制机制,而 diff 是对“期望对象”和“当前对象”之间差异的检查。


一、先建立对象模型:YAML 不是 Kubernetes 对象本身

一个典型的 Kubernetes 对象如下:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: web
  namespace: demo
  labels:
    app: web

spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 80

这个文件经过解析后,才成为一个发送给 Kubernetes API Server 的结构化对象。各部分含义如下:

  • apiVersionkind 共同确定对象的 GVK
    • Group:API 组,例如 apps
    • Version:版本,例如 v1
    • Kind:资源类型,例如 Deployment
  • metadata 保存对象身份和附加信息,例如:
    • name
    • namespace
    • labels
    • annotations
    • uid
    • resourceVersion
    • managedFields
  • spec 表示用户或控制器声明的目标状态。
  • status 表示 Kubernetes 控制器观察到的实际状态。

通常,用户配置 spec,控制器更新 status。例如,Deployment 的 spec.replicas: 3 是目标副本数,而 status.availableReplicas: 3 是控制器观察到的可用副本数。二者相同并不意味着它们属于同一个写入流程。

YAML 只是一种序列化格式

以下 YAML 和 JSON 表达的是同一个 API 请求数据:

replicas: 3
{
  "replicas": 3
}

API Server 最终处理的是结构化对象,而不是 YAML 的缩进、注释或文件名。YAML 注释不会进入对象:

replicas: 3 # 期望运行三个副本

解析后只保留:

{
  "replicas": 3
}

因此,YAML 中的注释不能成为 Kubernetes 的运行时配置,也不会参与字段所有权或 diff。

YAML 类型仍然会影响 API 请求

YAML 中的值有类型。下面的 value 是布尔值,而不是字符串:

env:
  - name: FEATURE_ENABLED
    value: true

EnvVar.value 的 API 类型是字符串,因此这可能导致 API 校验失败。应显式写成字符串:

env:
  - name: FEATURE_ENABLED
    value: "true"

同理,端口、资源数量和字符串环境变量不要仅凭视觉判断类型:

containerPort: 8080  # 整数,正确
value: "8080"        # 字符串,适用于 EnvVar.value

kubectl 会先解析 YAML,再转换成 Kubernetes API 使用的 JSON 请求。解析成功不代表 API 校验一定成功;YAML 语法错误、字段类型错误和 API 语义错误属于不同阶段的问题。


二、对象进入 API Server 后会经过什么流程

一次典型的声明式写入大致经过以下路径:

sequenceDiagram
    participant F as YAML 文件
    participant K as kubectl
    participant A as API Server
    participant V as 认证/授权
    participant M as Admission
    participant S as 存储
    participant C as Controller

    F->>K: 解析 YAML
    K->>A: 发送创建、更新或 Apply 请求
    A->>V: 认证与 RBAC 授权
    A->>M: 默认值与 Admission 处理
    M-->>A: 修改或拒绝对象
    A->>A: OpenAPI/schema 校验与字段管理
    A->>S: 持久化对象
    S-->>A: 返回对象
    A-->>K: 返回结果
    S-->>C: 对象变化触发 watch
    C->>A: 更新 status 或派生资源

这里有几个容易被忽略的事实:

  1. kubectl 解析 YAML,但不负责完成全部 Kubernetes 语义校验。
  2. API Server 可能补充默认值。
  3. Admission Webhook 可能修改对象,也可能拒绝对象。
  4. API Server 根据资源版本和 schema 校验字段。
  5. 对象写入后,控制器通过 watch 观察变化,再更新其他字段或资源。

因此,文件内容不是最终存储对象的完整描述。最终对象可以表示为:

Ostored=A(D(V(P(Y))))O_{\text{stored}} = A\bigl( D\bigl( V\bigl( P(Y) \bigr)\bigr)\bigr)

其中:

  • YY:YAML 文件;
  • PP:YAML 解析;
  • VV:认证、授权和基本请求处理;
  • DD:默认值填充;
  • AA:Admission 等服务器端变更;
  • OstoredO_{\text{stored}}:API Server 实际接受并存储的对象。

这个表达式说明:不能只比较 YAML 文件,就断言两个对象在 Kubernetes 中完全相同。


三、默认值:省略字段不等于值为零

1. 省略字段、显式零值和 null 不同

考虑一个 Deployment:

spec:
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27

这里没有写 spec.replicas。对于 Deployment,API 默认值通常会把它解释为 1

以下三种写法不是同一件事:

# A:省略
spec:
  selector: ...
# B:显式设置为零
spec:
  replicas: 0
  selector: ...
# C:显式 null
spec:
  replicas: null
  selector: ...

它们的含义分别是:

  • 省略:让 API 默认机制或其他逻辑决定;
  • 0:明确要求零个副本;
  • null:是否允许取决于字段 schema 的 nullable 语义和具体资源实现。

对于非 nullable 的字段,null 通常会被拒绝或在处理过程中被视为无效;不能把 null 当成“恢复默认值”的通用写法。

2. 默认值可能来自不同层次

一个最终字段值可能来自:

  1. Kubernetes API 内置默认值;
  2. CRD 的 OpenAPI schema 默认值;
  3. Mutating Admission Webhook;
  4. 控制器后续写入;
  5. 客户端在发送请求前生成的值。

这几类来源不能混为一谈。例如:

  • API 默认的 Deployment.spec.replicas 是 API 处理阶段的行为;
  • Pod 的 status 通常由控制器和 kubelet 相关组件推动;
  • 一个企业自定义 Webhook 加上的标签,不是 Kubernetes 核心 API 的默认值;
  • kubectl run 生成的字段可能来自客户端逻辑,而不是 API Server 默认值。

3. 用 Server Dry Run 观察服务器处理结果

可以使用服务器端 dry-run,让 API Server 执行校验、默认值和 Admission 流程,但不持久化对象:

kubectl create namespace demo

kubectl create deployment web \
  --image=nginx:1.27 \
  -n demo \
  --dry-run=server \
  -o yaml

这个命令的输入是 kubectl create deployment 生成的对象,--dry-run=server 表示请求发送到 API Server,但不真正创建。-o yaml 用于查看服务器返回的对象形态。

也可以对文件执行:

kubectl apply \
  --server-side \
  --dry-run=server \
  --field-manager=demo-operator \
  -f deployment.yaml \
  -n demo \
  -o yaml

前提是:

  • 集群可访问;
  • 当前身份有相应权限;
  • 资源类型支持该 API 操作;
  • Admission Webhook 在 dry-run 场景下行为正确。

服务器 dry-run 不是完全脱离集群的本地模拟。它可能依赖集群中的 schema、Webhook、资源发现和当前对象,因此没有集群连接时无法得到等价结果。


四、字段未知、字段被裁剪与字段版本兼容

API Server 不会因为 YAML 中出现一个键就自动保存它。

对于 Kubernetes 内置资源,API Server 通常根据已知 schema 进行字段校验和处理。未知字段可能:

  • 在默认模式下被忽略或裁剪;
  • 在告警模式下返回 warning;
  • 在严格模式下直接拒绝。

可以显式要求严格校验:

kubectl apply \
  --field-validation=Strict \
  -f deployment.yaml \
  -n demo

如果文件中写成:

spec:
  replcias: 3

replicas 拼成了 replcias,严格校验有机会直接暴露问题;否则错误字段可能不会产生预期效果,最终表现为 Deployment 仍使用默认副本数。

CRD 的行为取决于它的 schema:

  • 具有结构化 OpenAPI schema 的 CRD,可以进行更准确的校验、默认和字段管理;
  • 某些保留未知字段的配置会允许任意数据继续存在;
  • CRD 的 schema 改变可能影响字段剪枝、默认值和 Apply 行为。

因此,“YAML 能被解析”只说明格式层面正确,不说明 GVK 存在、字段合法、类型正确或语义有效。


五、apply 的含义:声明期望状态,而不是执行脚本

命令式操作更接近“执行一个动作”:

kubectl scale deployment web -n demo --replicas=5

声明式操作更接近“提交一个对象状态”:

kubectl apply -f deployment.yaml -n demo

其中 deployment.yaml 描述期望状态。控制器负责把当前状态逐步调整到期望状态。用户不需要在 YAML 中写:

  1. 先创建 ReplicaSet;
  2. 再创建 Pod;
  3. 等待 Pod 启动;
  4. 失败后重试。

这些是控制器的职责。

Apply 与 Create、Replace、Patch 的差异

create

kubectl create -f deployment.yaml -n demo

要求对象不存在。若同名对象已经存在,通常返回 AlreadyExists

replace

kubectl replace -f deployment.yaml -n demo

倾向于用一个完整对象替换当前对象,并涉及 resourceVersion 并发检查。它不适合把“文件中没有写出的字段”简单理解为“保持不变”,也不应在不了解对象完整结构时随意使用。

patch

kubectl patch deployment web \
  -n demo \
  --type=merge \
  -p '{"spec":{"replicas":5}}'

它表达的是对现有对象执行局部修改。这里使用的是 JSON Merge Patch;不同 Patch 类型的数组合并和删除语义不同。

apply

kubectl apply -f deployment.yaml -n demo

它表达的是“这个配置来源希望管理哪些字段以及这些字段应为何值”。Apply 的核心不是单纯上传 YAML,而是根据声明式配置进行合并、删除和冲突处理。


六、Client-Side Apply:三方合并与历史注解

传统的 Client-Side Apply 由 kubectl 在客户端完成主要合并逻辑。它通常依赖三个输入:

  • 当前集群对象 LL,即 live object;
  • 本次配置 CC,即 config;
  • 上一次 Apply 的配置 PP,通常存储在对象的 kubectl.kubernetes.io/last-applied-configuration 注解中。

可以把它抽象为:

R=Apply(P,C,L)R = \operatorname{Apply}(P, C, L)

直觉是:

  • 如果字段在 PP 中、在 CC 中被删除,客户端认为操作者希望删除它;
  • 如果字段在 CC 中,则使用本次配置;
  • 如果字段不属于上一次配置,客户端会结合当前对象决定是否保留。

例如上一次配置为:

spec:
  replicas: 3
  strategy:
    type: RollingUpdate

本次配置变为:

spec:
  replicas: 5

Client-Side Apply 可能把 replicas 更新为 5,并尝试删除上一次配置中由该配置管理的 strategy。但如果 strategy 是其他来源添加的,或者对象经过了复杂的客户端、控制器和手工更新,结果可能不符合直觉。

这种机制存在几个边界:

  • 历史配置保存在对象注解中,会增加对象大小;
  • 其他工具直接更新对象时,客户端的三方信息可能变得不完整;
  • 多个客户端使用不同配置文件操作同一对象时,很难形成清晰的字段所有权;
  • 对复杂列表和并发修改的处理不如服务器端字段管理直观。

可以查看传统 Apply 保存的配置:

kubectl apply view-last-applied deployment/web -n demo

如果对象从未用 Client-Side Apply 创建或更新,该命令可能没有可用结果。这个注解也不是 Server-Side Apply 的字段所有权记录。


七、Server-Side Apply:字段管理器与所有权集合

Server-Side Apply,简称 SSA,由 API Server 负责 Apply 的合并和字段管理。它通过 managedFields 记录不同 Field Manager 管理的字段集合。

执行:

kubectl apply \
  --server-side \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo

其中:

  • --server-side 启用 SSA;
  • --field-manager=platform-team 指定本次请求的字段管理器名称;
  • API Server 根据对象 schema 解析字段路径、合并对象并记录所有权。

查看对象中的管理信息:

kubectl get deployment web -n demo -o yaml

输出中可能包含:

metadata:
  managedFields:
    - manager: platform-team
      operation: Apply
      apiVersion: apps/v1
      fieldsType: FieldsV1
      fieldsV1:
        f:spec:
          f:replicas: {}

实际输出会包含更多字段,并且字段路径采用 Kubernetes 内部的 FieldsV1 表示法。不要把 managedFields 当作用户业务配置;它是 API Server 用于追踪字段管理关系的元数据。

字段所有权不是对象所有权

如果 platform-team 拥有:

spec.replicas

autoscaler 通过其他 API 操作修改该字段,那么这不等于 autoscaler 自动获得了同样的 Apply 所有权,也不等于整个 Deployment 被某一方“锁定”。

字段所有权的粒度可以细到对象字段。不同管理器可以分别拥有:

platform-team: spec.template.spec.containers[name=web].image
autoscaler:    spec.replicas
controller:    status.*

这里的实际路径和归属取决于资源 schema、请求类型以及服务器处理结果,不能仅凭 YAML 缩进推断。


八、SSA 冲突:什么时候拒绝,为什么拒绝

设当前对象中有字段:

spec:
  replicas: 3

并且 platform-team 通过 SSA 管理 spec.replicas。现在 release-bot 执行:

spec:
  replicas: 5

如果这会改变由 platform-team 拥有的字段,API Server 通常返回冲突错误,而不是静默覆盖:

conflicts with "platform-team"

冲突可以形式化为:

Conflict(m,f)=Owner(f)mValuerequest(f)Valuelive(f)\operatorname{Conflict}(m, f) = \operatorname{Owner}(f) \ne m \land \operatorname{Value}_{request}(f) \ne \operatorname{Value}_{live}(f)

其中:

  • mm:当前字段管理器;
  • ff:字段;
  • Owner(f):当前字段所有者;
  • Value_request(f):本次请求中的值;
  • Value_live(f):当前对象中的值。

冲突的直觉是:当前请求不只是“声明自己想要什么”,而是在修改另一个管理器明确管理的字段。

强制解决冲突会转移所有权

可以使用:

kubectl apply \
  --server-side \
  --force-conflicts \
  --field-manager=release-bot \
  -f deployment.yaml \
  -n demo

--force-conflicts 不是“强制让所有字段归我”,而是允许本次 Apply 覆盖发生冲突的字段,并相应调整字段管理关系。它可能把字段所有权从 platform-team 转移给 release-bot

因此,强制冲突通常应当被视为一次明确的管理边界变更,而不是普通重试。自动化流水线盲目使用它,会把并发错误变成静默覆盖。


九、Apply 中的“删除”:字段被省略时会发生什么

声明式配置最容易误解的地方之一是:省略字段有时表示“删除我管理的字段”,有时表示“我不管理这个字段”。

在 SSA 中,假设 platform-team 之前应用过:

spec:
  replicas: 3
  minReadySeconds: 10

后来文件变为:

spec:
  replicas: 5

对于 platform-team 仍然拥有的 minReadySeconds,本次 Apply 可能表示:

platform-team 不再声明这个字段,因此从对象中移除它,或恢复到 API 默认状态。

但如果 minReadySeconds 已经由另一个管理器拥有,platform-team 的省略通常不会直接删除对方管理的字段。

所以需要区分:

  • “文件中没有字段”;
  • “当前管理器不再声明字段”;
  • “对象最终一定没有该字段”。

最后一种结论不能只由 YAML 判断,还要结合 managedFields、schema、默认值和其他管理器。

列表字段尤其依赖 schema

例如容器列表:

containers:
  - name: web
    image: nginx:1.27

Kubernetes 能否把 name 识别为列表项合并键,取决于资源 schema。对于结构化内置资源,containers 通常是按容器名识别的关联列表;但不是所有列表都这样处理。

有些列表是:

  • 原子列表:整个列表作为一个整体管理;
  • 映射列表:按指定键合并;
  • 集合列表:按元素集合处理。

如果两个管理器分别修改一个原子列表,往往会产生更大范围的冲突;不能假设所有 YAML 数组都能逐项合并。


十、控制器与用户同时写对象时的边界

用户 Apply 的对象并不只由用户修改。控制器也会写入对象:

  • Deployment 控制器创建和更新 ReplicaSet;
  • StatefulSet 控制器创建和更新 Pod;
  • Deployment 的 status 由控制器更新;
  • Service Controller 可能根据环境更新相关状态;
  • 自定义 Controller 可能给对象添加标签、注解或 status 字段。

合理的字段边界通常是:

用户或平台管理器:spec
控制器:status
控制器派生资源:由控制器创建的 ReplicaSet、Pod 等

但这不是 API Server 对所有资源强制执行的统一规则。某些控制器会修改 spec 的特定字段,某些 Webhook 也会注入 spec 内容。若用户和控制器同时声明同一个字段,可能出现:

  • SSA 冲突;
  • 控制器不断改回用户值;
  • 用户不断 Apply 覆盖控制器值;
  • 对象在两个值之间反复变化。

这类问题不是 YAML 格式问题,而是管理边界设计错误。诊断时应检查:

kubectl get deployment web -n demo -o yaml
kubectl get deployment web -n demo \
  -o jsonpath='{.metadata.managedFields}' | jq

同时查看控制器或 Webhook 的日志,确定是谁发起了后续修改。


十一、kubectl diff 比较的不是简单的文件文本

kubectl diff 的用途是查看“应用这个配置后,集群对象会有什么变化”:

kubectl diff -f deployment.yaml -n demo

它不是:

diff deployment.yaml downloaded-object.yaml

后者只比较两个文件,无法正确考虑:

  • API Server 默认值;
  • Admission 修改;
  • Apply 合并语义;
  • 当前对象的字段所有权;
  • 资源 schema 对列表的处理;
  • 服务器端 dry-run 结果。

可以把 diff 抽象为:

Δ=Diff(Olive,Owould-be-applied)\Delta = \operatorname{Diff} \left( O_{\text{live}}, O_{\text{would-be-applied}} \right)

其中:

  • OliveO_{\text{live}}:API Server 当前对象;
  • Owould-be-appliedO_{\text{would-be-applied}}:按指定 Apply 语义处理后的服务器端预期对象;
  • Δ\Delta:差异集合。

使用 SSA 语义查看差异:

kubectl diff \
  --server-side \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo

--server-side 的意义是让 diff 使用服务器端 Apply 处理,而不是只在客户端做传统合并推断。对于涉及多个管理器、默认值和列表合并的对象,这通常更接近实际 Apply 结果。

diff 的退出码

在脚本中不能只检查命令是否打印了文本。kubectl diff 通常使用如下约定:

  • 0:没有差异;
  • 1:存在差异;
  • 大于 1:发生错误。

例如:

set +e
kubectl diff \
  --server-side \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo

rc=$?
set -e

case "$rc" in
  0)
    echo "no changes"
    ;;
  1)
    echo "changes detected"
    ;;
  *)
    echo "diff failed: exit code $rc" >&2
    exit "$rc"
    ;;
esac

如果把退出码 1 当成失败,流水线会在“确实存在待发布变更”时误报;如果把所有非零退出码都当成“有差异”,又会掩盖 API Server 不可达、权限不足或资源校验失败。

diff 不等于发布成功

即使 diff 显示变更,也可能在真正 Apply 时失败:

  • 对象在 diff 和 apply 之间被其他管理器修改;
  • 字段所有权发生变化;
  • Webhook 对正式请求和 dry-run 的处理不一致;
  • 权限在两次请求之间发生变化;
  • 资源版本或外部依赖改变。

因此 diff 是预览,不是锁,也不是事务。它不能消除并发窗口。


十二、一个完整的端到端流程

准备命名空间:

kubectl create namespace demo

保存配置为 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 80

第一步:确认资源 schema

kubectl explain deployment.spec.replicas
kubectl explain deployment.spec.template.spec.containers

kubectl explain 使用 API Server 提供的资源 schema 解释字段。它能帮助发现字段名称和层级错误,但不能代替 Admission、权限和业务验证。

第二步:服务器端预演

kubectl apply \
  --server-side \
  --dry-run=server \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo \
  -o yaml

这一步不会创建 Deployment,但会让服务器尝试处理对象。若 YAML 中有未知字段、类型错误或 Webhook 拒绝,通常会在这一步暴露。

第三步:查看差异

kubectl diff \
  --server-side \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo

因为对象尚不存在,输出通常会显示新增内容;具体格式由 kubectl diff 使用的 diff 程序决定,不应在脚本中依赖固定文本。

第四步:实际 Apply

kubectl apply \
  --server-side \
  --field-manager=platform-team \
  -f deployment.yaml \
  -n demo

如果成功,API Server 保存 Deployment,并记录字段管理信息。之后 Deployment 控制器会创建 ReplicaSet,ReplicaSet 再创建 Pod。

第五步:检查最终对象和运行状态

kubectl get deployment web -n demo -o yaml
kubectl get pods -n demo -l app=web
kubectl rollout status deployment/web -n demo

要区分两个层次:

  • kubectl get deployment -o yaml 检查 API 对象是否按预期保存;
  • kubectl rollout status 检查控制器是否已经把运行状态推进到可用。

Apply 成功只表示 API Server 接受了对象,不表示镜像一定能拉取、Pod 一定能调度或应用一定健康。


十三、两个管理器的冲突示例

首先由 platform-team 管理副本数:

cat > replicas-platform.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 2
EOF

kubectl apply \
  --server-side \
  --field-manager=platform-team \
  -f replicas-platform.yaml

实际使用时,若只提交这个片段,Deployment 还必须已经存在,并且对象其他必需字段已经满足 API 约束。为了避免片段缺少必需字段,生产配置通常使用完整对象或针对已存在对象的经过验证的 Patch/Apply 文件。

然后另一个管理器提交:

cat > replicas-release.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 5
EOF

kubectl apply \
  --server-side \
  --field-manager=release-bot \
  -f replicas-release.yaml

如果 platform-team 仍拥有 spec.replicas,第二次操作可能得到冲突。正确处理方式不是默认加上 --force-conflicts,而是先决定管理边界:

  • 由平台统一管理副本数:release-bot 不应声明 spec.replicas
  • 由发布系统管理副本数:明确让发布系统接管,并评估平台配置是否需要移除该字段;
  • 使用 HPA:不要让静态 YAML 和 HPA 同时持续管理 spec.replicas,否则会出现反复覆盖。

这里的关键不是命令参数,而是同一字段只能有一个稳定的声明式管理者。


十四、如何诊断 Apply 或 Diff 问题

1. 先确认 GVK 和资源发现

kubectl api-resources | grep -i deployment
kubectl explain deployment
kubectl explain deployment.spec

常见错误包括:

  • kind 写错;
  • apiVersion 使用了不存在或不兼容的版本;
  • 把资源名写成 Kind;
  • CRD 尚未安装;
  • 使用了旧版本字段。

kubectl get deployment 使用的是资源名,而 YAML 中的 kind: Deployment 使用的是 Kind。二者相关但不是同一个字符串体系。

2. 区分权限错误与字段错误

kubectl auth can-i get deployments -n demo
kubectl auth can-i patch deployments -n demo
kubectl auth can-i update deployments -n demo

Apply 可能需要的权限取决于具体请求路径和客户端模式。服务器端 Apply 通常涉及 Apply/Patch 语义;实际权限应以集群版本和资源授权结果为准,不能假定“能 get 就一定能 apply”。

3. 检查最终对象而不是只看本地文件

kubectl get deployment web -n demo -o yaml

重点检查:

  • API Server 是否补充了默认值;
  • Webhook 是否注入标签、注解或容器;
  • 字段是否被剪枝;
  • managedFields 中由谁管理;
  • status 是否由控制器正常推进。

4. 检查事件与控制器状态

kubectl describe deployment web -n demo
kubectl get events -n demo --sort-by=.lastTimestamp

如果 Apply 成功但 Pod 没有运行,问题可能在:

  • 镜像拉取;
  • 调度;
  • ServiceAccount 或 RBAC;
  • 资源配额;
  • Pod Security;
  • 节点或网络;
  • readiness probe。

这些不是 Apply 合并失败,应沿着控制器和 Pod 生命周期继续诊断。

5. 检查字段冲突

kubectl get deployment web -n demo -o json \
  | jq '.metadata.managedFields'

应关注:

  • manager
  • operation
  • apiVersion
  • 被管理的字段路径;
  • 最近更新该字段的请求来源。

不要只看某个 manager 的名字判断责任。相同名字可能被不同自动化系统复用;应结合审计日志、工作流和请求时间确认来源。


十五、版本与兼容性边界

apiVersion 不是客户端版本

YAML 中的:

apiVersion: apps/v1

表示对象 API 的 Group 和 Version,不表示 kubectl 的版本。

kubectl、API Server 和资源 API 之间存在兼容矩阵。通常建议 kubectl 与 API Server 保持官方支持范围内的版本差异;过旧或过新的客户端可能:

  • 无法识别资源;
  • 使用不同的默认行为;
  • 使用不同的字段校验逻辑;
  • 对 Apply 或 Diff 参数支持不同。

可以检查:

kubectl version
kubectl api-versions
kubectl api-resources

API 版本迁移可能改变语义

从旧 API 版本迁移到新版本,不只是替换字符串。字段可能:

  • 改名;
  • 从 beta 变为 stable;
  • 被删除;
  • 改变默认值;
  • 改变列表合并和字段管理 schema;
  • 由服务端转换为存储版本。

生产迁移前应在目标版本集群上执行服务器端 dry-run、diff 和实际测试,并确认控制器、Webhook、CRD schema 与客户端都支持目标版本。

云厂商差异

托管 Kubernetes 集群可能额外提供:

  • 云厂商 Admission Webhook;
  • 特有的 StorageClass、LoadBalancer 行为;
  • 节点标签和调度约束;
  • 安全策略;
  • 版本升级节奏;
  • 对某些 API 或控制器的定制。

因此,服务器端 dry-run 和 diff 必须在目标集群执行。仅在本地 Kind、Minikube 或另一个云集群中验证成功,不代表生产集群一定有相同结果。


十六、生产中最重要的取舍

1. 是否使用 Server-Side Apply

SSA 适合:

  • 多个管理器需要同时管理同一对象的不同字段;
  • 希望 API Server 显式检测字段冲突;
  • 使用 GitOps、平台控制器或多个自动化系统协同管理资源;
  • 不希望依赖巨大的 last-applied 注解。

Client-Side Apply 仍然存在兼容场景,但在多个写入者并存时,历史注解和三方合并会使行为更难推断。迁移到 SSA 时,应明确字段管理器名称,并检查已有对象的 managedFields

2. 是否把默认值写入 YAML

把默认值显式写入配置有两个相反效果:

  • 好处:配置意图更明确,diff 不容易因为默认值而令人困惑;
  • 风险:一旦 API 默认值或平台策略改变,显式值会继续强制旧行为;
  • 风险:配置文件可能开始声明本来不应由该团队管理的字段;
  • 风险:不同 Kubernetes 版本的默认行为被固定成了某个版本的结果。

是否显式声明,应根据字段的管理责任决定,而不是机械地把服务器返回对象全部复制回 Git。

3. 是否强制冲突

--force-conflicts 适用于经过审查的所有权转移,而不适合作为流水线默认参数。其风险是:

  • 覆盖其他系统刚刚提交的值;
  • 改变字段所有权;
  • 隐藏配置边界冲突;
  • 让控制器和发布系统进入反复修改状态。

如果冲突频繁出现,优先重新划分字段管理范围,而不是增加强制覆盖次数。


结语:把四个层次分开,Apply 和 Diff 才可预测

可以用以下关系总结整个机制:

  1. YAML 是输入表示,负责表达对象数据;
  2. API Server 根据 GVK、schema、默认值和 Admission 生成可接受的对象;
  3. Apply 根据声明式写入语义合并对象,SSA 进一步记录字段所有权并检测冲突;
  4. Diff 比较当前对象与服务器处理后的预期对象,但不提供并发锁,也不保证后续 Apply 必然成功。

因此,遇到配置问题时应按顺序提问:

  • YAML 是否被正确解析,值的类型是否正确?
  • apiVersionkind 和字段 schema 是否匹配?
  • 服务器是否补充或修改了默认值?
  • 当前命令使用的是 Client-Side Apply、Server-Side Apply、Patch 还是 Replace?
  • 目标字段由哪个 Field Manager 管理?
  • diff 使用的语义是否与实际 Apply 一致?
  • Apply 成功后,控制器是否已经把 status 和派生资源推进到目标状态?

只有把这些层次分别验证,才不会把“文件看起来正确”“命令执行成功”“对象已经按预期运行”错误地当成同一件事。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。