
1. 从一次 404 说起k8s Ingress Nginx 部署与 rewrite-target 路由重写到底解决什么问题如果你在 k8s 集群里跑过前端或后端服务大概率遇到过这种场景服务本身监听的是根路径/但对外暴露时希望走/api、/web这类前缀或者后端接口定义里带了版本号/v1/xxx而网关层想统一收口成/service/v1/xxx。这时候如果只靠 Service 的 NodePort 或 ClusterIP路径是原样透传的前端请求/api/user打到后端就变成 404因为后端只认/user。Ingress Nginx 就是来解决这层「七层路由 路径改写」的。它由三部分组成一个以 NodePort 或 LoadBalancer 暴露的 Service、一个真正干活的 Ingress ControllerNginx 实现、以及一份声明路由规则的 Ingress 资源。Controller 监听 Ingress 对象的变化动态生成 Nginx 配置并 reload整个过程不用你手改 nginx.conf。而rewrite-target注解是这套体系里最常被搜、也最容易踩坑的能力。它的作用是在请求真正转发给后端 Service 之前把匹配到的路径按正则捕获组重写。比如把/something/foo改写成/foo把/api/v1/user改写成/v1/user。配合path: /something(/|$)(.*)这种正则写法就能实现「去前缀」「加前缀」「路径截断」等一整套路由重写规则。这篇内容面向的是已经在用或准备用 k8s 的运维/后端/全栈同学想快速把 Ingress Nginx 跑起来并且把 rewrite-target 配明白。同时我会把 TaoToken 的统一 Key 接入实践串进来——因为很多团队在集群里跑 AI 网关、模型代理服务时需要统一管理上游 API KeyTaoToken 的 API 通道正好可以作为一个上游服务被 Ingress 暴露和重写路由。下面从部署到验证一步步来。2. 部署前的准备TaoToken 统一 Key 与 API 通道在 k8s 里的定位在讲 YAML 之前先把 TaoToken 在这个架构里的角色说清楚不然后面配置会懵。TaoToken 提供的是统一的 API 通道和 Key 管理能力官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。你可以把它理解成集群里跑一个「模型调用代理服务」这个服务对外暴露一个路径内部拿着统一的 Key 去请求上游模型接口。为什么要在 k8s 里做这件事因为如果每个业务 Pod 各自持有不同的 Key轮换、审计、限流都很痛苦。把 Key 收敛到一个上游服务再由 Ingress 统一暴露路径业务侧只需要访问集群内的 Service 域名或 Ingress 域名即可。这时候 rewrite-target 就派上用场了业务侧可能习惯用/ai/chat/completions而上游服务实际监听的是/v1/chat/completions中间这层前缀改写就交给 Ingress。你需要提前准备的东西第一一个可用的 k8s 集群版本建议 1.20 以上因为networking.k8s.io/v1的 Ingress API 在 1.19 之后才稳定。第二kubectl已配置好上下文能执行kubectl get nodes。第三一个域名或 hosts 可解析的地址用于验证 Ingress 规则。第四TaoToken 的 API Key在控制台创建即可地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 的创建入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。这里有个关键点TaoToken 的 Key 不要硬编码在 Ingress 或 Deployment 的明文里正确做法是放进 k8s Secret然后通过环境变量注入到上游服务的 Pod。Ingress 本身不碰 Key它只负责路由和重写。这样职责清晰Ingress 管流量路径Secret 管凭证Service 管后端发现。另外提醒一句如果你的集群是云厂商托管的可能已经自带了一个 Ingress Controller先执行kubectl get pods -A | grep ingress确认避免重复部署导致端口冲突。如果没有再按下面的步骤装。TaoToken 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 API 通道的详细说明配路由前可以先扫一眼路径规范。3. 可复制配置Ingress Nginx 部署与 rewrite-target 路由重写 YAML这一节是核心所有 YAML 都可以直接复制。我按「命名空间与 Service → Controller → Ingress 规则 → rewrite-target」的顺序给每一步都说明改哪里。先创建命名空间和 Service。注意新版 Ingress Nginx 的标签选择器和老版本mandatory.yaml有差异下面这份是兼容性较好的写法apiVersion: v1 kind: Namespace metadata: name: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- apiVersion: v1 kind: Service metadata: name: ingress-nginx namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx spec: type: NodePort ports: - name: http port: 80 targetPort: 80 protocol: TCP nodePort: 30080 - name: https port: 443 targetPort: 443 protocol: TCP nodePort: 30443 selector: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx保存为nginx-service.yaml执行kubectl apply -f nginx-service.yaml。这里我显式指定了nodePort: 30080方便后面验证时不用去查随机端口。如果你的集群 30080 被占用改成 30081 之类即可。接着是 Controller 的 Deployment。为了篇幅可控RBAC 部分我用官方推荐的最小权限集核心是 ServiceAccount、ClusterRole、ClusterRoleBinding 和 DeploymentapiVersion: 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, get] - apiGroups: [] resources: [nodes] verbs: [get] - apiGroups: [] resources: [services] verbs: [get, list, watch] - apiGroups: [] resources: [events] verbs: [create, patch] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses] verbs: [get, list, watch] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses/status] verbs: [update] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nginx-ingress-clusterrole-nisa-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nginx-ingress-clusterrole subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-ingress-controller namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx template: metadata: labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx annotations: prometheus.io/port: 10254 prometheus.io/scrape: true spec: serviceAccountName: nginx-ingress-serviceaccount terminationGracePeriodSeconds: 300 containers: - name: nginx-ingress-controller image: registry.k8s.io/ingress-nginx/controller:v1.9.4 args: - /nginx-ingress-controller - --publish-service$(POD_NAMESPACE)/ingress-nginx - --election-idingress-controller-leader - --ingress-classnginx - --configmap$(POD_NAMESPACE)/nginx-configuration securityContext: allowPrivilegeEscalation: true capabilities: drop: [ALL] add: [NET_BIND_SERVICE] runAsUser: 101 env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ports: - name: http containerPort: 80 - name: https containerPort: 443 livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP periodSeconds: 10保存为nginx-controller.yaml执行kubectl apply -f nginx-controller.yaml。注意镜像我用的是registry.k8s.io/ingress-nginx/controller:v1.9.4这是较新的稳定版老教程里的quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0已经比较旧API 版本和注解行为都有差异建议用新版。然后是 Ingress 规则本体这里直接演示 rewrite-target 去前缀apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: rewrite-demo namespace: default annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/use-regex: true spec: ingressClassName: nginx rules: - host: rewrite.demo.local http: paths: - path: /api(/|$)(.*) pathType: ImplementationSpecific backend: service: name: taotoken-upstream port: number: 80这份配置的含义是访问http://rewrite.demo.local/api/v1/chat/completionsIngress 会把路径重写成/v1/chat/completions再转发给taotoken-upstream这个 Service。/$2里的$2对应正则(/|$)(.*)的第二个捕获组也就是/api之后的部分。use-regex: true是必须的否则 Nginx 不会把 path 当正则解析。如果你要把 TaoToken 的 API 通道接进来taotoken-upstream这个 Service 指向的 Pod 里环境变量注入 KeyapiVersion: v1 kind: Secret metadata: name: taotoken-secret namespace: default type: Opaque stringData: TAOTOKEN_API_KEY: sk-你的实际Key --- apiVersion: apps/v1 kind: Deployment metadata: name: taotoken-upstream namespace: default spec: replicas: 1 selector: matchLabels: app: taotoken-upstream template: metadata: labels: app: taotoken-upstream spec: containers: - name: proxy image: nginx:1.25-alpine env: - name: TAOTOKEN_API_KEY valueFrom: secretKeyRef: name: taotoken-secret key: TAOTOKEN_API_KEY ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: taotoken-upstream namespace: default spec: selector: app: taotoken-upstream ports: - port: 80 targetPort: 80这里taotoken-upstream我用了一个占位镜像实际你可以换成自己的代理服务镜像只要它监听 80 并读取TAOTOKEN_API_KEY环境变量即可。Base URL 用 https://taotoken.net/api Model ID 按你实际调用的模型填。这三件套Base URL Key Model ID在接入任何上游服务时都要对齐缺一个都会报错。4. 验证请求kubectl 命令与 curl 实测成功结果配置写完必须验证。先看 Pod 和 Service 状态kubectl get pods -n ingress-nginx kubectl get svc -n ingress-nginx kubectl get ingress -n default正常输出里nginx-ingress-controller应该是Runningingress-nginxService 的PORT(S)显示80:30080/TCP,443:30443/TCPIngress 的ADDRESS可能为空自建集群正常但PORTS是80。接着确认 Controller 真的加载了你的 Ingress 规则。进到 Controller Pod 里看生成的 nginx.confkubectl exec -n ingress-nginx deploy/nginx-ingress-controller -- cat /etc/nginx/nginx.conf | grep -A 5 rewrite-demo如果能看到rewrite ^/api(/|$)(.*) /$2 break;这类指令说明 rewrite-target 生效了。这一步很关键很多人配了注解但没生效就是正则没被识别。然后做实际请求验证。先在本地 hosts 加一条echo 127.0.0.1 rewrite.demo.local | sudo tee -a /etc/hosts如果你集群节点 IP 不是 127.0.0.1把 127.0.0.1 换成节点 IP。然后用 curl 打请求curl -v http://rewrite.demo.local:30080/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d {model:你的Model ID,messages:[{role:user,content:ping}]}预期结果是请求路径/api/v1/chat/completions被重写成/v1/chat/completions上游服务收到后正常返回。如果你在 Controller 日志里看到GET /v1/chat/completions HTTP/1.1 200就说明重写成功。日志命令kubectl logs -n ingress-nginx deploy/nginx-ingress-controller --tail50实测下来最容易确认成功的方式是对比「带前缀」和「不带前缀」两次请求的返回。带前缀走 Ingress 返回 200不带前缀直接打 Service 返回 404说明重写确实在 Ingress 层发生了。另外如果你用的是 TaoToken 的模型对话能力可以在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 里先验证 Key 和 Model ID 是否可用再放到集群里跑能省不少排查时间。对于长期在集群里跑编码类 Agent 的场景TaoToken 的 Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合需要持续调用、统一计费的团队。Claude Code 相关的接入说明在 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 如果你在集群里跑的是这类工具路由重写规则要按它的实际路径来调。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照配 Ingress rewrite-target报错基本集中在几类。我按真实遇到的顺序列。第一类401 Unauthorized。这通常不是 Ingress 的问题而是上游服务没拿到 Key。检查 Secret 是否挂载成功kubectl exec -n default deploy/taotoken-upstream -- env | grep TAOTOKEN如果输出为空说明secretKeyRef名字或 key 写错了。注意 Secret 的stringData里 key 是TAOTOKEN_API_KEYDeployment 里引用的 key 必须完全一致。另外Key 本身如果失效或额度用尽也会 401去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认状态。第二类local proxy failed或connect() failed (111: Connection refused)。这是 Ingress 找不到后端 Service。先确认 Service 的selector和 Pod 的 label 对得上kubectl get endpoints taotoken-upstream -n default如果ENDPOINTS是none说明 selector 不匹配。Ingress 的 backend 里service.name和service.port.number也要和 Service 定义一致端口写错一样连不上。第三类reading choices或类似解析错误。这类报错通常出现在上游返回体不是预期 JSON 时根因往往是 rewrite-target 把路径改错了导致上游返回了 HTML 错误页而不是 JSON。排查方法临时把rewrite-target去掉看请求能否正常如果能说明正则捕获组写错了。重点检查path: /api(/|$)(.*)和rewrite-target: /$2的对应关系$2必须是第二个捕获组。如果你写成了/$1就会把/api本身带过去。第四类OAuth 或鉴权跳转异常。如果你在上游服务前还挂了 OAuth 代理rewrite-target 可能会把回调路径也改掉导致redirect_uri不匹配。这时候要么给回调路径单独开一条不做重写的 Ingress 规则要么用nginx.ingress.kubernetes.io/configuration-snippet做更细的控制。注意configuration-snippet在新版里默认被禁用需要在 ConfigMap 里显式开启allow-snippet-annotations: true。第五类ingressClassName不生效。新版 Ingress 用spec.ingressClassName: nginx老版用注解kubernetes.io/ingress.class: nginx。如果你两个都写了可能冲突。统一用ingressClassName并确认 Controller 启动参数里有--ingress-classnginx。第六类路径匹配不到。pathType用ImplementationSpecific才能配合正则用Prefix时正则不生效。这是很多人 rewrite-target 失效的直接原因。排查顺序建议先kubectl get endpoints确认后端可达再kubectl exec进 Controller 看 nginx.conf 里有没有 rewrite 指令最后 curl 对比重写前后路径。三步走完基本能定位。6. 把统一 Key 接入落到集群从 Ingress 到 API 通道的完整链路最后把整条链路串一下方便你直接落地。集群里跑一个上游代理服务它读取 Secret 里的 TaoToken Key监听 80 端口Service 把它暴露成taotoken-upstreamIngress 用rewrite-target把业务侧习惯的/api/xxx改写成上游认的/xxxController 通过 NodePort 30080 对外。业务 Pod 只需要访问http://rewrite.demo.local:30080/api/v1/chat/completionsKey 和路径改写都在网关层完成。如果你要接的是模型对话类请求先在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 验证通路再放进集群。如果是长期编码或 Agent 场景Coding Plan 更合适入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。API 通道的完整文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 配路由前对照一下路径规范能少踩很多坑。一个实用技巧把rewrite-target和use-regex成对写并且给每条规则加注释说明捕获组含义团队协作时别人一眼能看懂。另外Controller 的 ConfigMap 里可以开log-format-upstream把重写后的路径打进日志排查时非常直观。这些配置都在nginx-configuration这个 ConfigMap 里改完 Controller 会自动 reload不用重启 Pod。