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 转换 来回切一遍,至少能提前抓出缩进和语法问题。