先说个真实经历。之前帮朋友迁移一个跑在单节点 K8S 上的微服务环境整套服务从测试服务器挪到云上 ECS按计划要“不停服、不丢数据”结果到了落地那天卡住的不是容器镜像不是网络策略而是一份 Deployment 的 YAML——selector 里少了一个标签新 Pod 建出来了Service 却怎么也转发不过去流量。那一刻我意识到K8S 里真正决定“能不能跑起来”的往往不是那些高大上的调度策略而是每天都要写的 YAML 文件。这篇是 K8S 系列第五篇专门聊聊 YAML 文件的解析和编写把小到缩进、大到对象关联的关键点一次讲透。很多初学者会把“写 YAML”理解成“写配置文件”但在 K8S 里YAML 承担的职责远不止如此。它是你和 Kubernetes API Server 之间的唯一沟通语言你的每次部署、升级、回滚本质上都是把一个 YAML 文件交给系统去解析、校验、执行。YAML 语法看似简单真正写起来却充满细节一个空格、一个引号、一个字段层级出错轻则文件解析失败重则服务悄悄运行在错误配置上排查起来相当磨人。这篇文章我会从一个实际使用者的角度拆解 K8S 中 YAML 的解析机制、核心字段的编排逻辑、常见报错场景以及一套可以照抄的多环境管理思路。不管你是刚接触容器编排、准备考 CKA还是正在维护生产集群这篇文章都能帮你少踩几个坑。1. 为什么 K8S 把 YAML 当成“通用语言”1.1 声明式 API 与 YAML 的关系K8S 之所以选择 YAML 作为主要描述格式根本原因在于它的设计哲学是“声明式”而不是“命令式”。用 Docker 的时候你敲docker run是在告诉系统“做什么”启动一个容器、映射端口、挂载卷。而用 K8S 时你提交一份 YAML是在告诉系统“最终状态是什么”我要 3 个副本、镜像版本是 v1.2.0、端口 8080、健康检查路径是 /healthz。至于中间怎么创建、怎么调度、怎么滚动更新那是控制面的事你不需要关心。这份“期望状态”就是 YAML 文件。Kubernetes 的控制面组件比如 kube-controller-manager 里的各种 controller会持续比对“当前状态”和“期望状态”一旦发现偏差就主动纠正。这个机制要求 YAML 字段表达必须是精确的、结构化的、可被机器解析的任何模糊都会让控制器无从下手。这种设计带来的好处是你可以在代码仓库里维护所有部署描述像管理代码一样管理环境出问题可以回滚到任意历史版本。这也是为什么“K8S 和 Docker 有什么区别”这类问题总会被拿出来讨论——Docker 解决“怎么跑容器”K8S 解决“如何编排容器”而编排的入口和依据就是 YAML。1.2 为什么不是 JSON 或 TOML很多人会问JSON 不是更适合机器解析吗TOML 不是更易读吗K8S 最终选择 YAML有几个现实原因。YAML 是 JSON 的超集所有合法的 JSON 都可以被 YAML 解析器读取这意味着历史上遗留的 JSON 配置不需要重写兼容性有保障。YAML 的注释能力是 JSON 没有的生产环境里每个字段后面写上一句“为什么这么配”三个月后回来改配置时能救你一命。YAML 的缩进结构让层级关系一目了然比 JSON 的嵌套括号直观得多适合人在终端里快速阅读和修改。TOML 虽然也强调可读性但它的设计更偏向扁平化键值对表达 K8S 那种深层次的嵌套结构比如spec.template.spec.containers[].resources.limits时路径会变得非常冗长远不如 YAML 的树形缩进自然。当然YAML 也有被人诟病的地方最典型的就是缩进敏感、类型推断带来的坑。但 Kubernetes 选择了它就意味着我们这些使用者必须把 YAML 的语法规则彻底吃透才能不被它绊倒。1.3 YAML 解析在 K8S 中要经过哪些环节你在终端执行kubectl apply -f deployment.yaml这一条命令背后YAML 经历了三个阶段。第一阶段是客户端解析。kubectl 读取本地文件先把 YAML 转成 JSON 结构因为 Kubernetes API Server 的接口只接受 JSON 格式。这也是为什么 YAML 报错信息里经常出现converting YAML to JSON这类字样。第二阶段是服务端校验。API Server 收到 JSON 后根据资源的 OpenAPI Schema 做字段校验检查 apiVersion、kind、必填字段、字段类型是否正确。第三阶段是持久化与控制器处理。校验通过后资源对象被写入 etcd对应的 controller 发现新的期望状态开始创建 Pod、Service 等实际资源。理解这三个阶段很重要因为不同阶段的错误排查方式完全不同。语法错误在客户端就暴露了字段校验错误会在 API Server 返回而运行期问题比如镜像拉不下来、探针失败则要看 Pod 的事件和日志。很多新手拿到一个 Pod 一直 Pending以为是 YAML 写错了反复改格式其实问题出在调度或镜像层面。2. 先过语法关YAML 解析绕不开的几道坎2.1 缩进永远用空格不要用 Tab这是 YAML 领域最著名的一条规则缩进必须使用空格不能使用 Tab。哪怕你在其他地方用 Tab 习惯了写 YAML 时也要刻意换过来。大多数现代编辑器VS Code、IntelliJ默认会把 Tab 转成空格但如果你用 vim 直接编辑线上文件很容易掉进这个坑。K8S 的 YAML 文件惯例是使用两个空格作为一级缩进不同层级之间递增两个空格。缩进的作用是表达字段之间的嵌套关系比如apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3这里的metadata比apiVersion多两个空格表示它是 Deployment 的顶层字段之一name比metadata多四个空格表示它是metadata的子字段。如果你把name的缩进弄成和apiVersion同级那么解析器就会认为name变成了顶层字段整个对象结构全乱了。判断缩进是否正确的技巧是同一层级的字段缩进必须完全一致。如果labels下面有app和env两个键它们必须对齐到同一个列位置否则 YAML 解析器会认为它们的层级不同产生mapping values are not allowed in this context一类的错误。2.2 键值对与数据类型冒号后面必须有空格YAML 的基本结构是键值对写法是key: value。注意冒号后面必须有一个空格否则解析器会把整行当成一个字符串。这是新手最常见的语法错误之一尤其在你从别处复制内容、或者输入法自动补全导致格式异常时特别容易发生。同样容易被忽视的是数据类型推断。YAML 没有显式的类型声明解析器会根据值的形态自动推断。比如replicas: 3会被解析成整数replicas: 3会被解析成字符串。大部分场景下 K8S 的 Schema 会帮你校验类型但有些坑值得提前知道值写成on、off、yes、no时YAML 1.1 规范会把它们当作布尔值K8S 使用的解析器大部分情况下不会这么激进但为了安全起见期望字符串时主动加引号。数字过大的时候解析器可能转成科学计数法比如容器端口写成4841000000000000000000就会出现问题。值里有特殊字符冒号、井号、大括号等时要用引号包起来否则解析结果会偏离预期。我写 YAML 时有一个习惯凡是值里面可能出现特殊符号的一律加引号尤其是镜像版本、环境变量值、启动参数这些内容。这种“防御式写法”能避免一半以上的解析问题。2.3 多行文本|与的使用场景ConfigMap 和 Job 的 YAML 里经常需要注入一段比较长的脚本或配置文件内容这时候会用到多行文本的两种表示方式字面块|和折叠块。apiVersion: v1 kind: ConfigMap metadata: name: app-config data: init.sql: | CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100) ); README.md: 这是一段被折叠成一行 的文本换行符会被替换成空格。|会保留文本中的换行符适合写脚本、SQL、配置文件这类换行有意义的场景。则会把换行符替换为空格适合普通文本、日志模板。这里的括号、分号、引号都会被原样保留不需要转义这也是 YAML 处理复杂文本内容时的优势。但要注意多行文本块里的内容缩进必须比冒号所在行更深而且所有行的缩进要保持一致。如果你在块里面不小心混入 Tab解析依然会失败。我一般把这类内容单独复制到编辑器里用格式化插件先处理一遍。2.4 一个典型的语法错误拆解以热词里频繁出现的报错为例yaml: line 12: mapping values are not allowed in this context这个错误的意思是解析器在第 12 行发现了一个它认为是“键值对”但上下文不允许出现的内容。最常见的触发原因是冒号后少了空格或者一个子字段的缩进比父字段还浅。比如spec: template: metadata: labels: app: nginx spec: containers:这里spec的缩进比metadata浅YAML 解析器就会困惑觉得spec应该结束上一层的 mapping 结构却又没有合法的键名。这种错误在手工编辑大文件时几乎无法避免好在一行行检查缩进就一定能找到问题。另一个常见错误是bad indentation of a mapping entry意思是缩进不一致往往是复制粘贴时编辑器把空格和 Tab 混在一起。我的建议是一旦遇到这类报错先把文件丢进支持 YAML lint 的编辑器里格式化一遍大多数情况下问题立刻暴露。3. 核心资源 YAML 逐一解剖3.1 Deployment最常用的工作量描述Deployment 是 K8S 中最常用的工作负载资源它负责管理无状态应用的部署、滚动更新和回滚。一个标准的 Deployment YAML 长这样apiVersion: apps/v1 kind: Deployment metadata: name: backend-service namespace: production labels: app: backend tier: backend spec: replicas: 3 selector: matchLabels: app: backend template: metadata: labels: app: backend version: v1.2.0 spec: containers: - name: backend-container image: registry.example.com/backend:v1.2.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 protocol: TCP env: - name: DB_HOST value: mysql-service resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10几个关键点需要展开说。metadata.name是资源的唯一标识同一个 namespace 内不能重名。metadata.namespace指定资源所属的命名空间不写就默认放在 default。spec.replicas是期望的副本数控制器会确保实际运行的 Pod 数量与这个值一致。spec.selector.matchLabels是 Deployment 用来“管理”哪些 Pod 的依据。这个字段极其重要它必须能够匹配到template里定义的 Pod 标签。换句话说selector 是 Deployment 和 Pod 之间的关联纽带。如果 selector 里的标签和 template 里的标签不一致Deployment 会拒绝创建或者创建了 Pod 却无法纳管导致资源混乱。spec.template是 Pod 的模板里面定义了 Pod 的元信息和规格。注意这里的metadata.labels必须覆盖 selector 里的所有标签键值对这是 K8S 强制性校验规则。很多初学的人不理解为什么selector里的标签要重复写在 template 里简单理解就是Deployment 用它来认领 PodPod 用这些标签标识自己两者必须对齐。resources.requests和resources.limits是资源配额。requests 是调度器做节点选择时考虑的“最低需求”limits 是运行时容器不能超过的上限。CPU 的单位是m千分之一核就是 1m100m 等于 0.1 核内存单位是MiMebibyte256Mi 约等于 268MB。合理设置 requests 和 limits 能有效避免单节点上多个 Pod 互相争抢资源但 limits 设置得太紧会引发 CPU 节流或 OOMKilled后面排查章节会细说。探针Probe是保障服务可用性的重要机制。readinessProbe 告诉系统“这个 Pod 可以接收流量了”livenessProbe 告诉系统“这个容器还活着如果不响应就重启”。对于初次接触的人我建议先从 readinessProbe 用起把健康检查路径写好至少能保证流量不会打到启动未完成的实例上。3.2 Service把流量带到 Pod 的桥Deployment 管好 Pod 后Service 负责提供稳定的访问入口。因为 Pod 的 IP 是动态的重启后就变了Service 通过标签选择器找到一组 Pod为它们提供一个虚拟 IPClusterIP并在集群内部做负载均衡。apiVersion: v1 kind: Service metadata: name: backend-service namespace: production spec: type: ClusterIP selector: app: backend ports: - port: 80 targetPort: 8080 protocol: TCPspec.selector.app决定这个 Service 会把流量转发给哪些 Pod。它的逻辑是所有带有appbackend标签的 Pod 都是这个 Service 的后端成员。这也就解释了我开头说的那个事故——当时的 Service 选择器写的是app: backend但 Deployment 的 Pod 模板里标签写成了name: backend结果 Pod 一直健康Service 却找不到任何后端。ports.port是 Service 对外暴露的端口targetPort是后端 Pod 容器监听的端口。请求到达 Service 的 80 端口后会被转发到后端 Pod 的 8080 端口。这个映射关系几乎是每个 K8S 初学者都会搞混的地方建议默念一句话Service 的 port 是给别人用的targetPort 是给 Pod 用的。Service 的type有几种ClusterIP仅集群内访问、NodePort每个节点上开一个端口通过节点 IP 访问、LoadBalancer云厂商负载均衡器接入。在单节点或自建环境里NodePort 是最直接的对外暴露方式开发调试阶段尤其方便。3.3 ConfigMap 与 Secret配置与敏感信息的解耦容器镜像一旦构建出来里面的配置文件就固化了。为了不改镜像就能调整配置K8S 提供 ConfigMap 和 Secret 两个资源。ConfigMap 用于存放非敏感配置比如配置文件内容、环境变量Secret 用于存放密码、Token、证书等敏感信息。apiVersion: v1 kind: ConfigMap metadata: name: app-config data: LOG_LEVEL: info redis-config: | maxmemory 256mb maxmemory-policy allkeys-lruapiVersion: v1 kind: Secret metadata: name: db-secret type: Opaque data: DB_PASSWORD: cGFzc3dvcmQxMjM stringData: DB_USER: admin注意 Secret 的data字段里的值默认是 Base64 编码后的字符串所以直接用明文写在data里会解析成功但存入 etcd 后是编码状态。如果你不想手动编码可以使用stringData字段它允许直接写明文API Server 会帮你自动编码。ConfigMap 和 Secret 可以通过环境变量注入也可以通过 Volume 挂载成文件。挂载成文件的好处是配置更新后容器里的文件内容会自动更新虽然进程需要自己决定是否重新加载。这个机制对于微服务中常见的“动态配置刷新”场景很有用比修改镜像再重新发布要高效得多。3.4 对象之间的关联方式labels 和 selector 是核心线索前面三个资源讲完你会发现一个反复出现的东西labels 和 selector。K8S 的对象不是孤立的Deployment 通过 selector 管理 PodService 通过 selector 选择 PodNetworkPolicy、Ingress 也依赖标签进行流量控制。可以说labels 和 selector 是 K8S 资源对象之间建立关联的“暗号系统”。设计标签时一定要注意收敛和规范不要随手乱写。比如用app表示应用名、tier表示层次、version表示版本、env表示环境。命名规范一致之后无论是人工排查还是运维脚本处理都会顺畅很多。我最常踩的坑是给 Pod 模板加了太多不必要的标签导致 Service 的选择器在匹配时“误伤”了本不该被选中的 Pod。标签尽量精简够用就好。4. 完整实操从零写一份能跑起来的 YAML4.1 场景设定单节点上部署一个后端服务假设我们有一个简单的后端服务镜像地址是registry.example.com/backend:v1.2.0容器监听 8080 端口服务需要通过 30080 端口从集群外部访问。我们打算在单节点 K8S 集群上完成部署这也是热词里提到的“单节点 K8S 上的微服务整套环境”最常见的起点。在这个过程中我会带着你一步步从空白文件写到可用配置每一步都说明为什么这么写。4.2 第一步用 kubectl 生成基础模板写 YAML 不用从零开始敲。kubectl 有一个非常好用的功能kubectl create deployment可以生成一个最小可用的 Deployment YAML。我用--dry-runclient -o yaml让它只输出内容不真正创建资源kubectl create deployment backend-service \ --imageregistry.example.com/backend:v1.2.0 \ --port8080 \ --dry-runclient \ -o yaml这条命令输出的内容大致是apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: backend-service name: backend-service spec: replicas: 1 selector: matchLabels: app: backend-service template: metadata: labels: app: backend-service spec: containers: - image: registry.example.com/backend:v1.2.0 name: backend-service ports: - containerPort: 8080 resources: {}你会发现 selector 里的标签和 template 里的标签已经是对齐的这就是我刚才强调的“Deployment 用 selector 管理 Pod”的默认实现。把它保存为deployment.yaml接下来在此基础上修改。用kubectl create生成模板的好处是K8S 官方帮你保证了基础结构一定是合法的你只需要在上边做增量修改而不是从零开始记忆一堆字段。这也是高效写 YAML 的最佳实践。4.3 第二步按实际需求补齐字段基础模板有几个问题副本数只有 1没有资源限制没有健康检查没有环境变量。我们逐步补齐。副本数调整spec: replicas: 3加上资源限制和健康检查spec: template: spec: containers: - name: backend-service image: registry.example.com/backend:v1.2.0 imagePullPolicy: IfNotPresent resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 5这里有几个字段值得解释。imagePullPolicy有三个取值Always每次拉取、IfNotPresent本地没有才拉、Never只用本地镜像。单节点测试环境建议写成IfNotPresent避免每次启动都要从远端仓库拉镜像节省时间生产环境则要按镜像标签策略设置latest标签配 Always 会带来不确定性。readinessProbe里的httpGet表示用 HTTP GET 请求去探测容器的健康状态。initialDelaySeconds是容器启动后等待多久才开始探测给应用留出初始化时间。periodSeconds是探测间隔。如果探针路径写错或者时间设置过短Pod 会一直处于未就绪状态不会接收流量这是“服务起不来”的另一个常见原因。再加上环境变量和端口配置env: - name: SPRING_PROFILES_ACTIVE value: production - name: DB_HOST value: mysql-service如果配置项多了也可以改用 ConfigMap 统一管理这里先保持简单。4.4 第三步编写 Service 并验证整体效果有了 Deployment我们再写一个 Service 文件。文件命名为service.yamlapiVersion: v1 kind: Service metadata: name: backend-service spec: type: NodePort selector: app: backend-service ports: - port: 80 targetPort: 8080 nodePort: 30080这里的关键是selector.app: backend-service必须和 Deployment 模板里的 Pod 标签app: backend-service一致。另外nodePort的取值范围是 30000-32767如果你不写K8S 会自动分配一个。然后执行部署kubectl apply -f deployment.yaml kubectl apply -f service.yaml查看状态kubectl get pods kubectl get svc正常的话你会看到 3 个 Pod 处于 Running/ReadyService 的EXTERNAL-IP是nodes此时通过任意节点的IP:30080就能访问到后端服务。我在单节点环境里验证过无数次这套组合拳是“快速验证 YAML 是否正确”的最佳路径。4.5 额外一步把整套 YAML 纳入版本管理当资源文件越来越多的时候单个文件逐个 apply 显然不现实。我的习惯是把同一套环境的所有 YAML 文件放在同一个目录下然后用kubectl apply -f ./manifests/一次性应用整个目录。对于跨环境部署再把每个环境的差异单独用 overlay 目录管理这在后面第 6 部分会展开。还有一个非常实用的技巧给 YAML 文件加上自定义注解比如负责人、变更单号、用途说明。这些信息不会影响 K8S 解析但半年后回来看的时候你能迅速知道这份文件是谁在什么背景下写的。5. 必踩的坑常见报错与排查实录5.1 解析类报错先分清“格式”还是“语义”先看这条经典报错error: error parsing configmap.yaml: error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context这属于“格式错误”也就是 YAML 语法不合法。后面跟的line 5是关键线索直接跳到第 5 行检查。最可能的原因有冒号后面没有空格、Tab 和空格混用、缩进层级错乱。另一条常见报错error: error validating deployment.yaml: error validating data: ValidationError(Deployment.spec): missing required field selector in io.k8s.api.apps.v1.DeploymentSpec这属于“语义错误”YAML 本身是合法的但不符合 K8S 的资源 Schema。比如 Deployment 必须要有spec.selector没有就会在 API Server 校验阶段被拒绝。解决方法是查对应资源的文档或kubectl explain把缺失字段补齐。要快速定位是哪种错误可以执行kubectl apply --dry-runclient -f deployment.yaml这条命令不会真正调用 API Server只做客户端本地解析如果这一步就报错说明是语法问题如果通过而kubectl apply报错那就是服务端校验问题。5.2 运行期报错YAML 过了Pod 却没跑起来YAML 文件解析成功、资源也创建了不代表 Pod 就能正常运行。基本流程是kubectl get pods看状态kubectl describe pod name看事件kubectl logs name看容器输出三步走基本能定位 90% 的问题。常见的几个状态和原因ImagePullBackOff镜像拉取失败。可能是镜像不存在、仓库地址写错、私有仓库没有配置拉取凭证。用kubectl describe pod查看事件里的 Error 信息比猜有用得多。也可能是imagePullPolicy: Always导致每次拉取都失败。CrashLoopBackOff容器启动后立即退出被反复重启。用kubectl logs看容器输出大多数时候会发现启动参数错误、连接不到数据库等。还有一种情况是 livenessProbe 的initialDelaySeconds太短应用还没起来就被杀死导致无限重启。Pending调度器找不到合适的节点。单节点集群常见原因是节点资源不足或者 Pod 设置了无法满足的资源 requests。kubectl describe node可以查看节点资源分配情况。CreateContainerConfigError容器配置有问题最常见的是 Secret 或 ConfigMap 引用不存在。检查kubectl describe pod里的 Events会直接告诉你缺哪个 key。5.3 我的排查顺序与经验踩过足够多的坑之后我形成了一套自己的排查流程先确认 YAML 语法kubectl apply --dry-runclient -f xxx.yaml不过这一步直接把文件丢 IDE 里格式化更快。再确认资源被正确创建kubectl get deployment,svc,pods确认资源都在预期 namespace。看 Pod 状态和事件kubectl describe pod name重点关注Events段的红字。确认 Service 后端kubectl get endpoints service-name如果 endpoints 列表为空说明 labels 和 selector 没有匹配上。最后看日志kubectl logs pod-name --previous--previous可以查看容器上一次崩溃前的日志。这个顺序每一步都基于前一步的结果向下推导能大幅缩短排查时间。切记不要跳过 describe 直接看日志很多配置问题根本没有容器日志可看。6. YAML 中的“高级玩法”多环境管理与模板化6.1 Helm 和 Kustomize 解决什么问题日常开发中开发和生产的 YAML 之间一定存在差异镜像版本不同、副本数不同、资源配置不同。如果每个环境都维护一份完整 YAML很容易出现 copy-paste 后漏改一个字段的尴尬局面。这正是 Helm 和 Kustomize 存在的意义。Kustomize 的思路是“基础文件 增量补丁”。你维护一份基础 Deployment然后在 overlay 目录里声明“生产环境副本数改成 5、镜像版本改成 v1.2.0”剩下的字段全从基础文件继承。Helm 的思路是“模板文件 变量注入”。在模板里用{{ .Values.replicas }}占位然后通过一个values.yaml为不同环境提供不同变量值。Helm 的管理体验更接近编程语言的“配置中心”适合复杂微服务场景Kustomize 更轻量也没有引入额外的依赖适合管理中小规模的部署文件。6.2 从一份 YAML 到一套环境的落地建议在单节点 K8S 上部署一套微服务环境时我通常建议按命名空间做隔离比如manifests/ base/ namespace.yaml backend-deployment.yaml backend-service.yaml mysql-service.yaml redis-service.yaml overlays/ dev/ kustomization.yaml production/ kustomization.yamlbase 目录存放所有服务的无差异配置overlays 里只放差异。这样你可以用一条命令给整个环境打补丁并应用kubectl apply -k overlays/production/这套模式在“单节点 K8S 整套微服务环境搭建”和“迁移到云 ECS”两个场景下都极其好用。因为你的资源定义是模板化的迁移只需要改镜像仓库地址和 NodePort不用重写所有文件。6.3 YAML 不是 K8S 专属但 K8S 的 YAML 值得认真学热词里出现了yolov10 yaml、esphome yaml platform、x-anylabeling 导出 yaml这些和 K8S 无关的技术栈也都在使用 YAML。YAML 本身是一种通用数据序列化格式不同框架对同一个 YAML 文件的解析方式不同字段含义完全不同。但语法层面的东西是通用的。比如你写 YOLO 模型配置文件时yaml只是描述神经网络结构的参数写 ESPHome 配置时YAML 描述硬件引脚、传感器和自动化逻辑而在 K8S 里YAML 描述的是集群资源的期望状态。理解“YAML 语法是公共的字段语义是领域专属的”这个区别能让你快速在不同的技术栈之间迁移。有人问“没有 GitLab 的 YAML 是否依然能触发 Runner”这本身也是 YAML 在 CI/CD 领域的典型应用。YAML 语法本身没问题只是 GitLab Runner 只认gitlab-ci.yml这个文件并且遵循它规定的字段语义。如果你把文件放在错误的位置或者字段名写错Runner 就不会被触发。这也再次说明解析方式决定了一件事语义决定另一件事学习和排查时必须分清楚。最后再分享一个个人习惯我平时写 K8S 的 YAML无论多急都会在最后加一道检查把文件丢进kubectl apply --dry-runserver -f里验证一次。客户端 dry-run 只能确认语法服务端 dry-run 会做完整的 Schema 校验能发现很多客户端看不见的字段问题。这个习惯帮我避免过好几次“配置看起来没问题apply 也不报错但运行结果完全不对”的奇怪故障。另外建议你在每个 YAML 文件头部加一个注释块写明这是一份什么资源、变更日期、改动意图。kubectl 完全无视这些注释但半年后回来改配置的你会感激当时的自己。这也是我认为 YAML 比 JSON 更适合写 K8S 配置的重要理由之一。
