1. 为什么要在 K8s 里用 nginx-controller 做统一网关
很多团队在 Kubernetes 里跑微服务,前端、后端、AI 能力调用各走各的入口,时间一长就会出现证书散落、路由规则混乱、鉴权逻辑重复的问题。我试过把 nginx-controller 作为集群的统一入口网关,把外部流量先收进来,再由网关按域名和路径转发到后端服务,后端服务再通过 TaoToken 的统一 Key 去调用大模型能力,整条链路会清爽很多。
nginx-controller 本质上是一个 Kubernetes Operator,它监听你创建的 NginxConf 自定义资源,把配置动态渲染成本地 nginx 的配置文件并热更新。你可以把它理解成「用 K8s 的声明式方式管理 nginx 配置」:加一个代理、挂一张证书、改一段 gzip 策略,都通过kubectl apply完成,不需要登录到 nginx 容器里手改nginx.conf。它支持 http、stream、custom 三种配置类型,证书可以来自 Value 内联、ConfigMap 或 Secret,更新时通过注解触发事件即可。
这套方案适合谁?适合已经在用 K8s、希望把入口流量统一收口、同时又想让后端服务以统一通道调用大模型 API 的团队。本文会从 CRD 安装、controller 部署、Ingress 路由、TLS 证书三种挂载方式,一直讲到用 curl 验证网关转发和 TaoToken 鉴权是否生效,每一步都给可复制的清单和命令。
核心检索词先明确:Kubernetes 安装 nginx-controller 作为统一网关,配合 TaoToken 统一 Key 接入大模型能力。下面按可跟做的顺序展开。
2. 安装 nginx-controller 的 CRD 与 Deployment 清单
2.1 创建 NginxConf 的 CRD
先准备 CRD。注意如果你的集群版本低于 1.29,需要删掉 CRD 里所有x-kubernetes-validations字段,否则 apply 会报 schema 校验错误。下面是精简后的 CRD 结构,保留了核心字段:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: nginxconfs.stable.lhstack.com spec: group: stable.lhstack.com scope: Namespaced names: kind: NginxConf plural: nginxconfs singular: nginxconf listKind: NginxConfList shortNames: - ncf versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object required: - config properties: configType: type: string enum: [http, stream, custom] default: http customConfigPath: type: string config: type: string additions: type: object properties: values: type: array items: type: object properties: value: type: string path: type: string configMaps: type: array items: type: object properties: name: type: string namespace: type: string path: type: string secrets: type: array items: type: object properties: name: type: string namespace: type: string path: type: string执行创建:
kubectl apply -f crd.yaml kubectl explain NginxConfkubectl explain能正常输出字段说明,就说明 CRD 注册成功了。
2.2 部署 controller 的 Deployment 与 Service
controller 我用 Deployment 部署,副本数 2 保证可用性,同时建一个 NodePort Service 暴露 80/443。清单如下:
apiVersion: v1 kind: Namespace metadata: name: ingress --- apiVersion: v1 kind: ServiceAccount metadata: name: nginx-controller namespace: ingress --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nginx-controller subjects: - kind: ServiceAccount name: nginx-controller namespace: ingress roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-controller namespace: ingress spec: replicas: 2 selector: matchLabels: app: ingress template: metadata: labels: app: ingress spec: serviceAccountName: nginx-controller containers: - name: controller image: lhstack/nginx-controller:latest imagePullPolicy: IfNotPresent ports: - containerPort: 80 name: http - containerPort: 443 name: https readinessProbe: httpGet: port: 9099 path: /readyz initialDelaySeconds: 5 periodSeconds: 30 livenessProbe: httpGet: port: 9099 path: /healthz initialDelaySeconds: 5 periodSeconds: 60 env: - name: KUBE_NAMESPACE value: "ingress" resources: requests: memory: 32Mi cpu: 10m limits: memory: 64Mi cpu: 10m --- apiVersion: v1 kind: Service metadata: name: ingress namespace: ingress spec: selector: app: ingress type: NodePort clusterIP: 10.43.80.80 ports: - port: 80 name: http protocol: TCP nodePort: 30080 - port: 443 name: https protocol: TCP nodePort: 30443这里KUBE_NAMESPACE=ingress表示只监听 ingress 命名空间下的 NginxConf,做配置隔离。如果你希望监听所有命名空间,把这个环境变量留空即可。clusterIP固定成 10.43.80.80 是为了方便内网 DNS 指向,前提是集群里已经装了 DNS 服务。
apply 之后确认两个副本都起来了:
kubectl apply -f deployment.yaml kubectl -n ingress get pods -o wide kubectl -n ingress get svc ingress看到两个 Running 的 Pod 和 NodePort 30080/30443,前置就绪。
3. 配置 Ingress 路由与 TaoToken 统一 Key 接入片段
3.1 用 NginxConf 写一条 http 代理
先写一条最简单的代理,把baidu.lhstack.com转发到外部站点,验证路由链路:
apiVersion: stable.lhstack.com/v1 kind: NginxConf metadata: name: baidu-web namespace: default spec: config: | server { server_name baidu.lhstack.com; listen 80; gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1000; gzip_comp_level 6; location / { proxy_pass https://www.baidu.com; proxy_http_version 1.1; } }kubectl apply -f baidu-nginx-conf.yaml kubectl -n ingress logs -l app=ingress --tail=50日志里能看到 controller 检测到 NginxConf 变更并完成 reload,就说明动态更新生效了。
3.2 后端服务通过 TaoToken 统一 Key 调用大模型
网关收口之后,后端服务调用大模型时不再各自维护 Key,而是统一走 TaoToken 的 API 通道。TaoToken 的 API 地址是https://taotoken.net/api,控制台和文档入口分别是:
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
后端服务里建议把 Base URL、Key、Model ID 三件套放进 ConfigMap 或 Secret,再由 Deployment 注入环境变量。下面是一个可复制的 settings 片段,路径与原文一致:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoTokenKey", "model_id": "claude-sonnet-4-5", "timeout": 60, "max_retries": 2 }如果你用的是 Codex 风格的auth.json,可以这样写:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoTokenKey", "model": "claude-sonnet-4-5" }在 K8s 里把它做成 Secret:
kubectl -n default create secret generic taotoken-cred \ --from-literal=TAOTOKEN_BASE_URL=https://taotoken.net/api \ --from-literal=TAOTOKEN_API_KEY=sk-你的TaoTokenKey \ --from-literal=TAOTOKEN_MODEL_ID=claude-sonnet-4-5然后在后端 Deployment 里引用:
env: - name: TAOTOKEN_BASE_URL valueFrom: secretKeyRef: name: taotoken-cred key: TAOTOKEN_BASE_URL - name: TAOTOKEN_API_KEY valueFrom: secretKeyRef: name: taotoken-cred key: TAOTOKEN_API_KEY - name: TAOTOKEN_MODEL_ID valueFrom: secretKeyRef: name: taotoken-cred key: TAOTOKEN_MODEL_ID这样后端服务只需要读环境变量,不把 Key 硬编码进镜像。网关层负责路由和 TLS,鉴权层由 TaoToken 统一 Key 承担,职责清晰。
3.3 给网关加上 TLS 证书
证书支持三种挂载方式:Value 内联、ConfigMap、Secret。先用 Value 方式演示,把证书内容直接写进 NginxConf:
apiVersion: stable.lhstack.com/v1 kind: NginxConf metadata: name: baidu-web namespace: default spec: additions: values: - path: /opt/tls/baidu/tls.key value: | -----BEGIN EC PRIVATE KEY----- ...你的私钥内容... -----END EC PRIVATE KEY----- - path: /opt/tls/baidu/tls.crt value: | -----BEGIN CERTIFICATE----- ...你的证书内容... -----END CERTIFICATE----- config: | server { listen 80; server_name baidu.lhstack.com; rewrite ^(.*)$ https://${server_name}$1 permanent; } server { server_name baidu.lhstack.com; listen 443 ssl http2; client_max_body_size 50m; ssl_certificate /opt/tls/baidu/tls.crt; ssl_certificate_key /opt/tls/baidu/tls.key; ssl_session_timeout 5m; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; location / { proxy_pass https://www.baidu.com; proxy_http_version 1.1; } }如果证书用 ConfigMap 保存,把additions.values换成additions.configMaps:
spec: additions: configMaps: - name: baidu-nginx-conf namespace: kube-system path: /opt/tls/baidu用 Secret 保存则换成additions.secrets,注意 Secret 的data要求 base64 格式:
apiVersion: v1 kind: Secret metadata: name: baidu-nginx-conf namespace: kube-system type: kubernetes.io/tls data: tls.key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0t... tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...三种方式各有取舍:Value 适合临时验证,ConfigMap 适合明文配置,Secret 适合证书这类敏感内容。生产环境建议用 Secret。
4. 用 curl 验证网关转发与 TaoToken 鉴权是否生效
4.1 验证网关转发
先确认 NodePort 可达,把域名解析到节点 IP 或固定 clusterIP:
curl -I http://baidu.lhstack.com:30080 curl -I https://baidu.lhstack.com:30443 -k预期看到 301 跳转到 https,以及 200 的响应头。如果返回 404,说明 NginxConf 没被 controller 加载,检查KUBE_NAMESPACE是否和 NginxConf 的 namespace 一致。
4.2 验证 TaoToken 鉴权
后端服务里用 curl 直接打 TaoToken 的 API,确认 Key 有效:
curl -sS https://taotoken.net/api/v1/messages \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 64, "messages": [{"role": "user", "content": "ping"}] }'返回里带content字段就说明鉴权通过。如果返回 401,说明 Key 无效或没带上Authorization头;如果返回local proxy failed,通常是 Base URL 写错或网络出口不通。
4.3 验证证书更新
证书过期后,如果用的是 ConfigMap 或 Secret,需要手动更新内容再触发事件:
kubectl annotate NginxConf --all -A --overwrite updated=$(date +%s)如果只想更新某一个:
kubectl annotate -n default NginxConf baidu-web --overwrite update=$(date +%s)用 Value 方式则不需要,因为 value 本身是 NginxConf 的字段,改动会自然触发更新。
5. 本篇常见报错排查
5.1 401 Unauthorized
最常见的是 Key 没带对。检查三件事:Authorization头是不是Bearer sk-xxx格式;Key 有没有多余空格;Base URL 是不是https://taotoken.net/api。如果后端服务从 Secret 注入,确认 Secret 的 key 名和 Deployment 里secretKeyRef.key完全一致。
5.2 local proxy failed
这个报错通常出现在请求还没到 TaoToken 就被本地网络拦了。检查集群节点能不能出网,DNS 能不能解析taotoken.net,以及有没有在 Pod 里配了错误的HTTP_PROXY。如果用了 NetworkPolicy,确认允许到 443 的出站流量。
5.3 reading choices 报错
这个一般出现在流式响应解析阶段,说明客户端把非流式响应当流式读了。检查请求体里有没有误加stream: true,或者客户端 SDK 的解析逻辑和实际响应格式不匹配。先用 curl 打一次非流式请求确认返回结构,再调客户端。
5.4 OAuth 相关报错
如果后端服务用的是 OAuth 方式拿 token,报错通常是 token 过期或 scope 不足。确认 token 有效期,以及申请的 scope 是否包含模型调用权限。用 API Key 方式可以绕开这类问题,建议后端服务优先用 Key。
5.5 NginxConf 不生效
先看 controller 日志:
kubectl -n ingress logs -l app=ingress --tail=100如果日志里没有检测到 NginxConf,检查KUBE_NAMESPACE和 NginxConf 的 namespace 是否匹配。如果日志里有 reload 失败,检查config里的 nginx 语法,可以用nginx -t在本地先验证。
6. 把网关和统一 Key 串起来的下一步
整套链路跑通之后,你会发现入口流量和模型调用被拆成了两层:nginx-controller 负责路由、TLS、限流这些网关职责,TaoToken 负责统一 Key、模型路由、用量统计这些鉴权职责。后端服务只需要关心业务逻辑,不用再为每个模型维护一套 Key。
如果你还在选模型阶段,可以先用模型对话页面快速试一下不同模型的效果:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
如果是要长期跑编码类或 Agent 类任务,建议直接上 Coding Plan,配额和稳定性更适合持续调用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
接入过程中遇到鉴权或路由问题,先查 API Keys 页面确认 Key 状态,再对照接入文档核对 Base URL 和请求格式:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 和 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
最后留一个实操建议:把 NginxConf 和 Secret 都纳入 Git 管理,证书更新走 CI 触发kubectl annotate,这样证书轮换就不会漏。网关配置和 Key 配置分离,排障时能快速定位是路由问题还是鉴权问题。