
第一次拿到官方 Ingress 控制器的部署 YAML 时我心里是有点发怵的。这份官方 Ingress 控制器 YAML 解析我以最常见的 ingress-nginx 控制器发布包为例文件不算长但里面同时出现了 Namespace、ServiceAccount、ClusterRole、Deployment、Service、Job、ValidatingWebhookConfiguration 等一堆资源而且每段之间还有依赖关系不是敲一条 apply 命令就完事。这篇文章想做三件事把这份官方 YAML 拆成几条主线讲清楚每段为什么存在把关键字段逐个对照解释讲清哪些可以安全修改再把我在实际环境里踩过的坑和排查思路放进来。适合正在被 Ingress 控制器整得头大的同学也适合那些控制器已经跑起来、但从没仔细读过 YAML 的人。1. 动手拆解前先建立全图官方 YAML 到底塞了哪些资源很多人第一次接触 Ingress 控制器会下意识认为它就是一个 Deploymentapply 完就结束了。但官方 YAML 的目的不是“启动一个容器”而是“交付一个完整可用的控制平面”所以它一口气声明了下面的资源资源类型名称示例一句话作用Namespaceingress-nginx把所有控制器相关资源隔离到独立命名空间ServiceAccountingress-nginx给控制器 Pod 一个集群内身份ClusterRole / ClusterRoleBindingingress-nginx授予控制器跨命名空间读取 Ingress、Service、Endpoint 等资源的权限ConfigMapingress-nginx-controller保存 Nginx 控制器的全局配置比如请求体大小限制、超时时间Deploymentingress-nginx-controller控制器的核心工作负载里面定义镜像、启动参数、探针Serviceingress-nginx-controller对外暴露 80/443 端口云环境通常用 LoadBalancer 类型Serviceingress-nginx-controller-admission给集群内的 Admission Webhook 提供访问入口Jobingress-nginx-admission-create / patch自动生成 Webhook 证书并注入配置ValidatingWebhookConfigurationingress-nginx-controller-admission在创建 / 更新 Ingress 资源时对请求做校验IngressClassnginx声明这个控制器负责处理哪个 class 的 Ingress这张表基本就是整个 YAML 的骨架。我的习惯是拿到文件后先按“命名空间与身份 - 权限 - 配置 - 工作负载 - 流量入口 - 校验链路”的顺序去读而不是从上往下硬啃。因为资源之间是有依赖关系的Pod 需要 ServiceAccount 才能启动ServiceAccount 需要 ClusterRoleBinding 才能拿到权限Webhook 需要 Job 先产出证书IngressClass 需要和 Deployment 的启动参数对得上。把这条依赖链先画在脑子里后面每一段看起来都会顺很多。1.1 为什么控制器要独占一个 Namespace官方 YAML 的第一段通常就是 Namespace。很多初学者会困惑直接把控制器放到 kube-system 不行吗可以但不推荐。控制器的 ConfigMap、ServiceAccount、Role 都是围绕这个命名空间展开的单独放一个ingress-nginx命名空间最大的好处是“边界清晰”删除这个命名空间控制器相关的所有资源一起消失不会残留一堆没有归属的配置RBAC 的 Role 绑定也可以精确限制在这个命名空间内避免把权限放大到整个集群。另一个容易忽略的点是命名空间的清理价值。控制器在运行中会写入 ConfigMap、Lease、Event 等资源这些在故障排查时很有用但长期跑下来会产生不少历史数据。如果你哪天想把控制器彻底卸载重装kubectl delete namespace ingress-nginx一条命令就能收拾干净而不用逐个资源去查 dependencies。这也是官方把 Namespace 单独列为 YAML 第一段的原因之一。1.2 资源多不代表流程复杂Apply 以后发生了什么官方文档给的安装命令一般就是kubectl apply -f deploy.yaml但这不代表 apply 一次后所有组件立刻进入可用状态。实际流程是kubelet 先创建 Namespace、ServiceAccount、RBAC 等基础资源然后 Deployment 控制器开始创建 PodPod 启动成功后下面的 Service 和 Endpoint 才会就绪。Webhook 这部分更特殊它依赖 Job 生成的证书 Secret而 Job 本身又是控制器集群里的普通任务所以刚 apply 完的几十秒内创建 Ingress 资源可能会碰到 Webhook 调用失败此时不是环境坏了而是证书还没准备好等 Job 跑完通常会自行恢复。理解这个流程有个实际好处排障时不用一上来就怀疑 YAML 写错。先看 Job 有没有完成再看 Webhook 是否注册成功最后才去看控制器 Pod 的日志。顺序对了排查速度会快很多。2. RBAC 权限段控制器的“眼睛”和“手”分别在哪官方 YAML 里 RBAC 部分有两个层次一个是作用于整个集群的 ClusterRole一个是只在ingress-nginx命名空间内生效的 Role。我第一次看的时候觉得有点重复后来才明白它们的职责完全不同。ClusterRole 里列出的 apiGroups 大致包括核心组空组、networking.k8s.io、coordination.k8s.io等resources 涉及 configmaps、endpoints、events、ingresses、ingressclasses、nodes、pods、secrets、services。核心 verbs 是 get、list、watch。这很好理解控制器是个集群级组件它要监听所有命名空间里的 Ingress、Service、Endpoint 和 Secret然后把它们翻译成 Nginx 配置。没有 list/watch 权限控制器就瞎了没有 get 权限它就无法在生成配置时去读取具体的 Secret 内容。2.1 为什么权限看着给得很大却仍然是合理的有人看到 ClusterRole 里一堆资源会觉得权限过大但要注意Ingress 控制器的本质决定了它必须拥有这些能力。举个例子一个 Ingress 规则引用了一个类型为 TLS 的 Secret控制器在生成 Nginx 配置时要把证书内容读出来所以 Secrets 的 get 权限是绕不开的。再比如控制器需要把后端 Service 对应的 Pod IP 集合算出来这就必须 watch Endpoints或者是 endpointslice否则后端节点变化时它无法及时更新配置。这不是开发者偷懒而是这个组件的功能边界本身就需要这么大的视野。真正需要小心的是官方 YAML 里的 Ingress 控制器没有把ingresses/status的 update 权限漏掉。有了这个权限控制器才能在 Ingress 创建后把负载均衡器的地址回写到status.loadBalancer字段里。如果自己手改 RBAC 时把这个动词删掉会出现一个很隐蔽的现象Ingress 本身能访问但你在kubectl get ingress里永远看不到 ADDRESS 那一列自动化工具依赖这个字段做域名解析时就会失败。2.2 命名空间内的 Role管好自己的一亩三分地Namespace 内的 Role 重点管的是 configmaps、events、leases、secrets、services。这里有两类关键操作第一控制器要把自身运行状态写为事件方便后续排障第二控制器多副本之间要抢占 Lease 锁实现 leader election避免多个副本同时往同一个 ConfigMap 里写 Nginx 配置导致混乱。这些资源全部集中在ingress-nginx命名空间里所以用 Role 而不是 ClusterRole范围控制得刚刚好。这里想提醒一句如果你为了省事把所有 RBAC 权限全部塞进一个 ClusterRole一般不会立刻出问题但一旦集群里有多个租户或团队这种权限放大迟早会给你带来审计麻烦。保持 ClusterRole 管“跨命名空间只读集群资源”、Role 管“命名空间内写操作”的拆分是官方设计里很值得学习的地方。3. Deployment 主程序每个参数都是运维者必须看懂的开关Deployment 是整份 YAML 里信息密度最高的段落。对于不想深究原理的人来说镜像版本改一改副本数改一改也就够用了。但真正排障时你会发现启动参数里隐藏了大量关键信息。控制器容器的启动参数一般长这样/nginx-ingress-controller --election-idingress-nginx-leader --controller-classk8s.io/ingress-nginx --ingress-classnginx --configmap$(POD_NAMESPACE)/ingress-nginx-controller --validating-webhook:8443 --validating-webhook-certificate/usr/local/certificates/cert --validating-webhook-key/usr/local/certificates/key --publish-service$(POD_NAMESPACE)/ingress-nginx-controller --tcp-services-configmap$(POD_NAMESPACE)/tcp-services --udp-services-configmap$(POD_NAMESPACE)/udp-services3.1 核心启动参数逐项拆解--ingress-classnginx和--controller-classk8s.io/ingress-nginx是控制器的“身份证”。它告诉集群我只处理ingressClassName为 nginx 的 Ingress 资源并且我的 controller 类标识是k8s.io/ingress-nginx。对应地官方 YAML 里会创建一个 IngressClass 对象其spec.controller字段也写的是k8s.io/ingress-nginx。两边必须对得上Ingress 才会被这个控制器接管。--configmap$(POD_NAMESPACE)/ingress-nginx-controller指定了控制器读取全局配置的 ConfigMap 位置。这里的$(POD_NAMESPACE)是一个变量引用实际值由环境变量传入后面会说到。如果 ConfigMap 里的某个 key 写错了控制器不会崩溃但对应的配置项不会生效而且可能在日志里只留下一条 Warning 级别的消息排查时需要留意。--publish-service$(POD_NAMESPACE)/ingress-nginx-controller的作用是告诉控制器“把哪个 Service 的地址写到 Ingress 的 status 里”。如果你把控制器部署后Ingress 一直没有显示 IP优先检查一下这个参数指向的 Service 是否存在以及 Service 的 LoadBalancer 是否已经成功分配到 IP。--tcp-services-configmap和--udp-services-configmap是用于非 HTTP 流量的。比如你想通过 Ingress 控制器暴露 MySQL 或者 UDP 服务需要创建独立的 TCP/UDP ConfigMap并在启动参数里指定好位置。官方 YAML 默认不会创建这两个 ConfigMap需要的时候手动建即可参数路径已经留好了。3.2 环境变量为什么不能直接写死Deployment 里通常有这么一段env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace这里直接从 Downward API 取 Pod 的名字和命名空间而不是写死成字符串是因为控制器需要拿这两个信息拼出自己的“身份”。最典型的是 leader election多副本场景下每个副本都要知道自己是哪个 Pod才能去抢 Lease 锁否则锁的持有者身份就会错乱。另外一个作用是日志控制器把 Pod 名打印出来排障时才能精确定位是哪个副本在报错。3.3 探针、端口与多副本的配合控制器容器一般会暴露四个端口80 和 443 是业务入口8443 是 Webhook10254 是健康检查和 metrics。livenessProbe 和 readinessProbe 都指向 10254 的/healthz但含义不同readiness 探针失败时Pod 会被从 Service 的负载均衡列表里摘掉流量不会打过来但进程不会被杀死liveness 探针失败时Kubernetes 会认为容器卡死直接重启 Pod。所以看到 Pod 反复 CrashLoopBackOff可以先看是哪种探针挂了再决定排查方向。副本数设置方面官方 YAML 一般把 replicas 设为 2目的是高可用。但要注意Ingress 控制器是“多副本、单主写”的工作模式多个副本同时接收流量请求但只有一个 leader 负责把 Nginx 配置写进 ConfigMap 并触发 reload。所以如果你为了省资源把副本数改成 1功能不受影响改成 2 或更多时你会在日志里看到attempting to acquire leader lease之类的字样这是正常的不是报错。4. 流量入口 Service从 LoadBalancer 到 NodePort 的取舍官方 YAML 里通常有两个 Service。一个是ingress-nginx-controller负责对外暴露 80/443另一个是ingress-nginx-controller-admission是给集群内部 Webhook 用的 ClusterIP 服务。这两个如果搞混排障时会比较痛苦尤其是当你在云环境创建了一个 Webhook Service 却怎么都调不通的时候。4.1 默认 LoadBalancer 模型是怎么工作的ingress-nginx-controller的默认类型是 LoadBalancer。在公有云环境里控制器创建后云平台的负载均衡器会自动被创建然后分配一个公网 IP。这就是 Ingress 出现 ADDRESS 的来源之一控制器通过--publish-service找到这个 Service把它的 IP/域名写进 Ingress 的 status 里。本地环境或者裸机环境没有云厂商的 LB 组件apply 完官方 YAML 后这个 Service 会一直处于 pending 状态。此时可以选择把它改成 NodePort让每个节点暴露一个对外端口来承载流量。改法就是把spec.type从LoadBalancer改成NodePort然后在浏览器里用http://节点IP:端口验证。需要注意NodePort 的默认端口范围是 30000-32767如果你要固定端口得显式指定nodePort字段否则每次重建服务端口都可能变。4.2 externalTrafficPolicy: Local 与客户端真实 IP还有一个字段值得专门说如果你的 Service 加了externalTrafficPolicy: Cluster默认值流量进到节点后会被二次转发来源 IP 会变成节点 IP导致后端应用拿不到真实客户端 IP。把该字段改成Local流量到达节点后直接转发给本节点的 Pod源 IP 能被保留。但 Local 也有代价如果某节点上没有控制器 Pod而负载均衡器又把请求调度到了这个节点请求会被丢弃或延迟。所以在使用 Local 模式时最好配合 Pod 反亲和性让每个节点尽量都有控制器 Pod或者干脆用 DaemonSet 形式部署控制器每个节点跑一份。这也是生产环境常用的组合DaemonSet hostNetwork Service externalTrafficPolicy: Local既能保持源 IP又能确保每个节点都能直接处理流量。4.3 三种暴露方式怎么选方式优势劣势适用场景LoadBalancer Service云环境自动分配公网 IP配置少裸机无法自动创建 LB需要额外组件公有云、托管集群NodePort Service任何环境都能用改 YAML 一处即可端口范围有限制节点变化影响入口本地测试、自建机房DaemonSet hostNetwork每个节点都能直接转发网络路径最短需要管理端口冲突、节点数量多时暴露面大生产自建、要求保留源 IP我自己在自建环境里用得比较多的是第三种但如果你只是想快速验证功能NodePort 是最快的一条路。改之前把官方 YAML 备份一份别直接改原文件。5. Webhook 与 IngressClassIngress 资源的第一道把关很多人在读官方 YAML 时会忽略后段的 Webhook 部分觉得那只是控制器附带的“校验功能”。实际上少了这部分虽然控制器还能跑但创建不合规的 Ingress 时不会得到任何反馈配置错误会一直到流量异常才暴露出来。5.1 Admission Webhook 为什么必须走一个独立 ServiceValidatingWebhookConfiguration 的作用是让 API Server 在 Ingress 资源创建或更新时先发给控制器校验一遍。比如检查 host 字段是否合法、annotation 是否能被识别、backend service 是否存在。所以 API Server 需要有一个稳定的访问入口来调控制器的 8443 端口这个入口就是ingress-nginx-controller-admission这个 ClusterIP Service。这个 Service 的端口映射是 443 - 8443targetPort 是名为webhook的容器端口。如果你把这个 Service 误当成了对外入口加了个 LoadBalancer 类型会白白多创建一个公网服务而且大部分云平台会对 443 端口做安全策略容易引发误解。记住对外流量走ingress-nginx-controllerWebhook 校验走ingress-nginx-controller-admission两者职责完全不同。5.2 证书由谁生成又由谁注入Webhook 要正常运转API Server 和控制器之间必须建立 TLS 连接。官方 YAML 用两个 Job 解决了证书问题第一个 Job 生成证书并把它写进 Secret第二个 Job 把证书的 CA Bundle 注入 ValidatingWebhookConfiguration。所以在 apply 完官方 YAML 后如果出现 Webhook 调用失败大概率是 Job 还没跑完或者 Secret 被误删了。重跑证书生成任务的办法也很简单把相关 Job 删除再 apply 一次Job 会重新创建并再次注入证书。当然前提是 RBAC 给了对应权限。这里要提醒一句不要手动去改 Webhook 里的 caBundle系统会自动维护它你手动改反而可能把连接搞挂。5.3 IngressClass 和 Deployment 参数必须“对上暗号”官方 YAML 里还有一个很容易被忽略的 IngressClass 对象。它定义了spec.controller: k8s.io/ingress-nginx而 Deployment 启动参数里--controller-classk8s.io/ingress-nginx与之呼应。当你创建 Ingress 时如果不写ingressClassName控制器会优先看 IngressClass 上有没有ingressclass.kubernetes.io/is-default-class: true这个注解如果连默认 IngressClass 都没有控制器就不会认领这个 Ingress。所以 404 排障时我一定是先看kubectl get ingress -o yaml里的ingressClassName字段再看 IngressClass 对象是否存在且无异常最后才去看控制器日志。跳过前两步直接查日志往往会绕远路。6. 真实环境里最容易翻车的四个点排查路径记录最后分享四个我实际部署和运维中反复遇到、也帮别人排查过的问题。这些问题都不是 YAML 语法错误而是资源间契约关系没有对齐导致的恰好能验证前面对 YAML 的拆解。6.1 Ingress 建好后一直 404先查什么这是高频问题Ingress 创建成功也没有报错但访问域名就是 404。我的排查路径是先看 Ingress 有没有被控制器认领认领的标志是kubectl get ingress里有 ADDRESS。如果没有 ADDRESS再看spec.ingressClassName是否存在以及集群里有没有对应的 IngressClass。如果这两个都正常再看控制器 Pod 日志通常会出现ignoring ingress ... because no matching class这句话就是在告诉你 IngressClass 没匹配上。还有一种情况是默认注解没打对is-default-class必须严格写成true很多复制粘贴时把引号丢了的资源也能创建但就是不会被认领。6.2 创建 Ingress 报 Webhook 超时怎么定位报错信息一般长这样Internal error occurred: failed calling webhook validate.nginx.ingress.kubernetes.io: failed to call webhook: Post https://...:443/...: context deadline exceeded。看到这个先别慌按顺序查三样东西第一Webhook 指向的 Service 是否存在且 selector 能否匹配到控制器 Pod第二控制器 Pod 的 8443 端口是否在监听第三Job 是否生成了证书。常见原因是控制器 Pod 没起来导致 Service 没有 EndpointWebhook 自然无响应。如果你修改过 Deployment 的 labels记得同步检查 Webhook 和 Service 里的 selector这个坑很隐蔽YAML 里 labels 不一致时 apply 不会报任何错。6.3 多副本日志频繁切换 Leader是不是没配好控制器日志里反复出现 leader 切换字样有时候会让人觉得集群环境有问题。实际上只要 Lease 对象存在多个副本之间正常竞选就是这样的。真正需要排查的是当你发现某段时间 Nginx 配置一直不更新但副本都没挂时看一下当前谁是 leader日志里有没有报 RBAC 权限不足的错误。只要 leader 还活着配置更新就由它负责其他副本即使日志里出现 refreshing 字样也不会真正写配置。6.4 改镜像源或资源配额时动错位置会怎样自建环境经常需要把镜像换成内网仓库地址。有些人习惯把 YAML 里所有image:字段全改一遍结果把两个 Job 的 certgen 镜像也改了然后发现 Webhook 证书生成失败。正确的做法是只改 Deployment 里 controller 容器的镜像Job 的镜像一般不需要动。另外给控制器加资源配额时CPU 和内存限制不要给得太低尤其是内存Nginx 配置量大了之后控制器需要持有全量配置和 WAF 规则内存不足会直接 OOM。建议至少预留 512Mi 起步视 Ingress 数量上调。最后说一点我自己的习惯拿到官方 YAML 后我永远保留一份原始文件然后按资源类型拆成多个文件放到目录里统一维护。这样每次变更前用kubectl diff -f 目录/对比一下改动内容一目了然回滚也方便。YAML 本身不难难的是把资源和资源之间的隐性契约看清希望这篇拆解能帮你少走一些弯路。