Buzeli
buzeliSoluções Digitais
Kubernetes

OCI CCM v1.34:不存在的 annotation 和让 LB 瘫痪的 Token Collision

发布于 2026年4月21日

OCI CCM v1.34: annotations que não existem e Token Collision 409

背景

在将一个 Next.js 应用迁移到 OKE(Oracle Kubernetes Engine)时,我们使用 LoadBalancer 类型的 Service,让 Cloud Controller Manager(CCM)自动创建和管理 OCI Load Balancer。CCM 版本为 v1.34,与 OKE 控制平面配套使用。

原本应该很简单的事情——在 Service 上声明 annotation 让 CCM 执行——变成了一系列静默失败、409 错误和孤立 LB 不断累积的过程。本文记录了真正可用的内容、被忽略的内容,以及最危险的陷阱:Token Collision。

OCI CCM v1.34 中不存在的 annotation

官方 OCI CCM 文档列出了一组支持的 annotation。但在运行真实工作负载时,一些在论坛、社区示例或旧版本中找到的 annotation 根本不起作用——CCM 不会发出任何警告,而是静默忽略并继续运行。

1. 复用已有 LB

尝试:将 CCM 指向一个账户中已存在的、在集群创建前手动创建的 Load Balancer。

复制
# 失败 — OCI CCM v1.34 中不存在此 annotation
service.beta.kubernetes.io/oci-load-balancer-id: "<lb-ocid>"

结果:CCM 完全忽略了该 annotation,创建了一个使用自动生成 UUID 名称的新 LB。手动 LB 继续运行并计费,没有任何流量。

2. Reserved IP(固定 IP)

尝试:使用 OCI Reserved IP 固定 Load Balancer 的公网 IP,以避免 Service 被重建时 IP 发生变化。

复制
# 失败 — annotation 被忽略,CCM 创建自己的 IP
service.beta.kubernetes.io/oci-load-balancer-reserved-ip-id: "<publicip-ocid>"

# 同样失败
oci.oraclecloud.com/load-balancer-ip: "192.0.2.4"

结果:CCM 忽略了两者,使用临时 IP 创建了 LB(但只要 Service 存在,IP 就保持稳定)。Reserved IP 保持 AVAILABLE 状态,产生费用但没有使用(约 1.80 美元/月)。

实际上,只要 Kubernetes Service 不被删除,CCM 管理的 LB IP 就不会改变。对于生产 DNS,这个 IP 足够稳定——不需要 Reserved IP。

3. 通过 NSG 进行安全规则管理

尝试:使用 NSG 模式让 CCM 通过 Network Security Group 而非 Security List 管理流量规则。

复制
# 失败 — 需要基本 OKE 未配置的 IAM 策略
service.beta.kubernetes.io/oci-load-balancer-security-rule-management-mode: "NSG"

# 同样失败 — 没有 VirtualNetwork Manage 权限,返回 404
service.beta.kubernetes.io/oci-network-security-groups: "<nsg-ocid>"

结果:VirtualNetwork API 返回 404 错误。基本 OKE 中的 CCM 服务账号没有创建或修改 NSG 所需的 IAM 策略。可行的解决方案是使用 `security-list-management-mode: None` 并手动管理网络规则。

OCI CCM v1.34 中经过确认可用的 annotation

排除有问题的 annotation 后,以下是经过生产验证的一致可用配置:

复制
annotations:
  # LB 将创建在哪个子网
  service.beta.kubernetes.io/oci-load-balancer-subnet1: "<公网子网-ocid>"

  # 弹性 shape(推荐,优于固定 shape)
  service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
  service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
  service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"

  # CCM 不修改 Security List(手动管理)
  service.beta.kubernetes.io/oci-load-balancer-security-list-management-mode: "None"

  # 健康检查(通过 kube-proxy healthz,端口 10256)
  service.beta.kubernetes.io/oci-load-balancer-health-check-protocol: "HTTP"
  service.beta.kubernetes.io/oci-load-balancer-health-check-path: "/healthz"
  service.beta.kubernetes.io/oci-load-balancer-health-check-retries: "3"
  service.beta.kubernetes.io/oci-load-balancer-health-check-interval: "10000"
  service.beta.kubernetes.io/oci-load-balancer-health-check-timeout: "5000"

  # 后端协议
  service.beta.kubernetes.io/oci-load-balancer-backend-protocol: "HTTP"

  # HTTPS(当集群中已创建 TLS secret 时)
  service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
  service.beta.kubernetes.io/oci-load-balancer-tls-secret: "myapp/myapp-tls"

Token Collision:最危险的问题

Token Collision 是 OCI CCM 中最隐蔽的陷阱。它表现为 OCI API 返回 HTTP 409——当它发生时,CCM 停止协调 LB,却不会在集群中发出任何明确的告警。

幂等性 token 的工作原理

OCI CCM 为每个 Load Balancer 创建操作生成一个幂等性 token。该 token 由以下内容派生:

复制
token = hash("cluster~createLoadBalancer~{serviceUID}")

Service UID 在 Service 存在期间是固定的。问题在于:对 Service spec 的任何更改(添加或删除 annotation、修改端口、调整 shape)不会改变 UID——但会改变发送给 OCI API 的请求内容。OCI 返回 409,因为该 token 已经以不同的 payload 使用过了。

症状

CCM 进入协调循环,无法更新 LB。Service 事件显示重复错误,但集群中没有高级别日志指示发生了什么。OCI 中的 LB 保持之前的状态,不反映 Service 的变更。

唯一出路:删除并重建 Service

没有办法在不重建 Service 的情况下解决 Token Collision。新的 Service 生成新的 UID → 新 token → 对 OCI 的干净操作。正确的步骤是:

复制
# 1. 移除 finalizer 以解除删除阻塞
kubectl patch service myapp-svc -n myapp -p   '{"metadata":{"finalizers":[]}}' --type=merge

# 2. 删除 Service(CCM 自动删除 LB)
kubectl delete service myapp-svc -n myapp

# 3. 使用正确的 annotation 重建 Service
kubectl apply -f service.yaml

# 4. 在新 LB 上重新激活 WAF(如果有关联的 WAF policy)
# LB 的 OCID 会变更——WAF instance 需要重建
关键:在生产环境删除 Service 之前,必须规划好 WAF 的重新注册。CCM 删除 Service 时会删除旧 LB——关联的 WAF instance 会失去引用。

避免 Token Collision 的黄金法则

在第一次 apply 之前确定好 Service 的最终 spec。一旦以某个 UID 创建,任何结构性更改(端口、shape annotation、子网)都需要完整的 delete + recreate 流程。不要在生产环境中用 `kubectl edit` 或 `kubectl patch` 修改功能性 annotation——这会触发 409。

文档中未重点说明的必要端口

两个网络要求是关键的,在官方文档中只作为次要说明出现,但如果缺失会导致静默失败。

端口 12250 — 节点注册

工作节点通过 TCP 端口 12250 与 OKE 管理端点通信。如果端点的 NSG 中没有此入站规则,节点永远无法完成注册。

复制
# OKE 端点 NSG
方向:INGRESS
协议:TCP
来源:工作节点 NSG(或节点子网 CIDR)
目标端口:12250
描述:Worker 节点 -> OKE 管理端点

没有端口 12250 的症状:所有节点保持 `UnknownNodeError` 状态,显示消息"has not been seen for more than 20 minutes"——即使是节点池刚刚创建的节点也是如此。

端口 10256 — Load Balancer 健康检查

CCM 将 LB 健康检查配置为指向节点上的端口 10256,即 kube-proxy 的 `/healthz` 端点。如果没有允许从 LB 子网到节点该端口的规则,所有后端都会被标记为不健康,流量无法被路由。

复制
# 工作节点 NSG(或 Security List)
方向:INGRESS
协议:TCP
来源:LB 子网 CIDR(如 10.1.0.0/24)
目标端口:10256
描述:OCI LB 健康检查 -> kube-proxy healthz

注意:CCM 使用 10256 作为默认健康检查端口,无论配置了什么 health-check-path annotation——该端口在控制器行为中是硬编码的。

由 CCM 管理的 HTTPS:在 Service 中声明,而非在 LB 中

一个常见陷阱是直接通过 CLI 或控制台将 HTTPS 监听器添加到 OCI Load Balancer。CCM 会定期协调 LB(在每次节点或 Pod 变更时),并删除所有未在 Kubernetes Service 中声明的内容。

让 CCM 管理 HTTPS 的正确方式:

复制
# 1. 在集群中创建 TLS secret
kubectl create secret tls myapp-tls -n myapp \
  --cert=myapp.crt \
  --key=myapp.key

# 2. 在 Service spec 中声明端口 443
spec:
  ports:
    - name: http
      port: 80
      targetPort: 3000
      protocol: TCP
    - name: https
      port: 443
      targetPort: 3000
      protocol: TCP

# 3. 添加 TLS annotation
annotations:
  service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
  service.beta.kubernetes.io/oci-load-balancer-tls-secret: "myapp/myapp-tls"

使用此配置,CCM 会创建两个监听器和两个后端集。当节点变化时,CCM 会自动协调并重建监听器——无需手动干预。

CCM 后端自动更新:约 2 分钟内确认

解决所有 annotation 和端口问题后,我们验证了 CCM 的自动更新行为:我们通过 OCI 控制台手动移除了一个节点。节点池创建了一个替代节点,CCM 在大约 2 分钟内检测到新节点并更新了 LB 后端,无需干预。

复制
# 节点替换期间的 Service 事件
Warning  UnAvailableLoadBalancer  11m   service-controller  There are no available provisioned nodes for LoadBalancer
Normal   UpdatedLoadBalancer      9m21s (x4 over 13m)  service-controller  Updated load balancer with new hosts

真实的停机时间不是由 CCM 造成的——而是由容器镜像拉取时间决定的(2.1 GB 在每个新节点上需要 1 分 34 秒)。CCM 做好了它的工作;瓶颈在于镜像大小。

为减少节点替换时的停机时间:使用 Next.js standalone 输出、多阶段构建和适当的 .dockerignore。2.1 GB 的镜像可以降至约 500 MB——拉取时间随之降低。

陷阱总结

对于第一次配置 OKE + CCM v1.34 的人,按影响程度排列的主要陷阱:

1. Token Collision 409 — 创建后永远不要编辑 Service spec。任何结构性更改都需要移除 finalizer 后 delete + recreate。

2. 缺少端口 12250 — 节点永远处于 UnknownNodeError。没有这个端口,集群无法正常工作。

3. 缺少端口 10256 — 所有 LB 后端都不健康。没有这个端口,流量永远无法到达应用程序。

4. 不存在的 annotation — oci-load-balancer-id、reserved-ip-id 和 security-rule-management-mode: NSG 被静默忽略。不要使用。

5. 在 Service 外配置 HTTPS — 直接添加到 OCI LB 的监听器会在 CCM 下次协调时被删除。