课程目录(第 8 章 / 共 33 章)
课程/让应用可访问、可配置

Ingress:从外部访问集群服务

8 章 / 共 33·18 分钟·入门Ingressingress-nginx路由

装好 Ingress Controller,用一份 Ingress 清单按域名和路径把外部流量路由到 web 与 api 两个 Service。

学完这一章,你将能够

  • 说清 Ingress 与 Service 的分工,以及为什么必须先装 Controller
  • 写出 networking.k8s.io/v1 的 Ingress 清单,按路径把流量分给两个 Service
  • 用 /etc/hosts 或 Host 头在本机验证路由是否生效

Service 解决了什么,还差什么

上一章我们用 Service 给 web 找到了稳定入口,但那个入口只在集群内部可用:ClusterIP 外部碰不到;NodePort 要靠「节点 IP:端口」访问,用户记不住;而你真正想要的是「访问 kube101.local 打开前端,访问 kube101.local/api 打到后端」。

四层(TCP/IP)的 Service 只能回答「转发到哪组 Pod」,回答不了「同一个端口上,按 HTTP 路径分给不同的服务」。这就是 Ingress 要补上的一层。

Ingress 与 Ingress Controller 的分工

两个概念必须分开:

概念是什么谁负责
Ingress一份路由规则清单(域名、路径、后端 Service)你写,kubectl apply
Ingress Controller真正读规则、跑反向代理、转发流量的组件你装,之后它自己干活

只写 Ingress 不装 Controller,什么都不会发生。 因为 Ingress 只是一个声明,Kubernetes 内置的控制器不负责实现它——这一点和 Deployment 很不一样,Deployment 有内置控制器管,Ingress 没有。

分工上可以这样记:

text
用户 → Ingress Controller(七层反向代理,按 host/path 路由)
          │ 读取

       Ingress(规则:kube101.local/api → Service api)
          │ 转发到

       Service(四层,选一组就绪的 Pod)→ Pod

一句话:Ingress 管「怎么分」,Service 管「给谁」,Controller 管「实际转发」。

先装一个 Ingress Controller

Controller 有很多实现(ingress-nginx、Traefik、HAProxy、各类云厂商实现),本课程用最通用的 ingress-nginx

bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

等它就绪(这条命令会阻塞到 Pod Ready 或超时):

bash
kubectl wait --namespace ingress-nginx --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller --timeout=180s
kubectl get pods -n ingress-nginx

看到 ingress-nginx-controller-xxxx 1/1 Running 就算装好了。

kind 必须先做端口映射

上面这份 kind 专用清单会让 Controller Pod 用 hostPort 直接占用节点的 80 和 443。而 kind 的「节点」其实是 Docker 容器,所以宿主机默认访问不到。要么用第 3 章 搭建本地集群里的多节点配置,要么建集群时带上端口映射:

yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 80
        hostPort: 80
      - containerPort: 443
        hostPort: 443

集群已经建好也没关系:用 kubectl port-forward -n ingress-nginx svc/ingress-nginx-controller 8080:80 也能验证,只是后面 curl 要改成 localhost:8080

准备 web 与 api 两个 Service

Ingress 的后端必须是 Service。web 沿用第 7 章 Service里的(nginx,80 端口),再补一个后端 api

yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: api, namespace: demo }
spec:
  replicas: 1
  selector: { matchLabels: { app: api } }
  template:
    metadata: { labels: { app: api } }
    spec:
      containers:
        - name: api
          image: hashicorp/http-echo:1.0
          args: ["-listen=:5678", "-text=hello from api"]
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata: { name: api, namespace: demo }
spec:
  type: ClusterIP
  selector: { app: api }
  ports:
    - { name: http, port: 5678, targetPort: 5678 }

保存为 api.yaml 并应用:

bash
kubectl apply -f api.yaml
kubectl get deploy,svc -n demo
kubectl get endpointslices -n demo

确认 webapi 两个 Service 的 ENDPOINTS 都非空。这一步很关键:Ingress 出 502,绝大多数时候是后端 Service 没有就绪的 Pod。

写第一份 Ingress 清单:ingressClassName 与 pathType

保存为 web-ingress.yaml

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kube101
  namespace: demo
spec:
  ingressClassName: nginx
  rules:
    - host: kube101.local
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 5678
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

几个字段的要点:

  • ingressClassName: nginx:指定「由哪个 Controller 处理这条规则」。ingress-nginx 安装后注册的类名就是 nginx。不写或写错,Controller 会直接忽略这条 Ingress。
  • pathType: Prefix:按路径前缀匹配。/api 会匹配 /api/api/users/ 匹配其它所有请求。另一种取值是 Exact(完全相等),以及 ImplementationSpecific(交给 Controller 自己解释,不建议)。
  • 路径顺序不决定优先级Prefix 类型下,Kubernetes 规定更长的路径优先,所以 /api 一定先于 / 被匹配。
  • backend.service.portnumber 指定 Service 的 port(不是 targetPort)。

如果你的后端不认识 /api 这个前缀,可以加注解 nginx.ingress.kubernetes.io/rewrite-target: / 把它去掉。这类 nginx.ingress.kubernetes.io/* 注解是 ingress-nginx 特有的,换 Controller 就不通用。

应用并查看:

bash
kubectl apply -f web-ingress.yaml
kubectl get ingress -n demo
kubectl describe ingress kube101 -n demo

kubectl get ingressCLASS 列应是 nginxADDRESS 列由 Controller 回填——kind 专用清单里配置了 --publish-status-address=localhost,所以这里显示 localhost;如果这一列一直为空,通常说明 Controller 没装好或没在运行。

验证:/etc/hosts 与 curl

Ingress 靠 Host 头分流,所以本机要能把这个域名解析到 Controller 监听的地址。最直接的办法是改 hosts:

bash
echo "127.0.0.1 kube101.local" | sudo tee -a /etc/hosts   # 不想改 hosts 就跳过,改用 -H
curl http://kube101.local/
curl http://kube101.local/api
curl -H "Host: kube101.local" http://localhost/api        # 带 Host 头的等价写法

前两条应分别返回 nginx 的欢迎页 HTML 与 hello from api。如果 Controller 是通过 port-forward 8080:80 访问的,把地址换成 http://localhost:8080 即可。

ingress-nginx 生产配置:真实 IP、超时与限流

跑通只是第一步。真实环境一定会遇到三件事:后端拿到的客户端 IP 全是网关自己、上传大文件报 413、慢接口被默认超时掐断。这些都用 ConfigMap 和注解解决,不用改应用

真实客户端 IP:默认后端看到的是 Controller 的地址,真实 IP 藏在 X-Forwarded-For 里。Controller 前面还有一层四层负载均衡时,必须显式声明信任哪些网段,否则这个头任何人都能伪造:

yaml
apiVersion: v1
kind: ConfigMap
metadata: { name: ingress-nginx-controller, namespace: ingress-nginx }
data:
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"
  proxy-real-ip-cidr: "10.0.0.0/8,172.16.0.0/12"

为什么必须配:风控、审计、限流全都按客户端 IP 做,配错等于所有用户共用一个 IP;proxy-real-ip-cidr 不写,X-Forwarded-For 就能被客户端随意伪造。

超时、请求体与缓冲决定「大文件能不能传、慢接口会不会被掐、响应头大了会不会 502」,写在 Ingress 注解里:

注解默认什么时候要调
proxy-body-size1m上传或回调报文大,不改就报 413
proxy-read-timeout / proxy-send-timeout60s长连接、大文件下载、慢查询,太小会 504
proxy-connect-timeout5s后端冷启动慢,握手就超时
proxy-buffer-size4k后端响应头大时 upstream sent too big header,调到 16k
yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
    nginx.ingress.kubernetes.io/limit-rps: "20"          # 超出直接返回 429
    nginx.ingress.kubernetes.io/limit-connections: "50"

限流按客户端 IP 计数,所以必须先配好真实 IP,否则等于给网关自己限流;它只挡请求频率,不是 WAF。

生产环境的三条经验

  • 注解是 Controller 私有的nginx.ingress.kubernetes.io/* 换 Controller 就不生效,别当成 Kubernetes 标准。
  • 改注解会触发 reload:频繁改会让 nginx 反复重载,用 GitOps 批量改。
  • TLS 交给 cert-managerspec.tls 里只写 secretName,签发与续期见 安全加固

多域名、证书与灰度

一个 Ingress 可以写多个 host,每个 host 配自己的 tls;证书交给 cert-manager 自动签发,只要在注解里指向签发者,并让 spec.tls[].secretName 与证书名一致:

yaml
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - { hosts: ["app.example.com"], secretName: app-tls }
  rules:
    - host: app.example.com          # 再加一条 host: api.example.com 就多一个域名
      http:
        paths:
          - { path: /, pathType: Prefix, backend: { service: { name: web, port: { number: 80 } } } }

内网域名没有公网 DNS 时,用泛解析把 *.dev.example.com 指到 Controller 的 EXTERNAL-IP,省掉逐条加记录;这个 IP 用 controller.service.loadBalancerIP 固定住(裸金属上由 MetalLB 从池里分配,见 集群网络),否则重建 Controller 后 DNS 要跟着改。

灰度用一组 canary-* 注解做小流量切流,不必引入服务网格:

yaml
metadata:
  name: kube101-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"

canary: "true" 的 Ingress 必须与主 Ingress 同 host、同路径canary-weight 是百分比;canary-by-headercanary-by-cookie 的优先级高于权重。

参考:reference/k8s-in-action/network/ingress-nginx/SKILL.md

Gateway API:Ingress 的下一代标准

Ingress 的能力边界由注解决定,而注解是各家 Controller 私有的,换实现就要重写。Gateway APIgateway.networking.k8s.io)把「谁管网关」和「谁管路由」拆成不同对象,这也是它相比 Ingress 最大的结构差异:

text
GatewayClass(平台团队:用哪个实现,集群级)
      │ 被引用

Gateway(平台团队:监听端口、TLS 证书、允许哪些命名空间接入)
      │ parentRefs

HTTPRoute(业务团队:自己的 host、路径、权重)→ Service → Pod
维度IngressGateway API
路由规则注解 + rulesHTTPRoute 的原生字段
跨命名空间不支持,路由与被代理服务必须同命名空间支持,Gateway 可共享给多个命名空间
流量分割靠 Controller 私有注解原生 weight

kgateway 是支持这套标准的网关实现之一(还提供 Inference Extension,面向 AI 推理负载)。先由平台团队建 Gateway,业务团队在自己的命名空间写 HTTPRoute

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: { name: main-gateway, namespace: kgateway-system }
spec:
  gatewayClassName: kgateway
  listeners:
    - { name: http, protocol: HTTP, port: 80 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: web, namespace: demo }
spec:
  parentRefs:
    - { name: main-gateway, namespace: kgateway-system }
  hostnames: ["kube101.local"]
  rules:
    - backendRefs:
        - { name: web, port: 80, weight: 90 }
        - { name: web-v2, port: 80, weight: 10 }
bash
kubectl get gateway -n kgateway-system
kubectl -n demo describe httproute web     # 看 Conditions 是否为 Accepted=True

Accepted=TrueResolvedRefs=True 才算真的挂上;报 NotAllowedByListeners 通常是 Gateway 没放开对应命名空间,BackendNotFoundbackendRefs 里的 Service 名或端口写错。与 Ingress 的关系:不是立刻替换,多数实现(ingress-nginx、Istio、kgateway)同时支持两者;新项目优先 Gateway API,存量 Ingress 慢慢迁,网关实现不用换。

参考:reference/k8s-in-action/network/kgateway/SKILL.mdreference/k8s-in-action/network/istio/gateway-api/README.md

常见坑与排错

404、502、503 的区别

这几个状态码都意味着「请求没被正确送到后端」,但原因完全不同,先把它们分开:

状态码谁返回的含义最常见原因
404Controller 的默认后端没有任何规则匹配这个请求Hostpath 对不上;ingressClassName 写错导致规则没被加载
404后端 Pod规则匹配上了,但应用没有这个路径path 带了 /api 前缀而后端只认 /,需要 rewrite-target
502Controller连不上后端,或后端给了无效响应targetPort 写错、后端进程崩了、Pod 正在重启
503Controller后端列表为空,或全部不健康EndpointSlice 为空、Pod 未通过就绪探针
504Controller连上了后端,但后端响应超时后端处理太慢,或 proxy-read-timeout 之类参数太小

区分办法很直接:404 先看规则,502/503/504 先看 EndpointSlicekubectl describe ingress kube101 -n demo 看规则有没有生效,kubectl get endpointslices -n demo 看后端有没有就绪的 IP。502 与 503 的具体分配取决于 Controller 实现(ingress-nginx 在「没有可用上游」时通常返回 503,在「连不上上游」时返回 502),但排查方向一致。

三个高频现象

  • `ADDRESS` 一直为空:Ingress 只是规则,没 Controller 就没人实现它。kubectl get ingressclass 里没有 nginx,说明 Controller 没装成功,或 ingressClassName 写成了不存在的类名。
  • `default backend - 404`:请求的 Host 没匹配任何规则。改 /etc/hosts,或全程用 curl -H "Host: kube101.local" http://localhost/
  • 502 Bad Gateway:Controller 连不上后端。按 kubectl get endpointslices -n demokubectl get pods -n demokubectl describe ingress kube101 -n demokubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50 的顺序查,最常见是 EndpointSlice 为空或 port.number 与 Service 的 port 不一致。
现象原因怎么确认怎么办
ADDRESS 一直为空Controller 没装好或没在运行kubectl get pods -n ingress-nginxkubectl get ingressclass重装 Controller,确认有 nginx
返回 default backend - 404请求的 Host 没有匹配任何规则kubectl describe ingress kube101 -n demoRules/etc/hosts 或改用 curl -H "Host: ..."
Could not resolve host域名没有解析cat /etc/hostsnslookup kube101.local加 hosts 条目,或全程用 -H 带 Host 头
502 Bad Gateway后端连不上kubectl get endpointslices -n demo 是否为空修 selector / targetPort / 后端进程
503 Service Temporarily Unavailable没有就绪的后端同上,看 EndpointSlice 与 Pod 的 READY等 Pod Ready,或修就绪探针
504 Gateway Timeout后端响应太慢Controller 日志里的 upstream timed out优化后端,或调大超时注解
/api 返回了 nginx 首页请求落到了 / 那条规则kubectl describe ingressRulespath核对 pathpathType,注意更长前缀优先
apply 报 pathType: Required value漏写 pathType看报错点名的字段pathType: Prefix
改了 Ingress 但行为没变改错了对象或命名空间kubectl get ingress -A 找同名规则确认在 demo 里、ingressClassName 正确

动手练习:用域名分流两个服务

  1. 装好 ingress-nginx,确认 kubectl get ingressclass 里有 nginx
  2. 创建 webapi 两个 Deployment 和 Service,确认两者的 EndpointSlice 都非空。
  3. 应用 web-ingress.yaml,用 curl -H "Host: kube101.local" http://localhost/http://localhost/api 分别验证。
  4. /api 那条规则的 path 改成 /api/v1 再 apply,观察 /api 请求变成由 web 处理,理解「更长前缀优先」的含义,然后改回来。
自测:为什么只写 Ingress 不装 Controller,就什么都不会发生?(点击展开答案)

因为 Ingress 只是一份声明式的数据,Kubernetes 内置的控制器并不实现它——这一点和 Deployment 完全不同,Deployment 有内置控制器盯着,Ingress 没有。真正干活的是 Controller:它 watch Ingress 对象,把规则翻译成反向代理配置(例如 nginx 的 server 块)并重载进程。没有 Controller,这些对象只是躺在 etcd 里,既没有人读它,也不会有人回填 ADDRESS

自测:`/api` 写在 `/` 后面,为什么仍然优先匹配?(点击展开答案)

因为 pathType: Prefix 的优先级由规则定义——更长的路径优先,与清单里的书写顺序无关,顺序只影响可读性。所以 /api/users 会命中 /api 而不是 /。反过来说,如果你把 /apipathType 改成 Exact/api/users 就不再匹配它,而是落到 / 那条规则上,表现会突然「变成前端页面」。

自测:Ingress 的后端为什么必须写 Service,不能直接写 Pod IP?(点击展开答案)

因为 Pod IP 会随重建而改变,副本数也会增减,把 IP 写进路由规则等于把不稳定性直接引入配置。Service 提供的是稳定入口,Controller 只记 Service 的名字和端口,后端 Pod 的增删由 EndpointSlice 承接,规则一个字都不用改。这也正好对应本章的分工:Ingress 管「怎么分」,Service 管「给谁」,Controller 管「实际转发」

小结

  • Ingress 是路由规则,Ingress Controller 是实现;只写 Ingress 不装 Controller 不会有任何效果。
  • Ingress 工作在七层,按 hostpath 分流;Service 工作在四层,负责选 Pod。
  • 后端必须是同命名空间下的 Service,backend.service.port.number 对应 Service 的 port
  • ingressClassName 决定由谁处理规则,pathType: Prefix 下更长的路径优先。
  • 验证靠 Host 头(改 hosts 或 curl -H);502 基本都指向「后端没有就绪 Pod」。

练习

  1. 再加一条 host: api.kube101.local 的规则,让它直接路由到 api Service,并用 curl -H 验证。
  2. 故意把 Ingress 里 api 的端口改成 9999,观察访问 /api 的返回,再从 Controller 日志里找出对应记录。
  3. 想一下:如果集群里有 20 个服务都要对外暴露,为什么「每个服务一个 LoadBalancer」不如「一个 Ingress 统一入口」划算?

应用已经能被外部访问了,但镜像里往往还写着数据库地址、日志级别这些跟环境相关的东西。下一章我们用 第 9 章 ConfigMap 与 Secret 把配置从镜像里彻底拿出来。

相关章节:Ingress 的后端 Service 是怎么建的,见第 7 章 Service;如果连不上,第 15 章 排障手册 给了一套固定的排查顺序。