跳到主内容
d.devtul.fun
EN
运维 · 2026-08-20

写给 Kubernetes 的 YAML:写出凌晨三点也不会崩的 manifest

Kubernetes 的 manifest 就是 YAML。这话既是好消息也是坏消息:格式本身很简单,但每个字段都有含义,一处对不上,凌晨三点你就得盯着怎么都起不来的 Pod 发愁。这篇文章逐字段走一遍最小可用配置——一个 Deployment 和它前面的 Service——再讲几个生产环境里真会发生的错误。

一个最小的 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: shop
  labels:
    app: checkout
spec:
  replicas: 3
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: registry.internal/shop/checkout:1.4.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /live
              port: 8080
            periodSeconds: 15

这是一个完整的、能部署的对象。下面讲容易写错的部分。

apiVersion 不是装饰

apiVersion 告诉 API Server 用哪套 schema 来校验,它和 kind 绑在一起。Deployment 属于 apps/v1;裸的 v1 留给 ConfigMap、Service 这类核心对象。Deployment 写成 apps/v1,Service 写成 v1,写反了服务器会在看其余内容之前就拒掉这个对象。

labels 和 selector 必须匹配——经典的"改了没反应"事故

selector.matchLabels 声明了这个 Deployment 管哪些 Pod。Pod 模板里的 labels 必须包含 selector 里的每一对键值。一旦两边漂移,Deployment 会创建出它不管理的 Pod;更常见的是你改了模板的 label,结果 kubectl apply 静默失败——因为 selector 是不可变的。

错误写法:

selector:
  matchLabels:
    app: checkout
template:
  metadata:
    labels:
      app: checkout
      tier: web     # 看着无害,但 selector 就此对不上了

正确做法是让模板的 labels 严格包含 selector 的 labels。这条出错时,症状通常是"我改了配置却什么都没发生"——因为控制器拒了这次 apply,而没人去看报错。

requests 和 limits 在生产里不是可选项

resources 块看着像可选,集群也确实允许你空着不写。别这么干。没有 requests,调度器不知道该预留多少 CPU 和内存,Pod 就会全堆到一台节点上直到它崩。没有 limits,一个失控的进程就能把同节点上的其他容器全饿死。

requests 是调度器保证给到的量,limits 是上限。按容器分别设置,尤其要把内存的 limit 写上——内存不可压缩,超限的容器会被 OOM 杀掉而不是被限流。

liveness、readiness、startup 三种探针不是一回事

把它们搞混,就会得到"明明在跑却在一直返回错误"的服务。

  • livenessProbe 回答"进程还活着吗?"失败的话,Kubernetes 就重启容器。把它指向只在进程真卡死时才失败的地方。
  • readinessProbe 回答"这个 Pod 能接流量了吗?"失败的话,Pod 仍留在 Service 的端点里,只是不再收到请求。数据库还没连好时就该用它。
  • startupProbe 回答"应用启动完了吗?"它保护启动慢的应用,避免在还没起来之前就被 liveness 探针杀掉。

最常见的错:把 liveness 和 readiness 指到同一个端点。数据库短暂不可用时,readiness 正确地报"未就绪",但 liveness 也跟着失败,于是 Kubernetes 把本来好好的容器硬重启——把一次小波动变成一次完整故障。

ConfigMap 与 Secret:两种挂载方式

两者都能以环境变量或卷内文件的形式注入。文件通常更安全,因为很多场景下改文件不用重启,而且不会把密钥泄露到 printenv 的输出里。

env:
  - name: LOG_LEVEL
    valueFrom:
      configMapKeyRef:
        name: checkout-config
        key: log_level
volumeMounts:
  - name: secrets
    mountPath: /etc/secrets
    readOnly: true

Secret 只是 base64 编码,默认情况下静态不加密——把它当明文对待,并限制谁可以 kubectl get secret。

kubectl apply、create、replace 的区别

  • kubectl create -f 从零创建,对象已存在就报错。适合一次性初始化,不适合反复迭代。
  • kubectl apply -f 是声明式的:它计算与线上对象的差异,只补丁真正变了的地方。日常改配置就该用它。
  • kubectl replace -f 拿文件里的整份对象覆盖线上。要是有人在线上改了点什么而你的文件里没有,那次改动就没了。

上线前先试:--dry-run 与 diff

有两个命令救过的凌晨告警,比任何仪表盘都多:

kubectl apply -f deploy.yaml --dry-run=client    # 本地校验,不碰集群
kubectl diff -f deploy.yaml                      # 显示到底会改什么

--dry-run=client 在你动集群之前先确认 manifest 能解析、格式正确。kubectl diff 把与线上对象的差异打出来,让你按回车之前就看清影响范围。

多环境别靠复制粘贴

一旦你用手维护 dev.yaml、staging.yaml、prod.yaml,它们迟早漂移。用 Kustomize 的 overlay 或 Helm 的 values 保一份 base、再叠加差异:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../base
patches:
  - target:
      kind: Deployment
      name: checkout
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 5

namespace 忘写的坑

如果 metadata 里没写 namespace,对象就会落到当前上下文的默认命名空间——通常是 default,那里没有 NetworkPolicy、没有资源配额、也没有命名规范。务必显式写上;更好的做法是放进 Kustomization 的上下文里,让你根本没机会忘。

配套的 Service

apiVersion: v1
kind: Service
metadata:
  name: checkout
  namespace: shop
spec:
  selector:
    app: checkout
  ports:
    - port: 80
      targetPort: 8080

注意 Service 的 selector 必须匹配 Pod 的 labels(不是 Deployment 的 selector)——规则一样,对象不同。这里写错,Service 的端点数就是零,每个请求都超时。

想把 manifest 在没集群的情况下先草稿并校验,可以用 JSON ⇄ YAML 转换 来回切一遍,至少能提前抓出缩进和语法问题。

继续阅读