1. 单 master 节点里Service NodePort 为什么不够用在单 master 节点的 k8s 集群里把应用跑起来只是第一步真正让人头疼的是「怎么让集群外面的请求进来」。你大概率经历过这样的过程Pod 起来了Service 建好了用kubectl get svc看到类型是 ClusterIP然后发现从宿主机根本访问不到于是改成 NodePort终于能用节点IP:30080打开页面了。NodePort 确实能解决问题但它是四层代理。所谓四层指的是它只看 TCP/UDP 协议和端口不关心你请求的是哪个域名、哪个 URL 路径。Service 的职责是「用标签选中一组 Pod并给它们一个稳定的虚拟 IP 做负载均衡」它解决的是 Pod IP 动态变化的问题而不是「同一个 80 端口怎么按域名分发到不同服务」的问题。当你的服务只有一两个时NodePort 完全够用。但服务一旦多起来问题就暴露了每个服务都要占一个 30000-32767 之间的端口端口号既难记又难维护防火墙要开一堆规则而且你没法用api.example.com和web.example.com这种域名去区分它们——因为四层根本不认识域名。这时候自然的想法是在集群里放一个 Nginx所有流量先到 Nginx再由 Nginx 按域名和路径反向代理到各个 Service。这个思路是对的Nginx 是七层代理能看懂 HTTP 请求里的 Host 头和 URL 路径。但如果你手动维护这个 Nginx 的nginx.conf每次新增服务都要登进去改配置、reload这跟 k8s 的自动化管理理念完全冲突。Ingress 就是把这个「改 Nginx 配置」的动作抽象成了 k8s 的 API 资源。你用 YAML 声明「host 是 foo.com、path 是 /bar 的请求转发到 bar-service」Ingress Controller 会持续监听这些资源的变化自动生成 Nginx 配置并 reload。这一篇我们就聚焦单 master 节点环境用 nginx ingress controller 跑通第一条反向代理链路从 Service NodePort 平滑演进到 Ingress。本篇适合已经有一个能用的单节点 k8s 集群、跑过 Deployment 和 Service、想搞明白 Ingress 到底怎么落地的人。核心检索词就是 k8s ingress 反向代理我会把 controller 部署清单、Ingress 资源 YAML、curl 验证动作都给全你照着做就能看到域名路由生效。2. 部署 nginx ingress controller 的前置准备在单 master 节点上部署 nginx ingress controller有几个前置条件必须先确认否则后面 Pod 起不来或者流量进不来排查起来会很绕。第一集群的 CNI 网络插件要正常工作。单节点环境常用 flannel 或 calico你可以用kubectl get pods -n kube-system确认网络插件 Pod 是 Running 状态。如果 CNI 有问题ingress controller 的 Pod 即使起来了也没法把流量转发到后端 Service 的 Pod IP。第二节点上 80 和 443 端口不能被占用。nginx ingress controller 默认用 hostNetwork 模式直接占用宿主机的 80/443。你可以先执行ss -lntp | grep -E :80|:443检查一下如果有别的进程占着要么停掉它要么改 controller 的监听端口。第三需要给 controller 创建 RBAC 权限。controller 要监听 Ingress、Service、Endpoints、Pod 等资源的变化没有对应的 ServiceAccount、ClusterRole、ClusterRoleBinding它会一直报权限错误。这部分清单比较长我把它和 Deployment 放在一起。第四需要一个 default backend。当请求的域名或路径没有匹配任何 Ingress 规则时controller 会把请求转发到 default backend它通常返回 404 页面同时提供/healthz健康检查接口。controller 通过启动参数--default-backend-service指定它。下面是我在单 master 节点上实际用的部署清单。先创建命名空间和 RBACapiVersion: v1 kind: Namespace metadata: name: ingress-nginx --- apiVersion: v1 kind: ServiceAccount metadata: name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nginx-ingress-clusterrole rules: - apiGroups: [] resources: [configmaps, endpoints, nodes, pods, secrets, namespaces] verbs: [list, watch] - apiGroups: [] resources: [nodes] verbs: [get] - apiGroups: [] resources: [services] verbs: [get, list, watch] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses] verbs: [get, list, watch] - apiGroups: [] resources: [events] verbs: [create, patch] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses/status] verbs: [update] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nginx-ingress-clusterrole-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nginx-ingress-clusterrole subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx这里有个细节要注意ClusterRole里我同时写了extensions和networking.k8s.io两个 apiGroups因为不同 k8s 版本的 Ingress 资源所属的 API 组不一样老版本在extensions下新版本在networking.k8s.io下两个都写上兼容性更好。接着是 ConfigMapcontroller 会读取它来生成 Nginx 配置apiVersion: v1 kind: ConfigMap metadata: name: nginx-configuration namespace: ingress-nginx data: proxy-read-timeout: 120 proxy-send-timeout: 120 client-max-body-size: 50m use-proxy-protocol: falseproxy-read-timeout和proxy-send-timeout设成 120 秒是为了避免大文件上传或慢接口被 Nginx 提前断开client-max-body-size设成 50m允许客户端上传最大 50MB 的请求体。这些值你可以按实际业务调整。然后是 default backend 的 Deployment 和 ServiceapiVersion: apps/v1 kind: Deployment metadata: name: default-http-backend namespace: ingress-nginx labels: k8s-app: default-http-backend spec: replicas: 1 selector: matchLabels: k8s-app: default-http-backend template: metadata: labels: k8s-app: default-http-backend spec: terminationGracePeriodSeconds: 60 containers: - name: default-http-backend image: registry.cn-hangzhou.aliyuncs.com/hachikou/defaultbackend:1.0 livenessProbe: httpGet: path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: default-http-backend namespace: ingress-nginx labels: k8s-app: default-http-backend spec: ports: - port: 80 targetPort: 8080 selector: k8s-app: default-http-backenddefault backend 的镜像只要满足两个条件就行访问/返回 404访问/healthz返回 200。controller 启动时会用--default-backend-serviceingress-nginx/default-http-backend指向这个 Service。最后是 controller 本身的 Deployment这是核心apiVersion: apps/v1 kind: Deployment metadata: name: nginx-ingress-controller namespace: ingress-nginx labels: k8s-app: nginx-ingress-controller spec: replicas: 1 selector: matchLabels: k8s-app: nginx-ingress-controller template: metadata: labels: k8s-app: nginx-ingress-controller spec: hostNetwork: true serviceAccountName: nginx-ingress-serviceaccount terminationGracePeriodSeconds: 60 containers: - name: nginx-ingress-controller image: registry.cn-hangzhou.aliyuncs.com/peter1009/nginx-ingress-controller:0.20.0 readinessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 1 ports: - containerPort: 80 hostPort: 80 - containerPort: 443 hostPort: 443 env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace args: - /nginx-ingress-controller - --default-backend-service$(POD_NAMESPACE)/default-http-backend - --configmap$(POD_NAMESPACE)/nginx-configuration这里hostNetwork: true让 controller 的 Pod 直接使用宿主机网络所以它监听的 80/443 就是宿主机的 80/443。单 master 节点只有一个节点这样最省事外部请求直接打到节点 IP 的 80 端口就能进 controller。POD_NAME和POD_NAMESPACE两个环境变量通过 fieldRef 从 Pod 元数据里取controller 内部会用到。把上面这些清单保存成文件依次kubectl apply -f应用。应用完之后用kubectl get pods -n ingress-nginx看状态controller 和 default backend 都应该是 Running。如果 controller 一直 CrashLoopBackOff多半是 RBAC 权限没配对用kubectl logs -n ingress-nginx controller-pod看具体报错。3. 可复制的 Ingress 资源 YAML 与路由规则controller 跑起来之后它本身还不知道要把流量转发到哪里这需要靠 Ingress 资源来声明。Ingress 是一组规则描述「什么域名、什么路径的请求转发到哪个 Service 的哪个端口」。controller 监听 Ingress 资源的变化把它翻译成 Nginx 的 server 块和 location 块。先准备两个后端服务来演示。我用最经典的 nginx 和 httpd 两个 Deployment加上各自的 ServiceapiVersion: apps/v1 kind: Deployment metadata: name: web-nginx spec: replicas: 2 selector: matchLabels: app: web-nginx template: metadata: labels: app: web-nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-nginx-svc spec: selector: app: web-nginx ports: - port: 80 targetPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: web-httpd spec: replicas: 2 selector: matchLabels: app: web-httpd template: metadata: labels: app: web-httpd spec: containers: - name: httpd image: httpd:2.4 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-httpd-svc spec: selector: app: web-httpd ports: - port: 80 targetPort: 80这两个 Service 都是 ClusterIP 类型集群内部能访问外部访问不到。现在写 Ingress 把外部流量按域名分发到它们apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress namespace: default annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: nginx.demo.local http: paths: - path: / pathType: Prefix backend: service: name: web-nginx-svc port: number: 80 - host: httpd.demo.local http: paths: - path: / pathType: Prefix backend: service: name: web-httpd-svc port: number: 80这份 Ingress 定义了两条规则访问nginx.demo.local的请求转发到web-nginx-svc的 80 端口访问httpd.demo.local的请求转发到web-httpd-svc的 80 端口。pathType: Prefix表示路径前缀匹配/能匹配所有路径。这里有几个关键字段要理解清楚。ingressClassName: nginx指定由哪个 Ingress Controller 处理这条规则。如果你集群里同时装了 nginx 和 traefik 两个 controller这个字段就很重要它决定规则归谁管。老版本 k8s 用的是kubernetes.io/ingress.class注解新版本推荐用ingressClassName字段。annotations里的rewrite-target: /是 nginx ingress controller 特有的注解作用是重写转发给后端的路径。比如你请求/api/foo如果配了 rewrite-target 为/转发到后端时就变成/foo。这个注解在路径前缀和后端实际路径不一致时特别有用。如果你想让同一个域名下按路径分发到不同服务可以这样写apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: path-ingress namespace: default spec: ingressClassName: nginx rules: - host: app.demo.local http: paths: - path: /nginx pathType: Prefix backend: service: name: web-nginx-svc port: number: 80 - path: /httpd pathType: Prefix backend: service: name: web-httpd-svc port: number: 80这样app.demo.local/nginx走 nginx 服务app.demo.local/httpd走 httpd 服务。注意这里没加 rewrite-target所以转发到后端时路径会带上/nginx或/httpd前缀后端服务得能处理这个前缀否则会 404。如果后端只认根路径就加上nginx.ingress.kubernetes.io/rewrite-target: /$2配合正则路径使用。应用完 Ingress 之后用kubectl get ingress能看到它kubectl describe ingress demo-ingress能看到规则详情和 controller 分配的地址。如果 ADDRESS 一栏是空的说明 controller 还没处理到这条规则检查一下ingressClassName是否和 controller 匹配。4. curl 验证域名路由是否生效Ingress 资源创建好之后最直接的验证方式就是用 curl 带上 Host 头去请求节点 IP。因为我们是单 master 节点controller 用 hostNetwork 占了宿主机 80 端口所以直接请求节点 IP 的 80 端口就行。先拿到节点 IPkubectl get nodes -o wide假设节点 IP 是192.168.1.100。因为nginx.demo.local这个域名没有真实 DNS 解析我们用 curl 的--resolve参数手动把域名指向节点 IPcurl -I --resolve nginx.demo.local:80:192.168.1.100 http://nginx.demo.local/如果路由生效你会看到类似这样的响应头HTTP/1.1 200 OK Server: nginx/1.25.x Date: ... Content-Type: text/html Content-Length: 615Server: nginx/1.25.x说明请求被转发到了后端的 nginx Pod。再验证 httpd 那条规则curl -I --resolve httpd.demo.local:80:192.168.1.100 http://httpd.demo.local/这次响应头里应该是Server: Apache/2.4.x说明请求被转发到了 httpd Pod。同一个节点 IP、同一个 80 端口靠 Host 头区分出了两个不同的后端服务这就是七层反向代理的效果。如果你想看完整的响应体而不是只看头去掉-I换成-scurl -s --resolve nginx.demo.local:80:192.168.1.100 http://nginx.demo.local/ | head -20会看到 nginx 的欢迎页面 HTML。再请求一个不存在的域名验证 default backend 是否生效curl -I --resolve notexist.demo.local:80:192.168.1.100 http://notexist.demo.local/应该返回HTTP/1.1 404 Not Found这个 404 就是 default backend 返回的。如果返回的是 nginx 的 404 页面而不是 default backend 的说明 default backend 没配好。验证路径分发的那条 Ingresscurl -I --resolve app.demo.local:80:192.168.1.100 http://app.demo.local/nginx curl -I --resolve app.demo.local:80:192.168.1.100 http://app.demo.local/httpd第一条应该返回 nginx 的响应第二条返回 httpd 的响应。如果第二条返回 404很可能是路径前缀没被后端正确处理需要加 rewrite-target 注解。还有一个排查利器是直接看 controller 生成的 Nginx 配置。进入 controller Podkubectl exec -it -n ingress-nginx controller-pod -- cat /etc/nginx/nginx.conf你会看到 controller 把 Ingress 规则翻译成的 server 块每个 host 对应一个 server里面的 location 对应路径规则proxy_pass 指向对应的 Service。看懂这份配置你对 Ingress 到 Nginx 的映射关系就彻底清楚了。如果你在浏览器里测试需要改本机 hosts 文件把nginx.demo.local和httpd.demo.local指向节点 IP。Windows 在C:\Windows\System32\drivers\etc\hostsLinux/Mac 在/etc/hosts加两行192.168.1.100 nginx.demo.local 192.168.1.100 httpd.demo.local然后浏览器访问http://nginx.demo.local就能看到 nginx 页面。不过改 hosts 只适合本地测试生产环境还是要有真实的 DNS 解析。5. 常见报错排查401、local proxy failed、reading choicesIngress 链路涉及 controller、Service、Pod 好几层出问题时现象往往很模糊。下面是我在单 master 节点上踩过的几个典型报错对照着排查能省不少时间。报错一controller 日志里出现local proxy failed或connect() failed (111: Connection refused)这个报错的意思是 controller 把请求转发给后端 Service 时连不上。原因通常是 Service 的 selector 没匹配到任何 Pod或者 Pod 没就绪。先用kubectl get endpoints web-nginx-svc看 Endpoints 列表如果显示none说明 Service 没选中 Pod。检查 Deployment 的 Pod 标签和 Service 的 selector 是否一致kubectl get pods --show-labels能看到实际标签。还有一种可能是 Pod 的 readinessProbe 没通过Pod 处于 NotReady 状态Endpoints 里就不会有它的 IP。用kubectl describe pod pod-name看探针的失败原因。报错二curl 返回 401 Unauthorized如果你给 Ingress 配了 basic auth 注解但没提供正确的用户名密码就会返回 401。检查 Ingress 的 annotations 里有没有nginx.ingress.kubernetes.io/auth-type: basic和auth-secret。如果确实配了认证curl 要带上-u user:password。如果没打算配认证却出现 401可能是 Secret 里的 htpasswd 内容有问题重新生成一次。报错三controller 日志里出现reading choices或unexpected end of JSON input这类报错通常和 Ingress 资源的 API 版本有关。老版本 controller比如 0.20.0对networking.k8s.io/v1的支持可能不完整如果你用的是新版本 k8s 但 controller 镜像太老解析 Ingress 时就会出错。解决办法是升级 controller 镜像到和 k8s 版本匹配的版本或者把 Ingress 的 apiVersion 改成 controller 支持的版本。用kubectl api-versions | grep ingress看集群支持哪些 Ingress API 版本。报错四OAuth相关的认证失败如果你在 Ingress 上配了nginx.ingress.kubernetes.io/auth-url做外部认证认证服务返回非 2xx 就会导致请求被拒。先单独 curl 一下认证服务的地址确认它能正常返回 200。认证服务的地址如果是集群内的 Service要确保 controller 能访问到它。报错五ingressClassName不生效Ingress 创建了但 ADDRESS 为空用kubectl get ingressclass看集群里有哪些 IngressClass。如果列表是空的说明 controller 没有创建对应的 IngressClass 资源。老版本 controller 可能不自动创建需要手动建一个apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: nginx spec: controller: k8s.io/ingress-nginx创建后 Ingress 的ingressClassName: nginx才能匹配上。报错六请求超时504 Gateway Timeout后端服务处理时间超过了 Nginx 的proxy-read-timeout。默认是 60 秒如果你在 ConfigMap 里设了 120 秒还是超时检查后端服务本身是不是卡住了。用kubectl logs backend-pod看后端有没有报错。另外proxy-connect-timeout控制的是连接后端的超时和proxy-read-timeout不是一回事必要时两个都调大。排查 Ingress 问题的通用思路是先看 controller 日志kubectl logs -n ingress-nginx controller-pod再看后端 Pod 日志最后看 controller 生成的 nginx.conf。日志里通常会直接告诉你哪一步失败了比盲目猜要快得多。6. 从 NodePort 到 Ingress 的演进收尾走到这里你应该已经在单 master 节点上跑通了第一条 Ingress 反向代理链路外部请求打到节点 80 端口controller 根据 Host 头把流量分发到不同的 ServiceService 再负载均衡到后端 Pod。整个过程不需要手动改 Nginx 配置新增服务只要写一份 Ingress YAML 应用一下就行。回头看这条演进路径其实每一步都在解决前一步的痛点。Pod IP 会变所以有了 ServiceService 只做四层认不出域名和路径所以有了 NodePort 暴露端口NodePort 端口难维护所以有了 Nginx 反向代理手动改 Nginx 配置不自动化所以有了 Ingress 资源Ingress 只是规则需要有人把它翻译成 Nginx 配置所以有了 Ingress Controller。理解了这个链条你就明白每个组件为什么存在而不是死记 YAML 字段。单 master 节点的环境有个特点controller 用 hostNetwork 直接占宿主机 80/443省去了 LoadBalancer 类型的 Service。如果是多节点集群通常会用 NodePort 或 LoadBalancer 把 controller 暴露出去再在前面挂一个外部负载均衡。但核心的 Ingress 规则写法是一样的换的只是 controller 的暴露方式。下一步你可以尝试的方向给 Ingress 配 TLS 证书实现 HTTPS用nginx.ingress.kubernetes.io/ssl-redirect强制跳转用canary注解做灰度发布把一部分流量导到新版本或者试试 traefik ingress controller对比一下它和 nginx 在配置方式上的差异。这些都是在跑通基础链路之后自然要碰的东西。如果你在验证过程中遇到 controller 日志刷屏、Ingress 规则不生效、或者 curl 一直连不上后端的情况可以对照第 5 节的报错清单逐条排查。多数问题集中在 Service selector 不匹配、IngressClass 没对上、以及 controller 镜像版本和 k8s 版本不兼容这三类。把这三类排除掉剩下的基本就是配置细节问题了。