Como implementar service discovery dinâmico sem dependência de DNS estático no Kubernetes

1. Fundamentos do Service Discovery no Kubernetes

1.1. Como o DNS do Kubernetes funciona por padrão (CoreDNS e serviços ClusterIP)

No Kubernetes, o service discovery tradicional depende do CoreDNS, que resolve nomes como meu-servico.meu-namespace.svc.cluster.local para endereços ClusterIP. Quando um pod consulta esse DNS, o CoreDNS retorna o IP virtual do serviço, que faz load balancing para os pods saudáveis. Esse mecanismo é simples e funcional para a maioria dos cenários, mas apresenta limitações significativas em ambientes de alta dinamicidade.

1.2. Limitações do DNS estático: latência, cache e mudanças de topologia

O DNS estático no Kubernetes enfrenta três problemas críticos:

  • Latência de propagação: Mudanças em endpoints podem levar segundos para serem refletidas no DNS devido ao TTL (padrão de 30 segundos no CoreDNS).
  • Cache em múltiplos níveis: O cache no kubelet, no nó e no aplicativo pode atrasar ainda mais a descoberta de novos pods.
  • Mudanças de topologia: Em clusters com escalonamento rápido (autoscaling), pods podem ser criados e destruídos em segundos, tornando o DNS obsoleto quase instantaneamente.

1.3. Casos de uso que exigem descoberta dinâmica sem DNS

Ambientes efêmeros (CI/CD), jobs batch de curta duração e sistemas de mensageria em tempo real (como Kafka ou RabbitMQ) exigem que os consumidores descubram produtores em milissegundos, não em segundos. O DNS tradicional simplesmente não atende a esses requisitos.

2. Arquitetura de Service Discovery Baseada em API Server

2.1. Utilizando a API do Kubernetes como fonte de verdade para endpoints

A API do Kubernetes mantém o estado atualizado de todos os recursos, incluindo endpoints de serviços. Em vez de depender do DNS, podemos consultar diretamente a API para obter a lista de pods que correspondem a um determinado selector.

# Exemplo: Consultando endpoints via API REST
curl -X GET https://kubernetes.default.svc/api/v1/namespaces/default/endpoints/meu-servico \
  -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
  --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

2.2. Watch e informers: detectando mudanças em tempo real via list-watch

O mecanismo de watch da API permite que aplicativos recebam notificações imediatas sobre mudanças em recursos. Usando client libraries como client-go, podemos implementar informers que mantêm um cache local sincronizado.

// Exemplo de informer em Go (client-go)
import (
    "k8s.io/client-go/informers"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/cache"
)

func main() {
    clientset, _ := kubernetes.NewForConfig(config)
    factory := informers.NewSharedInformerFactory(clientset, 0)
    informer := factory.Core().V1().Endpoints().Informer()

    informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            endpoint := obj.(*v1.Endpoints)
            fmt.Printf("Novo endpoint detectado: %s\n", endpoint.Name)
        },
        UpdateFunc: func(old, new interface{}) {
            fmt.Println("Endpoint atualizado")
        },
    })

    stop := make(chan struct{})
    informer.Run(stop)
}

2.3. Vantagens sobre DNS: consistência imediata e redução de latência

Ao usar a API diretamente, as mudanças são propagadas em milissegundos (vs. segundos no DNS). Não há cache intermediário e a consistência é garantida pelo etcd, que é o banco de dados subjacente do Kubernetes.

3. Implementando com Custom Resources e Operadores

3.1. Criando um CRD para registrar serviços dinâmicos

Podemos estender a API do Kubernetes com Custom Resource Definitions (CRDs) para representar serviços dinâmicos que não são gerenciados pelo controlador padrão.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: servicosdinamicos.exemplo.io
spec:
  group: exemplo.io
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                endereco:
                  type: string
                porta:
                  type: integer
  scope: Namespaced
  names:
    plural: servicosdinamicos
    singular: servicodinamico
    kind: ServicoDinamico

3.2. Desenvolvendo um operador que atualiza endpoints automaticamente

O operador observa mudanças nos CRDs e atualiza os endpoints reais do Kubernetes.

# Exemplo de reconciliação em Python com kopf
import kopf
import kubernetes

@kopf.on.create('exemplo.io', 'v1', 'servicosdinamicos')
def criar_servico(spec, name, namespace, **kwargs):
    api = kubernetes.client.CoreV1Api()

    # Criar um endpoint apontando para o serviço dinâmico
    endpoint = {
        "kind": "Endpoints",
        "apiVersion": "v1",
        "metadata": {"name": name, "namespace": namespace},
        "subsets": [{
            "addresses": [{"ip": spec['endereco']}],
            "ports": [{"port": spec['porta']}]
        }]
    }

    api.create_namespaced_endpoints(namespace, endpoint)
    return {'status': 'endpoint_criado'}

3.3. Exemplo prático: operador que sincroniza serviços entre namespaces

Um operador pode replicar endpoints de um namespace para outro, permitindo descoberta cross-namespace sem DNS.

4. Service Discovery via Etcd e Consul no Kubernetes

4.1. Integrando o etcd como backend de descoberta para pods efêmeros

O etcd pode ser usado como um registro de serviço leve, onde cada pod registra seu endpoint ao iniciar e o remove ao terminar.

# Exemplo: Registro no etcd via shell
etcdctl put /servicos/pod-abc123 '{"ip":"10.0.0.5","porta":8080}'

# Descoberta: listando todos os serviços
etcdctl get /servicos/ --prefix --keys-only

4.2. Usando Consul com sidecar para registro e descoberta sem DNS

O Consul oferece um mecanismo mais robusto com health checks e catálogo centralizado.

# Configuração do sidecar Consul no pod
apiVersion: v1
kind: Pod
metadata:
  annotations:
    "consul.hashicorp.com/connect-inject": "true"
spec:
  containers:
    - name: meu-app
      image: meu-app:latest
      ports:
        - containerPort: 8080

4.3. Comparação de desempenho: etcd vs. Consul vs. API Server nativo

Mecanismo Latência típica Complexidade Consistência
API Server 1-5ms Baixa Forte
etcd 2-10ms Média Forte
Consul 5-20ms Alta Eventual

5. Padrão de Service Mesh para Descoberta Dinâmica

5.1. Como o Istio e o Linkerd implementam descoberta sem DNS (via xDS e Envoy)

O Istio usa o protocolo xDS para enviar configurações de descoberta diretamente aos proxies Envoy, ignorando completamente o DNS. Cada proxy mantém um cache atualizado em tempo real.

5.2. Configuração de mTLS e roteamento dinâmico baseado em labels

# Exemplo: VirtualService do Istio para roteamento dinâmico
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: roteamento-dinamico
spec:
  hosts:
    - meu-servico
  http:
    - match:
        - headers:
            versao:
              exact: v2
      route:
        - destination:
            host: meu-servico
            subset: v2
    - route:
        - destination:
            host: meu-servico
            subset: v1

5.3. Exemplo de código: configurando um VirtualService para descoberta em tempo real

O VirtualService é atualizado automaticamente quando novos pods com labels específicas são detectados.

6. Abordagem Híbrida: Combinando DNS com Mecanismos Dinâmicos

6.1. Cache inteligente de DNS com TTL dinâmico baseado em eventos

Podemos implementar um DNS resolver customizado que ajusta o TTL baseado na frequência de mudanças detectadas via watch.

6.2. Usando Headless Services e StatefulSets para descoberta previsível

Headless services (ClusterIP=None) retornam todos os IPs dos pods diretamente, permitindo que o cliente faça o load balancing.

apiVersion: v1
kind: Service
metadata:
  name: meu-statefulset
spec:
  clusterIP: None
  selector:
    app: meu-app
  ports:
    - port: 8080

6.3. Técnica de fallback: API Server como primário, DNS como fallback

Implementar uma lógica que primeiro tenta a API e, em caso de falha, recorre ao DNS.

function descobrirEndpoints() {
  try {
    return await apiServerWatch();
  } catch (error) {
    return await dnsLookup();
  }
}

7. Monitoramento e Troubleshooting da Descoberta Dinâmica

7.1. Métricas-chave: latência de registro, taxa de atualização e consistência

Monitore:
- service_discovery_registration_latency_ms
- service_discovery_update_rate
- service_discovery_consistency_errors_total

7.2. Ferramentas de debug: kubectl watch, logs de operadores e tracing distribuído

# Monitorando mudanças em endpoints em tempo real
kubectl get endpoints -w

# Logs do operador
kubectl logs -l app=meu-operador --tail=100 -f

7.3. Cenários comuns de falha e estratégias de resiliência

  • Falha na API: Implementar retry com backoff exponencial.
  • Inconsistência de cache: Usar watch com reconexão automática.
  • Sobrecarga de eventos: Rate limiting no processamento de eventos.

Referências