
1. 裸金属 k8s 里 LoadBalancer 一直 Pending 的真实原因本地用 kubeadm 或二进制方式搭起来的 k8s 集群默认没有云厂商那套 LoadBalancer 实现。你写一个type: LoadBalancer的 Servicekubectl get svc里 EXTERNAL-IP 会一直卡在pending因为集群里没有任何组件去认领这个请求。这不是配置写错了而是裸金属环境天生缺一个能分配外部 IP 的角色。MetalLB 就是补这个缺口的。它做两件事一是从你划定的地址池里给 LoadBalancer 类型的 Service 分配一个局域网可达的 VIP二是通过二层模式在局域网里用 ARP 宣告这个 VIP让外部流量能找到持有该 IP 的节点。它不碰七层只负责把四层入口打通。七层的事交给 ingress-nginx。ingress-nginx 控制器本身也是一个 Pod它对外暴露的方式通常是一个 LoadBalancer 类型的 Service。这个 Service 拿到 MetalLB 分配的 VIP 后所有到达这个 VIP 的 80/443 流量就进了 ingress-nginx再由它根据 Ingress 资源里的 host 和 path 规则转发到后端微服务。整条链路是客户端 → MetalLB VIP → ingress-nginx Service → ingress-nginx Pod → 后端 Service → 后端 Pod。这套组合适合谁适合在本地机房、测试环境、边缘节点上跑微服务又不想引入云厂商负载均衡的人。它能让你的本地集群拥有和云上几乎一致的LoadBalancer体验Ingress 规则、TLS、Basic Auth 这些七层能力都能直接用。我这次要交付的不只是把链路跑通还要在入口这一层接上 TaoToken 的统一 Key 通道做鉴权与调用验证。也就是说外部请求打到 ingress-nginx 后除了路由到后端微服务还要能带着统一的 API Key 去调用模型接口验证入口鉴权和转发是否同时生效。下面从 MetalLB 地址池开始一步步给可复制的配置。2. TaoToken 统一 Key 通道前置准备在把七层链路搭起来之前先把 TaoToken 的接入参数准备好后面 Ingress 的鉴权注解和 curl 验证都要用到它。TaoToken 在这里扮演的角色是统一 Key 通道你不需要为每个模型或每个后端服务单独维护一套密钥而是用同一个 Key 去访问统一的 API 入口入口侧再做鉴权和转发。先拿到 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册登录后进入控制台。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面创建一个新的 Key。创建入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建时给它起个能认出来的名字比如k8s-ingress-test方便后面在 Ingress 注解里引用。拿到 Key 之后记住三个核心参数后面配置里会反复出现参数值用途Base URLhttps://taotoken.net/api所有请求的统一入口不带 UTMAPI Keysk-开头的一串放在 Authorization 头里做鉴权Model ID例如claude-sonnet-4-5指定要调用的模型这里要强调一点Base URL 用https://taotoken.net/api不要在后面拼多余的路径。很多 401 和 404 就是因为把 Base URL 写成了带/v1或带具体模型路径的形式。统一入口的设计就是让你只填根地址剩下的由通道侧处理。如果你后面要长期跑编码类或 Agent 类任务可以了解下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它适合需要持续调用、按周期使用的场景和本篇的一次性验证不冲突验证阶段用普通 API Key 就够了。模型对话的在线调试入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 你可以先在网页上确认 Key 能用、模型能通再去配 k8s。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言 SDK 的 Base URL 填法配 Ingress 注解前扫一眼能少踩坑。把 Key 存到一个环境变量里后面 curl 直接用避免手抖复制错export TAOTOKEN_KEYsk-你的实际Key export TAOTOKEN_BASEhttps://taotoken.net/api先做一次最朴素的连通性验证确认 Key 本身没问题再去折腾 k8scurl -sS $TAOTOKEN_BASE/v1/messages \ -H Authorization: Bearer $TAOTOKEN_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, max_tokens: 64, messages: [{role: user, content: ping}] }返回里能看到content字段就说明 Key 和 Base URL 都对。这一步过了后面 Ingress 鉴权验证才有意义这一步就 401先回去检查 Key 有没有复制全、有没有多余空格。3. MetalLB 地址池与 ingress-nginx 可复制配置这一节给完整可复制的配置。先部署 MetalLB再配地址池然后装 ingress-nginx最后把它的 Service 改成 LoadBalancer 让 MetalLB 分配 VIP。先装 MetalLB。用官方清单注意版本号写全kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml如果你的节点拉不到公网镜像就按老办法在能上网的机器上docker pull quay.io/metallb/controller:v0.14.8和quay.io/metallb/speaker:v0.14.8打 tag 推到自己的私有仓库再用sed把清单里的镜像地址替换掉。替换命令sed -i s|quay.io/metallb/controller:v0.14.8|reg.example.com/metallb/controller:v0.14.8|g metallb-native.yaml sed -i s|quay.io/metallb/speaker:v0.14.8|reg.example.com/metallb/speaker:v0.14.8|g metallb-native.yaml kubectl apply -f metallb-native.yaml装完确认 Pod 都起来了kubectl -n metallb-system get podscontroller 一个、speaker 每个节点一个全部 Running 才算好。接着配地址池。地址池必须是你本网段里没被占用的连续 IP比如你的节点是192.168.239.0/24那就挑一段没人用的apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: first-pool namespace: metallb-system spec: addresses: - 192.168.239.240-192.168.239.250 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: example namespace: metallb-system spec: ipAddressPools: - first-pool保存成configmap.yml后kubectl apply -f configmap.yml。注意 namespace 必须是metallb-system和前面部署的命名空间一致写错就宣告不出去。然后装 ingress-nginx。用 controller-v1.11.2 的清单kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/aws/deploy.yaml同样拉不到镜像就替换成私有仓库地址。装完确认kubectl -n ingress-nginx get pods kubectl -n ingress-nginx get svc ingress-nginx-controller默认这个 Service 是 LoadBalancer 类型MetalLB 会自动给它分配一个 VIP。如果 EXTERNAL-IP 还是 pending检查 MetalLB 的 speaker 是否 Running、地址池是否 apply 成功。正常的话你会看到类似192.168.239.241的地址。现在把 TaoToken 的 Key 做成 k8s Secret供 Ingress 鉴权注解引用。这里用 generic 类型存 Keykubectl create secret generic taotoken-key \ --from-literalapi-key$TAOTOKEN_KEY确认创建成功kubectl get secret taotoken-key到这里四层入口MetalLB VIP和七层入口ingress-nginx都就位了Key 也进了集群。下一节用一个真实的 Ingress 资源把后端微服务和鉴权串起来然后用 curl 验证整条链路。4. 验证七层转发与 TaoToken 鉴权生效先起两个后端微服务模拟 v1 和 v2 两个版本方便验证基于路径的七层路由kubectl create deployment nginx-v1 --imagenginx:latest --replicas1 --port80 kubectl create deployment nginx-v2 --imagenginx:latest --replicas1 --port80 kubectl expose deployment nginx-v1 --namesvc-nginx-v1 --port80 --target-port80 --typeClusterIP kubectl expose deployment nginx-v2 --namesvc-nginx-v2 --port80 --target-port80 --typeClusterIP进 Pod 改一下默认页面方便区分kubectl exec -it deploy/nginx-v1 -- bash -c echo this is nginx-v1 /usr/share/nginx/html/index.html kubectl exec -it deploy/nginx-v2 -- bash -c echo this is nginx-v2 /usr/share/nginx/html/index.html现在写 Ingress 资源把/v1路由到 svc-nginx-v1/v2路由到 svc-nginx-v2同时加上 rewrite 和 TaoToken 鉴权注解。这里用auth-url指向 TaoToken 的校验入口把 Key 从 Secret 注入请求头apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: webcluster annotations: nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/auth-url: https://taotoken.net/api/v1/auth/verify nginx.ingress.kubernetes.io/auth-method: GET nginx.ingress.kubernetes.io/auth-snippet: | proxy_set_header Authorization Bearer $http_x_taotoken_key; spec: ingressClassName: nginx rules: - http: paths: - path: /v1 pathType: Prefix backend: service: name: svc-nginx-v1 port: number: 80 - path: /v2 pathType: Prefix backend: service: name: svc-nginx-v2 port: number: 80保存成ingress-route.yml后 applykubectl apply -f ingress-route.yml kubectl get ingress webclusterADDRESS 列出现 MetalLB 分配的 VIP 就说明 Ingress 被 ingress-nginx 接管了。现在验证七层转发VIP$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath{.status.loadBalancer.ingress[0].ip}) curl -s http://$VIP/v1 curl -s http://$VIP/v2分别返回this is nginx-v1和this is nginx-v2说明基于路径的七层路由生效了。rewrite-target 把/v1重写成/所以后端 nginx 能找到默认页面。接着验证 TaoToken 鉴权。带上 Key 请求curl -s http://$VIP/v1 -H X-TaoToken-Key: $TAOTOKEN_KEY不带 Key 请求curl -s -o /dev/null -w %{http_code}\n http://$VIP/v1预期不带 Key 返回 401带上 Key 返回 200 并拿到后端内容。如果 auth-url 那一步走的是 TaoToken 的校验接口那么 401 就证明入口鉴权生效了。你也可以直接用 TaoToken 的模型接口做一次端到端验证确认 Key 通道本身可用curl -sS $TAOTOKEN_BASE/v1/messages \ -H Authorization: Bearer $TAOTOKEN_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-5,max_tokens:32,messages:[{role:user,content:hello}]}返回正常内容说明从 k8s 入口到 TaoToken 通道整条链路都通了。这一步过了七层负载和统一 Key 鉴权就算同时验证完成。5. 本篇常见报错排查EXTERNAL-IP 一直 pending先看kubectl -n metallb-system get podsspeaker 没起来就不会宣告 VIP。再看地址池是否 apply 成功、namespace 是否写对。地址池里的 IP 必须在本网段且未被占用否则分配了也 ping 不通。curl VIP 超时检查 VIP 是否真的被宣告。在集群外的一台同网段机器上arping或ping这个 VIP通不了说明二层宣告没生效。另外确认 ingress-nginx-controller 的 Service 确实是 LoadBalancer 类型不是 NodePort。401 Unauthorized这是鉴权生效的正常表现但如果你带了 Key 还 401检查三件事。一是 Key 有没有多余空格或换行echo -n $TAOTOKEN_KEY | wc -c看长度对不对。二是 Base URL 是不是写成了https://taotoken.net/api多写/v1或漏写都会 401。三是 Secret 里的 Key 和 curl 用的 Key 是不是同一个。local proxy failed / connection refusedingress-nginx 的 auth-url 指向外部地址时如果集群 DNS 解析不了或出网被拦就会报这个。确认节点能解析taotoken.net并且出网 443 通。可以在 ingress-nginx Pod 里curl -I https://taotoken.net/api测一下。reading choices 报错这类错误通常出现在调用模型接口时返回体格式不对。检查请求头Content-Type: application/json有没有带请求体 JSON 有没有语法错误。用curl -v看完整请求和响应比猜快得多。OAuth 相关报错如果你用的是需要 OAuth 的客户端比如某些 CLI 工具报 OAuth 失败通常是 token 过期或 scope 不对。回到控制台重新生成 Key或者检查客户端配置里的 Base URL 和 Model ID 是否和文档一致。Codex 的auth.json、Cline 的 MCP 配置、CC Switch 这类工具都要保证三件套齐全Base URL 填https://taotoken.net/apiKey 填sk-开头那串Model ID 填实际模型名缺一个都会报错。Ingress 规则不生效kubectl describe ingress webcluster看 Events 有没有 Sync 报错。常见的是ingressClassName写错或者后端 Service 名字拼错。pathType 用 Prefix 时路径要以/开头用 ImplementationSpecific 才能配合正则。6. 把统一 Key 通道接进你的日常链路链路跑通之后日常用起来其实就三件事MetalLB 管 VIPingress-nginx 管七层规则TaoToken 管统一 Key。你新增一个微服务只要写一个 Ingress 资源把 host 或 path 指过去鉴权注解复用同一套 Secret不用每个服务单独配密钥。需要长期跑编码或 Agent 任务的话Coding Plan 比按次调用更省心https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档里有各语言 SDK 的 Base URL 填法配客户端前扫一眼https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想先在网页上试模型用模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 管理在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。实测下来最容易翻车的不是 k8s 配置而是 Key 的复制粘贴和 Base URL 多写路径。把这两个点固定成检查项后面加服务就是复制 Ingress 改 path 的事。