课程目录(第 8 章 / 共 33 章)
Ingress:从外部访问集群服务
装好 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 没有。
分工上可以这样记:
用户 → 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:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml等它就绪(这条命令会阻塞到 Pod Ready 或超时):
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 章 搭建本地集群里的多节点配置,要么建集群时带上端口映射:
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:
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 并应用:
kubectl apply -f api.yaml
kubectl get deploy,svc -n demo
kubectl get endpointslices -n demo确认 web 和 api 两个 Service 的 ENDPOINTS 都非空。这一步很关键:Ingress 出 502,绝大多数时候是后端 Service 没有就绪的 Pod。
写第一份 Ingress 清单:ingressClassName 与 pathType
保存为 web-ingress.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.port用number指定 Service 的port(不是targetPort)。
如果你的后端不认识 /api 这个前缀,可以加注解 nginx.ingress.kubernetes.io/rewrite-target: / 把它去掉。这类 nginx.ingress.kubernetes.io/* 注解是 ingress-nginx 特有的,换 Controller 就不通用。
应用并查看:
kubectl apply -f web-ingress.yaml
kubectl get ingress -n demo
kubectl describe ingress kube101 -n demokubectl get ingress 的 CLASS 列应是 nginx。ADDRESS 列由 Controller 回填——kind 专用清单里配置了 --publish-status-address=localhost,所以这里显示 localhost;如果这一列一直为空,通常说明 Controller 没装好或没在运行。
验证:/etc/hosts 与 curl
Ingress 靠 Host 头分流,所以本机要能把这个域名解析到 Controller 监听的地址。最直接的办法是改 hosts:
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 前面还有一层四层负载均衡时,必须显式声明信任哪些网段,否则这个头任何人都能伪造:
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-size | 1m | 上传或回调报文大,不改就报 413 |
proxy-read-timeout / proxy-send-timeout | 60s | 长连接、大文件下载、慢查询,太小会 504 |
proxy-connect-timeout | 5s | 后端冷启动慢,握手就超时 |
proxy-buffer-size | 4k | 后端响应头大时 upstream sent too big header,调到 16k |
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。
生产环境的三条经验
多域名、证书与灰度
一个 Ingress 可以写多个 host,每个 host 配自己的 tls;证书交给 cert-manager 自动签发,只要在注解里指向签发者,并让 spec.tls[].secretName 与证书名一致:
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-* 注解做小流量切流,不必引入服务网格:
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-header、canary-by-cookie 的优先级高于权重。
参考:reference/k8s-in-action/network/ingress-nginx/SKILL.mdGateway API:Ingress 的下一代标准
Ingress 的能力边界由注解决定,而注解是各家 Controller 私有的,换实现就要重写。Gateway API(gateway.networking.k8s.io)把「谁管网关」和「谁管路由」拆成不同对象,这也是它相比 Ingress 最大的结构差异:
GatewayClass(平台团队:用哪个实现,集群级)
│ 被引用
▼
Gateway(平台团队:监听端口、TLS 证书、允许哪些命名空间接入)
│ parentRefs
▼
HTTPRoute(业务团队:自己的 host、路径、权重)→ Service → Pod| 维度 | Ingress | Gateway API |
|---|---|---|
| 路由规则 | 注解 + rules | HTTPRoute 的原生字段 |
| 跨命名空间 | 不支持,路由与被代理服务必须同命名空间 | 支持,Gateway 可共享给多个命名空间 |
| 流量分割 | 靠 Controller 私有注解 | 原生 weight |
kgateway 是支持这套标准的网关实现之一(还提供 Inference Extension,面向 AI 推理负载)。先由平台团队建 Gateway,业务团队在自己的命名空间写 HTTPRoute:
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 }kubectl get gateway -n kgateway-system
kubectl -n demo describe httproute web # 看 Conditions 是否为 Accepted=TrueAccepted=True、ResolvedRefs=True 才算真的挂上;报 NotAllowedByListeners 通常是 Gateway 没放开对应命名空间,BackendNotFound 是 backendRefs 里的 Service 名或端口写错。与 Ingress 的关系:不是立刻替换,多数实现(ingress-nginx、Istio、kgateway)同时支持两者;新项目优先 Gateway API,存量 Ingress 慢慢迁,网关实现不用换。
参考:reference/k8s-in-action/network/kgateway/SKILL.md、reference/k8s-in-action/network/istio/gateway-api/README.md
常见坑与排错
404、502、503 的区别
这几个状态码都意味着「请求没被正确送到后端」,但原因完全不同,先把它们分开:
| 状态码 | 谁返回的 | 含义 | 最常见原因 |
|---|---|---|---|
| 404 | Controller 的默认后端 | 没有任何规则匹配这个请求 | Host 或 path 对不上;ingressClassName 写错导致规则没被加载 |
| 404 | 后端 Pod | 规则匹配上了,但应用没有这个路径 | path 带了 /api 前缀而后端只认 /,需要 rewrite-target |
| 502 | Controller | 连不上后端,或后端给了无效响应 | targetPort 写错、后端进程崩了、Pod 正在重启 |
| 503 | Controller | 后端列表为空,或全部不健康 | EndpointSlice 为空、Pod 未通过就绪探针 |
| 504 | Controller | 连上了后端,但后端响应超时 | 后端处理太慢,或 proxy-read-timeout 之类参数太小 |
区分办法很直接:404 先看规则,502/503/504 先看 EndpointSlice。kubectl 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 demo→kubectl get pods -n demo→kubectl describe ingress kube101 -n demo→kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50的顺序查,最常见是 EndpointSlice 为空或port.number与 Service 的port不一致。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
ADDRESS 一直为空 | Controller 没装好或没在运行 | kubectl get pods -n ingress-nginx、kubectl get ingressclass | 重装 Controller,确认有 nginx 类 |
返回 default backend - 404 | 请求的 Host 没有匹配任何规则 | kubectl describe ingress kube101 -n demo 看 Rules | 补 /etc/hosts 或改用 curl -H "Host: ..." |
Could not resolve host | 域名没有解析 | cat /etc/hosts、nslookup 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 ingress 看 Rules 与 path | 核对 path、pathType,注意更长前缀优先 |
apply 报 pathType: Required value | 漏写 pathType | 看报错点名的字段 | 补 pathType: Prefix |
| 改了 Ingress 但行为没变 | 改错了对象或命名空间 | kubectl get ingress -A 找同名规则 | 确认在 demo 里、ingressClassName 正确 |
动手练习:用域名分流两个服务
- 装好 ingress-nginx,确认
kubectl get ingressclass里有nginx。 - 创建
web与api两个 Deployment 和 Service,确认两者的 EndpointSlice 都非空。 - 应用
web-ingress.yaml,用curl -H "Host: kube101.local" http://localhost/与http://localhost/api分别验证。 - 把
/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 而不是 /。反过来说,如果你把 /api 的 pathType 改成 Exact,/api/users 就不再匹配它,而是落到 / 那条规则上,表现会突然「变成前端页面」。
自测:Ingress 的后端为什么必须写 Service,不能直接写 Pod IP?(点击展开答案)
因为 Pod IP 会随重建而改变,副本数也会增减,把 IP 写进路由规则等于把不稳定性直接引入配置。Service 提供的是稳定入口,Controller 只记 Service 的名字和端口,后端 Pod 的增删由 EndpointSlice 承接,规则一个字都不用改。这也正好对应本章的分工:Ingress 管「怎么分」,Service 管「给谁」,Controller 管「实际转发」。
小结
- Ingress 是路由规则,Ingress Controller 是实现;只写 Ingress 不装 Controller 不会有任何效果。
- Ingress 工作在七层,按
host和path分流;Service 工作在四层,负责选 Pod。 - 后端必须是同命名空间下的 Service,
backend.service.port.number对应 Service 的port。 ingressClassName决定由谁处理规则,pathType: Prefix下更长的路径优先。- 验证靠
Host头(改 hosts 或curl -H);502基本都指向「后端没有就绪 Pod」。
练习
- 再加一条
host: api.kube101.local的规则,让它直接路由到apiService,并用curl -H验证。 - 故意把 Ingress 里
api的端口改成9999,观察访问/api的返回,再从 Controller 日志里找出对应记录。 - 想一下:如果集群里有 20 个服务都要对外暴露,为什么「每个服务一个 LoadBalancer」不如「一个 Ingress 统一入口」划算?
应用已经能被外部访问了,但镜像里往往还写着数据库地址、日志级别这些跟环境相关的东西。下一章我们用 第 9 章 ConfigMap 与 Secret 把配置从镜像里彻底拿出来。
相关章节:Ingress 的后端 Service 是怎么建的,见第 7 章 Service;如果连不上,第 15 章 排障手册 给了一套固定的排查顺序。