课程目录(第 5 章 / 共 33 章)
用 YAML 描述 Pod:清单结构详解
从零写一个完整的 Pod 清单,掌握 YAML 缩进、四段骨架、labels、resources 与多容器 Pod 的写法。
学完这一章,你将能够
- ✓独立写出一个语法正确、可直接 apply 的 Pod 清单
- ✓说清 apiVersion、kind、metadata、spec、status 各自是什么
- ✓用 kubectl explain 和 kubectl diff 查字段、看变更
为什么把 Pod 写成 YAML
上一章用 kubectl run 创建 Pod,快是快,但有几个致命缺点:命令里塞不下复杂的配置,没法版本管理,也没法复现。真实工作中,所有资源都写在 YAML 清单里,提交进 Git,用 `kubectl apply` 下发。
YAML 描述的是「期望状态」,而不是「操作步骤」。这正好对应第一章讲的声明式思路:你不说「先做什么再做什么」,只说「我要一个长这样的 Pod」,剩下的交给控制器。
这一章我们不引入新概念,只把上一章的 Pod 完整地写成 YAML,并学会读它、改它、验证它。
YAML 缩进规则:只有空格,没有 Tab
YAML 用缩进来表达层级,规则只有三条,但踩坑的人最多:
- 只能用空格,不能用 Tab。编辑器里 Tab 是不可见的,报错信息也往往只说「第几行有问题」,所以请把编辑器设成「Tab 转 2 空格」。
- 同一层级缩进必须完全一致。两个空格是最常见的约定,本课程统一用两个空格。
- 列表项用 `- ` 开头,它和上一行的键属于同一层级,所以
-通常要比键多缩进两个空格。
一个最小例子:
spec:
containers:
- name: web
image: nginx:1.27containers 是键,它的值是一个列表;- name: web 是列表的第一项,image 和 name 是同一项里的两个字段,所以和 name 对齐。写错一个空格,name 就会变成 containers 的兄弟字段,语义完全不同。
清单的四段骨架与 status
任何 Kubernetes 清单都长这样:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
# 期望状态写在这里| 字段 | 作用 | 取值说明 |
|---|---|---|
| apiVersion | 这个资源属于哪个 API 组和版本 | 核心资源用 v1,工作负载用 apps/v1,Ingress 用 networking.k8s.io/v1 |
| kind | 资源类型 | Pod、Deployment、Service |
| metadata | 身份信息 | name 必填,namespace 决定归属,labels 用于筛选 |
| spec | 期望状态 | 每种资源都不一样,Pod 里主要是容器定义 |
还有第五段 status,它只出现在 `kubectl get -o yaml` 的输出里,绝对不要手写。status 是集群回写的事实:Pod IP、所在节点、容器是否就绪。如果你在清单里写了 status,apiserver 会直接忽略它——因为事实只能由集群产生。
整张清单的流向可以画成一张图,左边是你负责的,右边是集群负责的:
你写进 Git 的清单(期望状态)
┌──────────────────────────────────┐ ┌────────────────────────────────┐
│ apiVersion: v1 │ │ status: │
│ kind: Pod │ │ phase: Running │
│ metadata: │ ─────► │ podIP: 10.244.1.7 │
│ name: web │ apply │ hostIP: 172.18.0.2 │
│ labels: {app: web} │ ◄───── │ conditions: Ready=True │
│ spec: │ 控制器 │ containerStatuses: [...] │
│ containers: [web] │ │ │
└──────────────────────────────────┘ └────────────────────────────────┘
▲ ▲
└── 你负责写、提交、apply └── 你只负责读,写它没用apiVersion + kind 决定「交给谁来处理这份清单」,metadata 回答「它是谁」,spec 回答「它该长什么样」,status 回答「它现在实际是什么样」。前四段你写,第五段集群写。
动手:写一个完整的 Pod 清单
从文件创建 Pod
新建 pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: demo
labels:
app: web
tier: frontend
spec:
containers:
- name: web
image: nginx:1.27
ports:
- name: http
containerPort: 80
env:
- name: APP_ENV
value: dev
- name: LOG_LEVEL
value: info
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi如果上一章的 demo 命名空间已经删掉了,先补上:
kubectl create namespace demo下发并验证:
kubectl apply -f pod.yaml
kubectl get pod web -n demo
kubectl get pod web -n demo -o yamlkubectl apply 返回 pod/web created 就说明成功;再执行一次会返回 pod/web unchanged,这就是 apply 的幂等性——同样的清单执行多少次,结果都一样。
逐个看这些字段:
ports是一个列表,每一项里写containerPort,还可以给端口起个name(后面 Service 会引用这个名字)。containerPort只是声明,不写也能跑,但写了更清晰。env注入环境变量,每一项是name+value。从 ConfigMap、Secret 注入的写法在第 9 章 ConfigMap 与 Secret。resources.requests是调度依据:调度器只把 Pod 放到资源足够的节点上。limits是运行上限:CPU 超了会被限流,内存超了会被杀掉(OOMKilled)。50m表示 0.05 核。
改完清单再下发一次,就会看到 apply 帮你更新对象:
kubectl apply -f pod.yaml用 kubectl explain 查字段
YAML 字段记不住很正常,kubectl explain 把字段文档直接打到终端里,比翻网页快:
kubectl explain pod
kubectl explain pod.spec.containers
kubectl explain pod.spec.containers.resources --recursive第一条列出 Pod 的顶层字段;第二条列出容器支持的所有字段(image、command、env、ports、volumeMounts 等);加 --recursive 会展开到最深层级,输出比较长,适合配合 grep 找字段名。
下面这份「常用路径清单」值得先抄到自己的笔记里,路径就是资源名加点号加字段名:
| 路径 | 能查到什么 |
|---|---|
pod | Pod 的顶层字段(spec、metadata、status 都在这里) |
pod.spec.containers | 容器支持的全部字段:image、command、args、env、ports、volumeMounts |
pod.spec.containers.env | 环境变量的两种写法:value 与 valueFrom |
pod.spec.containers.resources | requests / limits 的单位与格式 |
pod.spec.volumes | Pod 能挂哪些卷类型 |
pod.spec.nodeSelector | 怎么把 Pod 绑到指定标签的节点 |
deployment.spec.strategy | 滚动更新的 maxSurge / maxUnavailable |
service.spec.ports | port / targetPort / nodePort 的区别 |
ingress.spec.rules | host 与 path 的写法 |
任何一条路径都可以再往后接字段名,kubectl explain 会一直往下钻。报错里出现 unknown field 时,就用它反查正确拼写。
另一个技巧是让 kubectl 帮你生成骨架:
kubectl run tmp --image=nginx:1.27 --dry-run=client -o yaml--dry-run=client 表示只在本地生成对象、不发给集群,把输出重定向到文件就是一个可用的模板。
labels 与 selector
labels 是挂在对象上的键值对,本身不产生任何行为,它的作用是被选中。Service、Deployment、NetworkPolicy 都通过 selector 找目标对象,这是 Kubernetes 把资源「连接」起来的主要方式。
kubectl get pods -n demo --show-labels
kubectl get pods -n demo -l app=web
kubectl get pods -n demo -l 'tier in (frontend)'--show-labels把标签显示出来,确认清单里的labels真的写进去了。-l app=web是等值选择器,只返回标签匹配的 Pod。-l 'tier in (frontend)'是集合选择器,支持in、notin、exists。
给已有对象加标签也可以直接用命令:
kubectl label pod web -n demo env=dev最关键的一点:下一章 Service 的 spec.selector 必须和 Pod 的 labels 精确匹配(键和值都要一致)。如果 Service 选不到后端,第一件事就是对比这两处的标签。标签写错时 Kubernetes 不会报错,只会静默地「找不到 Pod」,这是新手最难发现的坑之一。
多容器 Pod 与 sidecar
Pod 里可以放多个容器,它们共享网络命名空间(同一个 IP,可以互相用 localhost 访问)和挂载的存储卷,但各自有独立的文件系统和进程空间。
最常见的模式叫 sidecar:主容器负责业务,辅助容器负责日志收集、指标暴露或流量代理。下面这个例子给 nginx 配了一个每 10 秒请求一次本地服务的探针容器:
apiVersion: v1
kind: Pod
metadata:
name: web-with-sidecar
namespace: demo
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
- name: probe
image: busybox:1.36
command:
- sh
- -c
- "while true; do wget -q -O- http://localhost:80 >/dev/null; sleep 10; done"注意 probe 容器里用的是 http://localhost:80,而不是 Pod 的 IP——因为两个容器共享网络命名空间。把上面的清单保存为 sidecar.yaml,下发后看 probe 容器的输出:
kubectl apply -f sidecar.yaml
kubectl logs web-with-sidecar -n demo -c probe实际生产中,sidecar 更多是服务网格的数据面代理或日志采集器。这里只需要记住概念:同一 Pod 内的容器是「同生共死」的,它们一起被调度,也一起被销毁。
常见坑
四个 YAML 相关的典型报错
- 缩进错误:报错类似
error converting YAML to JSON: yaml: line 8: mapping values are not allowed in this context,或者字段被解析到了错误的层级。先跑kubectl apply -f pod.yaml --dry-run=client做本地语法校验,再用编辑器显示空白字符逐行核对,重点检查是否混入了 Tab。 - 字段名拼错:报错类似
unknown field "spec.containerss",新版 kubectl 会提示strict decoding error。对照kubectl explain pod.spec的输出核对字段名,注意是containers不是container、是image不是images。 - 镜像拉不下来:清单语法没错,但 Pod 停在
ImagePullBackOff。用kubectl describe pod web -n demo看 Events,确认镜像名、标签和仓库权限。 - `ports` 与 `containerPort` 混淆:
ports必须是列表,每一项里写containerPort。写成ports: 80会报cannot unmarshal number into Go struct field ... ports;写成port: 80则会被当成未知字段。
写清单时最容易撞到的几种情况,先对着这张表找:
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
error converting YAML to JSON: yaml: line 8 | 缩进不一致,或混进了 Tab | 编辑器打开「显示空白字符」逐行看 | 同一层级统一 2 空格,Tab 全部替换 |
unknown field "spec.containerss" | 字段名拼错 | 对比 kubectl explain pod.spec 的输出 | 改成正确字段名 |
cannot unmarshal number into Go struct field ... ports | 把 ports 写成了数字 | 看报错里点名的字段 | 改成列表,每项写 containerPort |
mapping values are not allowed in this context | 值里有未加引号的冒号,或一行里塞了两个键 | kubectl apply -f pod.yaml --dry-run=client 定位行号 | 给值加引号,把两个键拆成两行 |
Pod 停在 ImagePullBackOff | 镜像名或 tag 写错、私有仓库无凭证 | kubectl describe pod web -n demo 看 Events | 换存在的稳定 tag,必要时补 imagePullSecrets |
Pod Running 但 READY 0/1 | 就绪探针没通过 | kubectl describe pod web -n demo 看 probe 相关 Events | 修探针的路径、端口或初始延迟 |
Pod 反复重启、Reason: OOMKilled | limits.memory 给小了 | kubectl describe pod web -n demo 看 Last State | 调大 limits.memory,或修内存泄漏 |
| 标签加上了却筛不出来 | -l 的键值写错,或 Pod 在别的命名空间 | kubectl get pods -n demo --show-labels | 用 --show-labels 的原始值复制粘贴 |
两个提高效率的习惯
第一,清单文件按 <资源类型>-<名字>.yaml 命名(如 pod-web.yaml),一个文件一个资源,方便 diff 和排查。第二,改完清单先用 kubectl diff -f pod.yaml 看这次会改什么,确认无误再 apply——它会把本地文件和集群里现存对象的差异打印出来(依赖系统里的 diff 命令,macOS 和常见 Linux 发行版都自带)。
自测:清单里写了 status,为什么集群完全不理你?(点击展开答案)
因为 status 描述的是事实,不是期望。你写进 spec 的是「我要一个长这样的 Pod」,集群负责把它变成现实,再把现实回写进 status。如果 status 也能由你写,声明式的模型就崩了——控制器无法再判断「现实和期望是否一致」。所以 apiserver 收到清单时只认前四段,status 一律由 kubelet 与控制器填写,你只能读。
自测:标签写错了,为什么 kubectl 不报错?(点击展开答案)
因为标签只是挂在对象上的键值对,本身没有任何约束——它不要求唯一、不要求存在,也没规定「必须被谁选中」。真正的匹配发生在读取侧:Service 或 Deployment 的 selector 找不到带这个标签的 Pod 时,结果只是「后端列表为空」,这在 Kubernetes 看来是完全合法的状态。所以这类错误只能靠自己主动对比发现:kubectl get pods --show-labels 看实际标签,kubectl get svc -o wide 看 SELECTOR 列。
自测:同一个 Pod 里的两个容器,为什么能用 localhost 互相访问?(点击展开答案)
因为 Pod 内的所有容器共享同一个网络命名空间:它们看到的是同一张网卡、同一个 IP、同一份端口空间。所以 sidecar 用 http://localhost:80 就能访问主容器,而不需要知道 Pod 的 IP;反过来说,两个容器也不能监听同一个端口,否则后启动的那个会报端口被占用。文件系统则是各自独立的,要共享文件必须显式挂同一个卷。
小结
- YAML 用空格缩进,列表项以
-开头;混入 Tab 是最高频的语法错误。 - 清单的四段骨架是
apiVersion、kind、metadata、spec;status由集群回写,不要手写。 kubectl apply -f下发、kubectl diff -f预览变更、kubectl delete -f删除,是清单工作流的三条主命令。labels是被选择的一方,selector是选择的一方,两者必须精确匹配。- 一个 Pod 可以放多个共享网络与存储的容器,sidecar 是最常见的用法。
练习
- 把清单里的
image改成nginx:1.27-alpine,用kubectl diff -f pod.yaml看看差异,再apply,最后用kubectl get pod web -n demo -o jsonpath='{.spec.containers[0].image}'确认。 - 给 Pod 加一个
env变量GREETING=hello,apply 后kubectl exec进去执行echo $GREETING验证。 - 故意把
containers下面的缩进多写一个空格,观察报错信息,再改回来。
单个 Pod 显然不够用:它挂了不会自动重建,也没法滚动更新。下一章我们用 第 6 章 Deployment 来管理多个 Pod 副本。
相关章节:第 4 章 第一个 Pod 讲了怎么用命令快速起一个 Pod,速查表 里有本章命令的一页速览。