Kubernetes Gateway API 从对象模型到生产治理
一次 API 发布最容易出现的错觉,是 YAML 已经被 API Server 接收,入口地址也已经分配,于是大家默认新路由已经上线。真实请求却可能仍然命中旧上游,或者因为 Route 没有附着到 listener、后端引用没有授权,只在状态字段里留下拒绝原因。此时继续盯着 kubectl apply 的退出码,只会把声明成功误当成流量成功。
Gateway API 的价值正在于把这条链拆成可检查的对象:平台选择实现,入口所有者声明 listener,应用团队发布 Route,后端命名空间决定是否接受跨边界引用。标准只定义对象与行为合同,真正供应负载均衡器、代理 Pod、云网络或其他数据面的是所选控制器。先把两者分开,后面的安装、排障和迁移才不会混成一团。
先分清标准对象和实现控制器
Gateway API 是一组 Kubernetes API 规范、CRD 和一致性测试,不是一个会转发流量的进程。安装 CRD 只让 API Server 认识 GatewayClass、Gateway、HTTPRoute、ReferenceGrant 等对象;还必须安装一个实现这些对象的控制器,才会发生 reconciliation、基础设施供应和数据面配置下发。
四个对象承担不同责任:
GatewayClass 是集群级类,spec.controllerName 选择实现;它不承诺公网、内网、共享代理或固定价格。Gateway 属于命名空间,声明地址请求与 listener。spec.addresses 是期望,status.addresses 才是实现报告的实际地址。HTTPRoute 用 parentRefs 请求附着到 Gateway 或指定 listener,再用 host、match、filter 和 backendRef 描述 HTTP 行为。
ReferenceGrant 放在被引用对象的命名空间,由资源所有者增加一条跨命名空间信任。它没有 deny,也不会覆盖其他 grant。
Route 到 Gateway 的跨命名空间附着不用 ReferenceGrant。它依靠 Route 的 parentRefs 与 listener 的 allowedRoutes 双向握手;Route 再去引用另一命名空间的 Service 或 Gateway listener 引用另一命名空间的 Secret,才进入 ReferenceGrant 的授权链。
安装时先确认 CRD 归谁管理
在隔离集群操作前,先固定 Kubernetes、Gateway API bundle 和控制器版本,并从所选控制器的兼容矩阵确认组合。下面以稳定的 Gateway API v1.5.1 Standard bundle 为可执行基线;版本升级必须作为受审查变更进入仓库,不能让下载地址漂移到最新标签。Experimental channel 或“CRD 中存在字段”也不等于控制器已经实现该能力。
export GATEWAY_API_VERSION='v1.5.1'
export GATEWAY_CLASS_NAME='<class-created-for-this-lab>'
export CONTROLLER_NAME='<implementation-controller-domain/path>'
kubectl version
kubectl get crd gateways.gateway.networking.k8s.io -o yaml 2>/dev/null || true
kubectl get gatewayclass \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.controllerName}{"\t"}{range .status.conditions[?(@.type=="Accepted")]}{.status}{"/"}{.observedGeneration}{end}{"\n"}{end}'若平台已经管理 Gateway API CRD,先向平台确认 release channel、served/storage versions、字段管理者和升级责任,只安装兼容的控制器组件。若实验集群没有这些 CRD,可从固定 release 安装 Standard bundle:
curl -fL -o /tmp/gateway-api-standard.yaml \
"https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/standard-install.yaml"
kubectl apply --server-side --dry-run=server \
-f /tmp/gateway-api-standard.yaml
kubectl diff --server-side -f /tmp/gateway-api-standard.yaml || true
kubectl apply --server-side -f /tmp/gateway-api-standard.yaml
kubectl get crd gateways.gateway.networking.k8s.io \
httproutes.gateway.networking.k8s.io \
referencegrants.gateway.networking.k8s.io接着按所选实现的固定版本安装控制器。安装动作必须来自该实现的版本化官方指南,不能用下面这条注释代替真实命令:
# 在这里执行所选实现的固定版本安装命令,然后检查其 namespace。
kubectl get deployments,pods -n '<controller-namespace>'
kubectl logs -n '<controller-namespace>' deploy/'<controller-deployment>' --tail=100控制器 Deployment Available 只证明进程存活。还要创建或确认一个 controllerName 匹配的 GatewayClass,并等待当前 generation 被接纳:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: lab-gateway
spec:
controllerName: example.net/gateway-controller上面的 example.net/gateway-controller 必须替换成目标实现声明的值。不要猜控制器名;先查版本化安装文档和现有 GatewayClass。应用后读取完整 condition:
kubectl apply -f gatewayclass.yaml
kubectl get gatewayclass lab-gateway -o yaml只有 Accepted=True、observedGeneration 等于对象的 metadata.generation,这个类才被当前控制器接纳。若没有 status,先检查 controllerName、控制器 watch scope 和 RBAC,而不是反复重建 CRD。
用两个上游跑通完整路由
实验创建 gateway-system、app-team 和 backend-team 三个命名空间。先在 app-team 放两个可区分的 HTTP 上游:orders-v1 返回 upstream=v1,orders-v2 返回 upstream=v2。这里使用通用回显镜像占位符,团队应把它替换为已批准且固定摘要的内部镜像。
apiVersion: v1
kind: Namespace
metadata:
name: gateway-system
---
apiVersion: v1
kind: Namespace
metadata:
name: app-team
labels:
gateway-access: shared-public
---
apiVersion: v1
kind: Namespace
metadata:
name: backend-team
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-v1
namespace: app-team
spec:
replicas: 2
selector:
matchLabels: {app: orders-v1}
template:
metadata:
labels: {app: orders-v1}
spec:
containers:
- name: echo
image: <approved-echo-image>@sha256:<digest>
args: ["--text=upstream=v1"]
ports: [{name: http, containerPort: 8080}]
readinessProbe:
httpGet: {path: /, port: http}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {memory: 128Mi}
---
apiVersion: v1
kind: Service
metadata:
name: orders-v1
namespace: app-team
spec:
selector: {app: orders-v1}
ports: [{name: http, port: 8080, targetPort: http}]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-v2
namespace: app-team
spec:
replicas: 2
selector:
matchLabels: {app: orders-v2}
template:
metadata:
labels: {app: orders-v2}
spec:
containers:
- name: echo
image: <approved-echo-image>@sha256:<digest>
args: ["--text=upstream=v2"]
ports: [{name: http, containerPort: 8080}]
readinessProbe:
httpGet: {path: /, port: http}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {memory: 128Mi}
---
apiVersion: v1
kind: Service
metadata:
name: orders-v2
namespace: app-team
spec:
selector: {app: orders-v2}
ports: [{name: http, port: 8080, targetPort: http}]先验证上游自身,不让网关替后端背锅:
kubectl apply -f upstreams.yaml
kubectl rollout status -n app-team deploy/orders-v1
kubectl rollout status -n app-team deploy/orders-v2
kubectl run -n app-team direct-check --rm -i --restart=Never \
--image='<approved-curl-image>@sha256:<digest>' -- \
sh -c 'curl -fsS http://orders-v1:8080/ && curl -fsS http://orders-v2:8080/'预期能分别看到 upstream=v1 与 upstream=v2。若直连失败,检查 EndpointSlice、readiness 和 NetworkPolicy,不要继续修改 Route。
Gateway 允许带指定标签的命名空间提交 HTTPRoute;Route 用两个相对权重相同的 backendRef 分流。权重是相对值,不是百分比字段,因此 1/1、50/50 都表达等比例。
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-http
namespace: gateway-system
spec:
gatewayClassName: lab-gateway
listeners:
- name: http
protocol: HTTP
port: 80
hostname: api.example.test
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: shared-public
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders
namespace: app-team
spec:
parentRefs:
- name: shared-http
namespace: gateway-system
sectionName: http
hostnames: [api.example.test]
rules:
- matches:
- path:
type: PathPrefix
value: /orders
backendRefs:
- name: orders-v1
port: 8080
weight: 1
- name: orders-v2
port: 8080
weight: 1kubectl apply -f gateway-route.yaml
kubectl get gateway -n gateway-system shared-http -o yaml
kubectl get httproute -n app-team orders -o yaml不要把 conditions 压成一行后凭肉眼判断。至少逐项核对:
Gateway 的整体 Accepted、Programmed 对当前 generation 为 True。status.listeners[] 中 http listener 的 conditions 没有冲突,attachedRoutes 符合预期。HTTPRoute 的 status.parents[] 中,目标 Gateway 与目标 controller 下 Accepted=True、ResolvedRefs=True,且 generation 新鲜。
status.addresses 给出的实际地址可从测试网络到达。
取得实现实际入口后发送真实请求。云环境可能给出主机名而不是 IP,本地实现也可能需要 port-forward;以实现报告的 status 和安装指南为准。
export GATEWAY_ADDRESS='<reachable-address>'
for i in $(seq 1 20); do
curl -fsS -H 'Host: api.example.test' "http://${GATEWAY_ADDRESS}/orders"
done | sort | uniq -c有限样本不能证明精确流量百分比,但应能看到两个上游都被命中。上线证据还要保存请求时间、Host、路径、响应体、request ID、Route generation 和控制器配置版本;否则出现偏流时无法区分随机波动、配置未更新与后端不健康。
三个反例揭示三种不同的拒绝
先把反例放在独立实验对象上,避免影响共享 Gateway 的其他 Route。
listener 名称错误:附着被拒绝
把 parentRefs[].sectionName 改成不存在的 https。API Server 仍可能接受 YAML,但目标 parent 下应出现 Accepted=False 及相应 reason;也可能在控制器尚未处理时暂时保留旧 condition。每次改 spec 后都先比较:
kubectl get httproute -n app-team orders \
-o jsonpath='{.metadata.generation}{"\n"}{range .status.parents[*].conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.observedGeneration}{"\n"}{end}'若 observedGeneration 小于 metadata.generation,结论只能是“状态仍旧”,不能说新配置已成功或已失败。将 sectionName 恢复为 http,等待新 generation 的 Accepted=True 后再继续。
Service 名称错误:引用无法解析
把一个 backendRef 改成 orders-missing。Route 仍可能附着成功,但 ResolvedRefs=False;对于本应转发到这个无效 backendRef 的请求,Gateway API 规范要求返回 HTTP 500,实验应保存状态码、Route generation 与访问日志,而不是只观察 curl 是否非零退出。这个实验说明 Accepted=True 只证明 Route 被 parent 接受,不证明全部后端引用有效。
修复时先确认 Service、端口与 EndpointSlice,再恢复 backendRef:
kubectl get service,endpointslice -n app-team
kubectl describe httproute -n app-team orders未授权跨命名空间:后端所有者拒绝引用
把 orders-v2 Deployment 与 Service 放到 backend-team,并在 HTTPRoute 中显式写 namespace: backend-team。尚未创建 grant 时,目标 parent 下应为 ResolvedRefs=False;错误信息不应泄露目标 Service 是否存在。
授权必须由 backend-team 创建:
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: allow-app-orders
namespace: backend-team
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: app-team
to:
- group: ""
kind: Service
name: orders-v2kubectl get referencegrant -n backend-team -o yaml
kubectl apply -f allow-app-orders.yaml
kubectl get httproute -n app-team orders -w等当前 generation 的 ResolvedRefs=True 后重放请求。随后删除 grant,确认控制器重新计算并撤销引用:
kubectl delete -f allow-app-orders.yaml
kubectl get httproute -n app-team orders -o yaml删除一个 grant 不一定能撤销权限,因为 ReferenceGrant 是加法授权。实验前后都要列出目标命名空间的全部 grant。TLS Secret 跨命名空间也是同一原则:授权对象放在 Secret 所在命名空间,而不是 Gateway 所在命名空间。
把 Route 接进项目发布流
生产仓库可以把平台对象与应用对象分开审查:
platform/gateway/
classes/
shared-gateways/
policies/
services/orders/deploy/
service.yaml
httproute.yaml
route-smoke.sh
rollback.md平台团队管理 CRD、GatewayClass、共享 Gateway 和 namespace 准入标签;入口团队管理 listener、证书与 allowedRoutes;应用团队只提交本命名空间 Route;后端团队通过 ReferenceGrant 决定是否接受跨边界依赖。CI 至少执行 schema 校验、server-side dry-run、策略准入和渲染 diff,部署后再等待目标 parent 的新鲜 conditions,并发出真实请求。
一个可用的发布门禁不是“所有 condition 都为 True”,而是针对目标 parent 和本次变更建立不变量:
metadata.generation == target conditions.observedGeneration
目标 parent: Accepted=True
全部必需引用: ResolvedRefs=True
Gateway: Programmed=True 且 status.addresses 可达
两个上游直连基线正常
正确 Host/Path 返回预期上游身份
错误 Host、错误引用和未授权引用保持拒绝回滚优先恢复上一份已验证的 Route 清单,而不是手工在线编辑。若变更涉及删除 listener、收紧 allowedRoutes 或撤销 ReferenceGrant,要先列出受影响 Route,因为这些动作会卸载其他团队的有效配置。
例如应用发布只改了 services/orders/deploy/httproute.yaml,流水线应先保存旧提交对应的渲染物,再用同一个 field manager 恢复,而不是执行 kubectl delete 后等待重建:
kubectl apply --server-side --field-manager=orders-delivery \
-f artifacts/previous/httproute.yaml
kubectl get httproute -n app-team orders -o yaml
# 等目标 parent 的 observedGeneration 追上新 generation 后重放基线。
curl -fsS -H 'Host: api.example.test' \
"http://${GATEWAY_ADDRESS}/orders"恢复旧 YAML 会产生新的 metadata.generation,因此不能拿发布前保存的绿色 condition 当回滚成功。只有目标 parent 重新出现新鲜的 Accepted=True、ResolvedRefs=True,正确请求恢复且错误 Host/未授权引用仍被拒绝,才算回滚闭环。若本次发布新建了 Route,删除前先确认它没有被其他 Gateway 接纳;CRD、GatewayClass、共享 Gateway 与 ReferenceGrant 不属于应用回滚清单,必须由各自 owner 单独审批。
策略附着不能只看对象存在
Gateway API 提供 Policy Attachment 的通用设计模式,但 Kubernetes 不会自动附送一套跨实现统一的限流、认证、重试 Policy。Standard 的 BackendTLSPolicy 与实现自定义 Policy 也不能混为一谈:前者约束 Gateway 到后端的 TLS 身份,后者可能处理客户端认证、限流或重试。具体 CRD、字段、继承、冲突和 condition 由目标 bundle 与实现决定;Experimental 能力只有在团队明确承担兼容与迁移预算时才进入生产候选。
评估一个策略要沿三层取证:
策略对象 status:目标引用对应的 Accepted condition 是否存在、generation 是否新鲜、reason 是否表明冲突或无效目标。目标与控制器证据:Gateway/Route condition、控制器日志、实现提供的生成配置或调试输出。数据面行为:有效与无效凭证、阈值内与阈值外请求、依赖不可达、策略撤销后的真实请求。
策略绑定到 listener 或命名 rule 前,先在目标版本执行 kubectl explain 并查看对应 CRD schema。不要把 development 文档中的 sectionName、继承或 merge 语义复制到 Standard 基线,也不要假设两个控制器会用同样的优先级。
conditions 是诊断入口,不是上线终点
常见故障可以按第一证据分层:
| 现象 | 第一证据 | 常见原因 | 修复后证明 |
|---|---|---|---|
| GatewayClass 无 status | controllerName、控制器 scope/RBAC | 没有实现负责该类 | 当前 generation Accepted=True |
Gateway Accepted=False | 整体与 listener conditions | 类未接纳、listener 冲突、参数无效 | 目标 listener condition 恢复 |
| Route 无目标 parent status | parentRef 与责任链 | 引用了不存在或非该控制器负责的 Gateway | 目标 parent 条目出现且 generation 新鲜 |
Route Accepted=False | parent reason | sectionName 错、namespace/kind/hostname 不被允许 | 双向附着成立 |
ResolvedRefs=False | backendRef/Secret 与 grant | 名称、端口、kind 错或跨 namespace 未授权 | 引用解析与反向请求同时恢复 |
| condition 正常但请求失败 | 数据面日志、EndpointSlice、NetworkPolicy | 地址不可达、后端未就绪、策略或网络拒绝 | 真实请求与上游身份符合预期 |
控制器日志排在 status 之后,因为 status 是实现对用户的稳定诊断接口。日志可以解释 reconcile 或供应失败,但不能替代当前对象的责任链;数据面访问日志则回答请求实际命中了哪个 listener、Route、策略与上游。三者的时间、request ID 和 generation 能关联起来,才算证据闭环。
升级和回退要把 CRD 当共享 Schema
升级前先查 Gateway API release notes、目标控制器兼容矩阵与 conformance report,确认 Standard/Experimental channel、Kubernetes 版本、served/storage versions、弃用字段和实现支持。CRD 是集群级共享 Schema,不能把它当成普通 Deployment 一起随意回滚;官方明确建议避免 CRD 降级,并优先逐个 minor 版本升级。
kubectl get crd gateways.gateway.networking.k8s.io -o yaml > /tmp/gateways-crd-before.yaml
kubectl get gatewayclass,gateway,httproute,referencegrant -A -o yaml > /tmp/gateway-api-before.yaml
kubectl get crd gateways.gateway.networking.k8s.io \
-o jsonpath='{.status.storedVersions}{"\n"}'
kubectl apply --server-side --dry-run=server -f /tmp/gateway-api-next.yaml
kubectl diff --server-side -f /tmp/gateway-api-next.yaml || true先在副本集群验证 CRD 与存量对象,再按控制器发行说明执行顺序。若新 controller 依赖新 CRD,顺序是先扩展兼容 Schema、再升级 controller、最后迁移业务对象;不能先启动一个无法识别旧 Schema 的控制器。升级后重跑新鲜 conditions、双上游、错误 listener、错误 Service、ReferenceGrant 创建与撤销、TLS 和策略反例。
回退前必须证明旧控制器能读取升级后的 CRD 和 stored object,并确认新字段没有成为业务正确性的必要条件。任一条件不成立就停止降级,保留新 CRD,以前向修复或经过验证的数据迁移恢复服务;恢复旧 Deployment 不会自动恢复旧数据模型,直接覆盖旧 CRD 还可能让现有对象无法解码。
安全、容量和成本最终落在同一张账上
共享 Gateway 扩大了复用,也扩大了故障域。RBAC 应让平台角色管理 CRD/GatewayClass,让入口 owner 管 Gateway/listener,让应用团队只管本命名空间 Route,让后端 owner 管 ReferenceGrant;普通用户不应写 status。allowedRoutes: All、宽泛 namespace selector 与过宽 grant 都要经过 admission、owner 审核和审计。
容量不能只看代理 CPU。listener、Route、backend、Endpoint 数量会增加控制器缓存与 reconcile 成本;连接数、请求体缓冲、TLS、重试和流量镜像会增加数据面 CPU、内存和带宽;全局限流、外部鉴权与 JWKS 又引入新的同步依赖。压测要同时观察配置收敛时延、拒绝率、代理饱和、后端放大和控制器队列,而不是只记录平均 QPS。
成本也不等于“Gateway API 免费”。标准与开源实现本身之外,云负载均衡器、公网地址、跨区/出口流量、证书、DNS、日志指标、托管集群、外部策略服务和冗余数据面都可能计费。每个 GatewayClass 都应带有 owner、供应模型、共享策略、容量预算、日志保留、升级窗口和退出办法;删除实验时先删除 Gateway 与可能生成的 LoadBalancer,再清理 Route、grant、控制器与 CRD,最后复查云资源和费用残留。
上线前最后复核
CRD channel、所有者、served/storage versions 与控制器兼容矩阵已经确认。GatewayClass、Gateway、目标 listener 与 HTTPRoute conditions 都对应当前 generation。两个上游有直连基线,真实入口能识别命中 Route、上游和请求 ID。
错误 listener、错误 backendRef、未授权跨命名空间引用都能稳定拒绝。ReferenceGrant 由目标命名空间 owner 管理,撤销实验考虑了其他加法 grant。策略同时有 status、生成配置或控制器证据、数据面正反请求三层证明。
RBAC、admission、NetworkPolicy、TLS Secret、日志与调试产物按高敏资产治理。升级顺序、Schema 迁移、业务回归和旧控制器可读性已经在副本集群演练。容量预算包含控制器与数据面,成本清单包含 LB、流量、观测和外部依赖。
清理后没有遗留 Gateway、LoadBalancer、跨命名空间授权、临时凭证和调试导出。
