课程目录(第 5 章 / 共 33 章)
课程/最小工作单元

用 YAML 描述 Pod:清单结构详解

5 章 / 共 33·16 分钟·入门YAML清单labels

从零写一个完整的 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 用缩进来表达层级,规则只有三条,但踩坑的人最多:

  1. 只能用空格,不能用 Tab。编辑器里 Tab 是不可见的,报错信息也往往只说「第几行有问题」,所以请把编辑器设成「Tab 转 2 空格」。
  2. 同一层级缩进必须完全一致。两个空格是最常见的约定,本课程统一用两个空格。
  3. 列表项用 `- ` 开头,它和上一行的键属于同一层级,所以 - 通常要比键多缩进两个空格。

一个最小例子:

yaml
spec:
  containers:
    - name: web
      image: nginx:1.27

containers 是键,它的值是一个列表;- name: web 是列表的第一项,imagename 是同一项里的两个字段,所以和 name 对齐。写错一个空格,name 就会变成 containers 的兄弟字段,语义完全不同。

清单的四段骨架与 status

任何 Kubernetes 清单都长这样:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  # 期望状态写在这里
字段作用取值说明
apiVersion这个资源属于哪个 API 组和版本核心资源用 v1,工作负载用 apps/v1,Ingress 用 networking.k8s.io/v1
kind资源类型PodDeploymentService
metadata身份信息name 必填,namespace 决定归属,labels 用于筛选
spec期望状态每种资源都不一样,Pod 里主要是容器定义

还有第五段 status,它只出现在 `kubectl get -o yaml` 的输出里,绝对不要手写status 是集群回写的事实:Pod IP、所在节点、容器是否就绪。如果你在清单里写了 status,apiserver 会直接忽略它——因为事实只能由集群产生。

整张清单的流向可以画成一张图,左边是你负责的,右边是集群负责的:

text
   你写进 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

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 命名空间已经删掉了,先补上:

bash
kubectl create namespace demo

下发并验证:

bash
kubectl apply -f pod.yaml
kubectl get pod web -n demo
kubectl get pod web -n demo -o yaml

kubectl 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 帮你更新对象:

bash
kubectl apply -f pod.yaml

用 kubectl explain 查字段

YAML 字段记不住很正常,kubectl explain 把字段文档直接打到终端里,比翻网页快:

bash
kubectl explain pod
kubectl explain pod.spec.containers
kubectl explain pod.spec.containers.resources --recursive

第一条列出 Pod 的顶层字段;第二条列出容器支持的所有字段(imagecommandenvportsvolumeMounts 等);加 --recursive 会展开到最深层级,输出比较长,适合配合 grep 找字段名。

下面这份「常用路径清单」值得先抄到自己的笔记里,路径就是资源名加点号加字段名:

路径能查到什么
podPod 的顶层字段(spec、metadata、status 都在这里)
pod.spec.containers容器支持的全部字段:image、command、args、env、ports、volumeMounts
pod.spec.containers.env环境变量的两种写法:valuevalueFrom
pod.spec.containers.resourcesrequests / limits 的单位与格式
pod.spec.volumesPod 能挂哪些卷类型
pod.spec.nodeSelector怎么把 Pod 绑到指定标签的节点
deployment.spec.strategy滚动更新的 maxSurge / maxUnavailable
service.spec.portsport / targetPort / nodePort 的区别
ingress.spec.ruleshostpath 的写法

任何一条路径都可以再往后接字段名,kubectl explain 会一直往下钻。报错里出现 unknown field 时,就用它反查正确拼写。

另一个技巧是让 kubectl 帮你生成骨架:

bash
kubectl run tmp --image=nginx:1.27 --dry-run=client -o yaml

--dry-run=client 表示只在本地生成对象、不发给集群,把输出重定向到文件就是一个可用的模板。

labels 与 selector

labels 是挂在对象上的键值对,本身不产生任何行为,它的作用是被选中。Service、Deployment、NetworkPolicy 都通过 selector 找目标对象,这是 Kubernetes 把资源「连接」起来的主要方式。

bash
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)' 是集合选择器,支持 innotinexists

给已有对象加标签也可以直接用命令:

bash
kubectl label pod web -n demo env=dev

最关键的一点:下一章 Service 的 spec.selector 必须和 Pod 的 labels 精确匹配(键和值都要一致)。如果 Service 选不到后端,第一件事就是对比这两处的标签。标签写错时 Kubernetes 不会报错,只会静默地「找不到 Pod」,这是新手最难发现的坑之一。

多容器 Pod 与 sidecar

Pod 里可以放多个容器,它们共享网络命名空间(同一个 IP,可以互相用 localhost 访问)和挂载的存储卷,但各自有独立的文件系统和进程空间。

最常见的模式叫 sidecar:主容器负责业务,辅助容器负责日志收集、指标暴露或流量代理。下面这个例子给 nginx 配了一个每 10 秒请求一次本地服务的探针容器:

yaml
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 容器的输出:

bash
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 ... portsports 写成了数字看报错里点名的字段改成列表,每项写 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 RunningREADY 0/1就绪探针没通过kubectl describe pod web -n demo 看 probe 相关 Events修探针的路径、端口或初始延迟
Pod 反复重启、Reason: OOMKilledlimits.memory 给小了kubectl describe pod web -n demoLast 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 wideSELECTOR 列。

自测:同一个 Pod 里的两个容器,为什么能用 localhost 互相访问?(点击展开答案)

因为 Pod 内的所有容器共享同一个网络命名空间:它们看到的是同一张网卡、同一个 IP、同一份端口空间。所以 sidecar 用 http://localhost:80 就能访问主容器,而不需要知道 Pod 的 IP;反过来说,两个容器也不能监听同一个端口,否则后启动的那个会报端口被占用。文件系统则是各自独立的,要共享文件必须显式挂同一个卷。

小结

  • YAML 用空格缩进,列表项以 - 开头;混入 Tab 是最高频的语法错误。
  • 清单的四段骨架是 apiVersionkindmetadataspecstatus 由集群回写,不要手写。
  • kubectl apply -f 下发、kubectl diff -f 预览变更、kubectl delete -f 删除,是清单工作流的三条主命令。
  • labels 是被选择的一方,selector 是选择的一方,两者必须精确匹配。
  • 一个 Pod 可以放多个共享网络与存储的容器,sidecar 是最常见的用法。

练习

  1. 把清单里的 image 改成 nginx:1.27-alpine,用 kubectl diff -f pod.yaml 看看差异,再 apply,最后用 kubectl get pod web -n demo -o jsonpath='{.spec.containers[0].image}' 确认。
  2. 给 Pod 加一个 env 变量 GREETING=hello,apply 后 kubectl exec 进去执行 echo $GREETING 验证。
  3. 故意把 containers 下面的缩进多写一个空格,观察报错信息,再改回来。

单个 Pod 显然不够用:它挂了不会自动重建,也没法滚动更新。下一章我们用 第 6 章 Deployment 来管理多个 Pod 副本。

相关章节:第 4 章 第一个 Pod 讲了怎么用命令快速起一个 Pod,速查表 里有本章命令的一页速览。