Buzeli
buzeliSoluções Digitais
Kubernetes

OCI CCM v1.34: as annotations que não existem e o Token Collision que paralisa o LB

Publicado em 21 de abril de 2026

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

Contexto

Durante a migração de uma aplicação Next.js para OKE (Oracle Kubernetes Engine), usamos um Service do tipo LoadBalancer para que o Cloud Controller Manager (CCM) provisionasse e gerenciasse o OCI Load Balancer automaticamente. A versão do CCM era a v1.34, pareada com o control plane OKE.

O que deveria ser simples — declarar annotations no Service e o CCM obedecer — virou uma sequência de comportamentos silenciosos, erros 409, e LBs órfãos acumulando na conta. Este post documenta o que de fato funciona, o que é ignorado, e a armadilha mais perigosa: o Token Collision.

Annotations que não existem no OCI CCM v1.34

A documentação oficial do OCI CCM lista um conjunto de annotations suportadas. Mas ao subir workloads reais, algumas annotations encontradas em fóruns, exemplos de comunidade ou versões mais antigas simplesmente não funcionam — e o CCM não avisa. Ele ignora silenciosamente e segue em frente.

1. Reutilizar um LB existente

Tentativa: apontar o CCM para um Load Balancer que já existia na conta, criado manualmente antes do cluster.

Copiar
# FALHOU — annotation inexistente no OCI CCM v1.34
service.beta.kubernetes.io/oci-load-balancer-id: "<ocid-do-lb-manual>"

Resultado: o CCM ignorou completamente a annotation e criou um novo LB próprio com nome UUID gerado automaticamente. O LB manual ficou ativo e cobrando, sem tráfego.

2. Reserved IP (IP fixo para o LB)

Tentativa: fixar o IP público do Load Balancer usando um Reserved IP da OCI para evitar mudança de IP se o Service fosse recriado.

Copiar
# FALHOU — annotation ignorada, CCM cria IP próprio
service.beta.kubernetes.io/oci-load-balancer-reserved-ip-id: "<ocid-do-publicip>"

# FALHOU também
oci.oraclecloud.com/load-balancer-ip: "192.0.2.4"

Resultado: o CCM ignorou ambas e provisionou o LB com um IP efêmero (mas estável enquanto o Service existir). O Reserved IP ficou em estado AVAILABLE, gerando custo sem uso (~$1.80/mês).

Na prática, o IP do LB gerenciado pelo CCM não muda enquanto o Service Kubernetes não for deletado. Para DNS de produção, esse IP é suficientemente estável — não há necessidade de Reserved IP.

3. Security Rule Management via NSG

Tentativa: usar o modo NSG para que o CCM gerenciasse as regras de tráfego via Network Security Groups em vez de Security Lists.

Copiar
# FALHOU — requer policies IAM que o OKE básico não provisiona
service.beta.kubernetes.io/oci-load-balancer-security-rule-management-mode: "NSG"

# FALHOU também — 404 sem permissão VirtualNetwork Manage
service.beta.kubernetes.io/oci-network-security-groups: "<ocid-do-nsg>"

Resultado: erros 404 na API VirtualNetwork. O service account do CCM no OKE básico não tem as policies IAM necessárias para criar ou modificar NSGs. A solução funcional é usar `security-list-management-mode: None` e gerenciar as regras de rede manualmente.

Annotations confirmadas funcionando no OCI CCM v1.34

Após descarte das annotations problemáticas, este é o conjunto testado em produção que funciona de forma consistente:

Copiar
annotations:
  # Subnet onde o LB será criado
  service.beta.kubernetes.io/oci-load-balancer-subnet1: "<ocid-da-subnet-publica>"

  # Shape flexible (recomendado vs shape fixo)
  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 não toca nas Security Lists (gerenciamento manual)
  service.beta.kubernetes.io/oci-load-balancer-security-list-management-mode: "None"

  # Health check via kube-proxy healthz (porta 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"

  # Backend protocol
  service.beta.kubernetes.io/oci-load-balancer-backend-protocol: "HTTP"

  # HTTPS (quando TLS secret estiver criado no cluster)
  service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
  service.beta.kubernetes.io/oci-load-balancer-tls-secret: "myapp/myapp-tls"

Token Collision: o problema mais perigoso

O Token Collision é a armadilha mais silenciosa do OCI CCM. Ele se manifesta como um HTTP 409 na API OCI — e quando ocorre, o CCM para de reconciliar o LB sem emitir nenhum alerta claro no cluster.

Como funciona o token de idempotência

O CCM OCI gera um token de idempotência para cada operação de criação de Load Balancer. O token é derivado de:

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

O Service UID é fixo enquanto o Service existe. O problema: qualquer alteração no spec do Service (adicionar ou remover uma annotation, mudar uma porta, alterar o shape) não muda o UID — mas muda o conteúdo da requisição enviada à API OCI. A OCI retorna 409 porque o token já foi usado com um payload diferente.

Sintoma

O CCM entra em loop de reconciliação sem conseguir atualizar o LB. Os eventos do Service mostram erros repetidos, mas nenhum log de alto nível no cluster indica o que houve. O LB na OCI fica no estado anterior, sem refletir as mudanças do Service.

A única saída: deletar e recriar o Service

Não há como resolver o Token Collision sem recriar o Service. Um novo Service gera um novo UID → novo token → operação limpa na OCI. O procedimento correto é:

Copiar
# 1. Remover o finalizer para desbloquear a deleção
kubectl patch service myapp-svc -n myapp -p   '{"metadata":{"finalizers":[]}}' --type=merge

# 2. Deletar o Service (CCM deleta o LB automaticamente)
kubectl delete service myapp-svc -n myapp

# 3. Recriar o Service com as annotations corretas
kubectl apply -f service.yaml

# 4. Reativar o WAF no novo LB (se houver WAF policy associada)
# O OCID do LB muda — a WAF instance precisa ser recriada
CRÍTICO: nunca altere o spec do Service em produção sem planejar o recadastro do WAF. O CCM deleta o LB antigo ao deletar o Service — e a WAF instance associada perde a referência.

Regra de ouro para evitar Token Collision

Defina o spec final do Service antes do primeiro apply. Após criado com um UID, qualquer mudança estrutural (ports, annotations de shape, subnet) exige o ciclo completo de delete + recreate. Não tente editar o Service em produção com `kubectl edit` ou `kubectl patch` para mudar annotations funcionais — isso desencadeia o 409.

Portas obrigatórias que a documentação não destaca

Dois requisitos de rede são críticos e aparecem apenas como notas secundárias na documentação oficial, mas causam falhas silenciosas quando ausentes.

Porta 12250 — registros de nodes

Os worker nodes se comunicam com o endpoint de gerenciamento do OKE pela porta TCP 12250. Sem essa regra de ingress no NSG do endpoint, os nodes nunca concluem o registro.

Copiar
# NSG do endpoint OKE
Direção: INGRESS
Protocolo: TCP
Origem: NSG dos worker nodes (ou CIDR da subnet de nodes)
Porta destino: 12250
Descrição: Workers -> OKE management endpoint

Sintoma sem a porta 12250: todos os nodes ficam em estado `UnknownNodeError` com a mensagem "has not been seen for more than 20 minutes" — mesmo recém-provisionados pelo node pool.

Porta 10256 — health check do Load Balancer

O CCM configura o health check do LB apontando para a porta 10256 dos nodes, que é o endpoint `/healthz` do kube-proxy. Sem a regra liberando essa porta da subnet do LB para os nodes, todos os backends ficam marcados como unhealthy e o tráfego não é roteado.

Copiar
# NSG dos worker nodes (ou Security List)
Direção: INGRESS
Protocolo: TCP
Origem: CIDR da subnet do LB (ex: 10.1.0.0/24)
Porta destino: 10256
Descrição: OCI LB health check -> kube-proxy healthz

Nota: o CCM usa 10256 como health check padrão independente das annotations de health-check-path configuradas — a porta é fixa no comportamento do controlador.

HTTPS gerenciado pelo CCM: declare no Service, não no LB

Uma armadilha frequente é adicionar listeners HTTPS diretamente no OCI Load Balancer via CLI ou console. O CCM reconcilia o LB periodicamente (a cada mudança de node ou pod) e deleta tudo que não está declarado no Service Kubernetes.

A forma correta de ter HTTPS gerenciado pelo CCM:

Copiar
# 1. Criar o TLS secret no cluster
kubectl create secret tls myapp-tls -n myapp \
  --cert=myapp.crt \
  --key=myapp.key

# 2. Declarar a porta 443 no spec do Service
spec:
  ports:
    - name: http
      port: 80
      targetPort: 3000
      protocol: TCP
    - name: https
      port: 443
      targetPort: 3000
      protocol: TCP

# 3. Adicionar as annotations de TLS
annotations:
  service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
  service.beta.kubernetes.io/oci-load-balancer-tls-secret: "myapp/myapp-tls"

Com essa configuração, o CCM cria dois listeners e dois backend sets. Quando nodes mudam, o CCM reconcilia e recria os listeners automaticamente — sem intervenção manual.

CCM auto-update de backends: confirmado em ~2 minutos

Após resolver todos os problemas de annotations e portas, validamos o comportamento de auto-update do CCM: removemos manualmente um node via console OCI. O node pool provisionou um substituto, e o CCM detectou o novo node e atualizou os backends do LB em aproximadamente 2 minutos, sem intervenção.

Copiar
# Eventos do Service durante a troca de node
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

O downtime real não foi causado pelo CCM — foi dominado pelo tempo de pull da imagem de container (2.1 GB levando 1m34s por node novo). O CCM faz sua parte; o gargalo é o tamanho da imagem.

Para reduzir downtime em trocas de node: use Next.js standalone output, multi-stage build e .dockerignore adequado. Uma imagem de 2.1 GB pode cair para ~500 MB — e o tempo de pull acompanha.

Resumo das armadilhas

Para quem está configurando OKE + CCM v1.34 pela primeira vez, as principais armadilhas em ordem de impacto:

1. Token Collision 409 — nunca edite o spec do Service após criado. Qualquer mudança estrutural exige delete + recreate com remoção de finalizer.

2. Porta 12250 ausente — nodes ficam em UnknownNodeError para sempre. Sem essa porta no NSG do endpoint, o cluster não funciona.

3. Porta 10256 ausente — todos os backends do LB ficam unhealthy. Sem essa porta, tráfego não chega às aplicações.

4. Annotations que não existem — oci-load-balancer-id, reserved-ip-id e security-rule-management-mode: NSG são ignoradas silenciosamente. Não use.

5. HTTPS fora do Service — listeners adicionados diretamente no LB OCI são deletados pelo CCM na próxima reconciliação.