39 min

Rota interna vs externa no Kubernetes: .svc.cluster.local ou Ingress?

Avatar image
Gregory Serrao
A diferença entre chamar um serviço por .svc.cluster.local e expor pelo Ingress — com diagramas, comandos de verificação e a regra que resolve.

Tem um erro que aparece em quase todo cluster que eu pego pra revisar. Ele não derruba nada, não aparece em log de erro e passa despercebido por meses — até alguém olhar a fatura de rede ou o gráfico de latência e perguntar: "por que essa chamada demora 40 ms se os dois serviços rodam na mesma máquina?"

O erro é este: um pod chamando outro pod pela URL pública da aplicação.

Algo assim, dentro do serviço de checkout:

// dentro do cluster, chamando o serviço vizinho
const r = await fetch('https://api.loja.com/pagamentos', {
  method: 'POST',
  body: JSON.stringify(pedido)
})

Funciona. É justamente por isso que sobrevive tanto tempo.

A resposta curta

Se quem chama roda dentro do cluster, use o nome do Service (pagamentos.default.svc.cluster.local). O tráfego fica na rede interna, resolvido pelo CoreDNS e balanceado pelo kube-proxy.

Se quem chama vem de fora — navegador, app, parceiro — use o Ingress (https://api.loja.com). Ele é a porta de entrada pública, onde ficam TLS, WAF e rate limit.

Os dois endereços apontam para os mesmos pods. A diferença é o caminho que o pacote percorre até chegar lá — e o preço desse caminho.

O caminho absurdo

Olha o que aquele fetch acima realmente faz:

Diagrama: pod chamando outro pod pela URL pública — o tráfego sai do cluster, vai até o Ingress na internet e volta pra dentro

O pacote sai do pod, atravessa a fronteira do cluster, sobe até o load balancer na internet, resolve DNS público, negocia um handshake TLS novo, e volta pra dentro — pra bater num pod que estava na mesma rede virtual, às vezes no mesmo nó físico.

Você pagou três vezes por uma conversa que era doméstica:

  • Latência — o que seriam sub-milissegundos vira dezenas de milissegundos, porque o pacote faz um passeio pela internet.
  • Custo de saída — provedores de nuvem cobram tráfego que sai da rede (egress). Essa chamada sai e entra de novo, e você paga a saída.
  • Fragilidade — uma instabilidade de DNS público ou do load balancer derruba uma comunicação que nem precisava sair de casa.

E tem um caso pior, que é quando nem funciona: se o load balancer não fizer hairpin NAT, o pacote sai e não consegue voltar pra origem. Aí você tem um bug que só aparece em produção, nunca no seu Docker Compose local.

Por que não chamar o pod direto, então?

Porque pod não tem endereço fixo. Essa é a raiz do problema todo.

Um pod é descartável por definição. Ele morre num deploy, num rolling update, num nó que caiu, num autoscaler que reduziu réplicas. E quando volta, volta com outro IP. Se você fixar 10.1.9.2 no código ou numa variável de ambiente, esse endereço vira uma bomba-relógio: funciona até o primeiro restart.

O Kubernetes resolve isso com o Service — e é ele o endereço interno que você procura.

Rota interna: o Service

O Service é um endereço estável na frente de um conjunto de pods que muda o tempo todo.

Diagrama da rota interna no Kubernetes: o pod checkout chama o Service pagamentos, que distribui entre as três réplicas dentro do cluster

apiVersion: v1
kind: Service
metadata:
  name: pagamentos
  namespace: default
spec:
  selector:
    app: pagamentos      # quem entra no grupo
  ports:
    - port: 8080         # porta do Service
      targetPort: 3000   # porta do container

Criando esse Service, você ganha três coisas de uma vez:

  1. Um nome de DNS estávelpagamentos, que o CoreDNS resolve dentro do cluster.
  2. Um IP virtual estável (o ClusterIP) — que não muda enquanto o Service existir, mesmo que todos os pods sejam recriados.
  3. Balanceamento automático — o tráfego é distribuído entre os pods que estão saudáveis naquele instante.

O selector é a parte que costuma confundir: o Service não guarda uma lista de IPs escrita à mão. Ele diz "todo pod com o label app: pagamentos", e o Kubernetes mantém essa lista atualizada sozinho, em tempo real. Pod novo entrou, entrou na lista. Pod morreu, saiu.

O fetch correto, então, é este:

// mesma chamada, agora pela rota interna
const r = await fetch('http://pagamentos.default.svc.cluster.local:8080/cobrancas', {
  method: 'POST',
  body: JSON.stringify(pedido)
})

Repara em dois detalhes: é http, não https (falo disso mais pra frente), e é a porta do Service (8080), não a do container.

Anatomia do .svc.cluster.local

Aquele nome comprido não é decoração. Cada pedaço significa uma coisa:

Pedaço O que é
pagamentos O nome do Service
default O namespace onde ele vive
svc O tipo do registro — é um Service (existe também pod)
cluster.local O domínio do cluster (configurável, mas esse é o padrão)

Ou seja: <service>.<namespace>.svc.cluster.local. Uma vez que você lê assim, o nome deixa de ser mágico e vira endereço.

Por que o nome curto também funciona

Se pagamentos.default.svc.cluster.local é o nome completo, por que http://pagamentos funciona igual?

Por causa do /etc/resolv.conf que o Kubernetes injeta em todo pod:

$ kubectl exec -it checkout-7d9f -- cat /etc/resolv.conf

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

A linha search é uma lista de sufixos que o resolver tenta colar no nome antes de desistir. Você pede pagamentos, ele tenta pagamentos.default.svc.cluster.local primeiro — e acerta de primeira, porque é o serviço vizinho no mesmo namespace.

Mas a linha options ndots:5 tem um efeito colateral que quase ninguém conhece, e que é um problema de performance de verdade.

ndots:5 significa: "se o nome tiver menos de 5 pontos, tente a lista de search antes de tratar como nome absoluto". Agora conta os pontos de api.loja.com — são dois. Menos de cinco. Então, antes de consultar o DNS público, o pod tenta:

api.loja.com.default.svc.cluster.local   → NXDOMAIN
api.loja.com.svc.cluster.local           → NXDOMAIN
api.loja.com.cluster.local               → NXDOMAIN
api.loja.com                             → finalmente resolve

Três consultas desperdiçadas em cada chamada — e como as bibliotecas costumam consultar IPv4 e IPv6, na prática são seis. Isso é gente descobrindo que "o DNS do cluster está lento" quando o problema é chamar domínio externo de dentro do pod sem pensar.

Existe um truque pra isso: um ponto final no fim do domínio (api.loja.com.) marca o nome como absoluto e pula a lista de search inteira. Vale quando você precisa mesmo falar com uma API externa de dentro do cluster.

O que acontece depois que o DNS resolve

O nome virou um ClusterIP, tipo 10.96.7.20. E aqui vem a parte que mais surpreende: esse IP não pertence a nenhuma máquina. Não existe interface de rede com esse endereço. Ele é virtual.

Quem faz ele funcionar é o kube-proxy, um componente que roda em todo nó e programa regras de rede (iptables, IPVS ou nftables, dependendo do modo). Quando um pacote sai do seu pod endereçado a 10.96.7.20, essas regras reescrevem o destino para o IP de um dos pods reais, na hora.

A lista de pods elegíveis vive num objeto chamado EndpointSlice, mantido pelo próprio Kubernetes. É por isso que um pod que acabou de morrer para de receber tráfego sem que ninguém precise mexer em nada.

Resumindo o caminho interno: nome → CoreDNS → ClusterIP → kube-proxy → pod. Nenhum passo sai do cluster.

Rota externa: o Ingress

Agora inverte o problema. O usuário está no navegador, do lado de fora, e precisa chegar na sua aplicação.

Diagrama da rota externa no Kubernetes: o usuário na internet entra pelo Ingress, que encaminha para o Service e daí para os pods

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: loja
spec:
  ingressClassName: nginx
  tls:
    - hosts: [api.loja.com]
      secretName: loja-tls
  rules:
    - host: api.loja.com
      http:
        paths:
          - path: /pagamentos
            pathType: Prefix
            backend:
              service:
                name: pagamentos
                port:
                  number: 8080

O Ingress trabalha na camada 7 — ele entende HTTP. É isso que permite rotear por host e por caminho: api.loja.com/pagamentos vai pra um Service, api.loja.com/pedidos vai pra outro, tudo atrás do mesmo IP público e do mesmo certificado.

Repara no final do YAML: o Ingress aponta pra um Service. Ele não fala com pod diretamente. Ou seja, a rota externa termina virando rota interna — o Ingress é só o pedaço a mais no começo do caminho, pra quem vem de fora.

Duas coisas que confundem muita gente:

O objeto Ingress sozinho não faz nada. Ele é só configuração. Você precisa de um Ingress Controller rodando no cluster (NGINX, Traefik, HAProxy, Istio) pra ler esse objeto e efetivamente atender as requisições. Criar o YAML sem ter controller instalado é o motivo número um de "meu Ingress não funciona".

O Ingress é onde a segurança de borda mora. TLS, autenticação, rate limit, WAF, bloqueio por IP — tudo isso fica ali, num lugar só. É o mesmo raciocínio de camadas que eu uso no post sobre proteger APIs Node.js: concentre a defesa na fronteira, não espalhe pela aplicação inteira.

Service, Ingress e LoadBalancer: quem faz o quê

Service (ClusterIP) Ingress Service (LoadBalancer)
Camada 4 (TCP/UDP) 7 (HTTP/HTTPS) 4 (TCP/UDP)
Quem alcança Só quem está dentro do cluster Qualquer um na internet Qualquer um na internet
Roteia por host/path Não Sim Não
Termina TLS Não Sim Não (repassa)
Custo na nuvem Zero Um IP público para muitos serviços Um IP público por serviço
Use quando Comunicação entre serviços Expor HTTP ao mundo Expor protocolo que não é HTTP

Essa última linha é a que economiza dinheiro: exportar dez APIs com dez Services do tipo LoadBalancer significa dez IPs públicos na fatura. Com Ingress, é um só.

E o Gateway API? É o sucessor oficial do Ingress, já estável e recomendado pelo projeto Kubernetes para casos novos. O modelo mental é o mesmo — uma porta de entrada L7 na frente dos Services — com uma separação melhor entre quem cuida da infraestrutura e quem cuida da rota. Tudo que está neste artigo sobre quando usar rota externa continua valendo.

A regra de ouro

Comparativo: rota interna usa o nome do Service com .svc.cluster.local; rota externa usa o domínio público pelo Ingress

Uma pergunta resolve qualquer dúvida:

Quem está chamando roda dentro do cluster? Se sim, use o nome do Service. Se não, use o Ingress.

E o corolário que pega quase todo mundo: o domínio público nunca deve ser o caminho pra falar com o pod vizinho. Se você digitou https:// numa chamada entre dois serviços seus dentro do mesmo cluster, provavelmente escolheu o caminho errado.

Como conferir no seu cluster

Três comandos que mostram a rota interna funcionando de ponta a ponta.

1. O Service existe e tem um ClusterIP?

kubectl get svc pagamentos -o wide

Se a coluna CLUSTER-IP mostra None, você tem um headless service — o comportamento é outro (explico daqui a pouco).

2. O nome resolve de dentro de um pod?

kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- \
  nslookup pagamentos.default.svc.cluster.local

Se isso falhar, o problema é DNS (CoreDNS fora do ar, namespace errado, nome errado) — não é rede, não é aplicação.

3. Tem pod atendendo por trás?

kubectl get endpointslice -l kubernetes.io/service-name=pagamentos

Lista vazia é o segundo erro mais comum do Kubernetes: o selector do Service não bate com os labels do pod. O Service existe, o DNS resolve, o ClusterIP responde — e não tem ninguém do outro lado. Confira com kubectl get pods --show-labels.

Quatro casos que confundem

Mesmo namespace — o nome curto basta. http://pagamentos:8080 funciona e é o que você vai ver na maior parte do código real.

Namespaces diferentes — o nome curto não funciona, porque a primeira entrada do search é o namespace do pod que chama. Aí você precisa de pelo menos pagamentos.financeiro (e o completo pagamentos.financeiro.svc.cluster.local sempre funciona). Esse é o caso em que mais gente acha que "o DNS do cluster está quebrado".

Headless service (clusterIP: None) — não tem IP virtual e não tem balanceamento. O DNS devolve os IPs de todos os pods de uma vez. É o que StatefulSets usam, quando você precisa falar com uma réplica específica — um nó de banco, por exemplo — e não com "qualquer uma".

Preciso de HTTPS entre pods? Por padrão, não. O tráfego não sai da rede do cluster, e você duplicaria custo de CPU com handshake pra proteger uma conversa que já está isolada. Se o seu compliance exige criptografia ponta a ponta, o caminho não é colocar https:// na mão em cada chamada — é um service mesh (Linkerd, Istio) que faz mTLS de forma transparente, sem mudar uma linha do seu código.

Fechando

O .svc.cluster.local não é frescura de YAML: é o Kubernetes te dando um nome estável pra uma coisa que muda o tempo todo. E o Ingress não é "o jeito de expor serviço": é a porta de entrada pra quem vem de fora.

Quando você separa as duas coisas na cabeça, uma categoria inteira de problema simplesmente some — latência que ninguém explica, conta de rede que não fecha, chamada interna que quebra quando o DNS público oscila.

Se esse tipo de conteúdo te ajuda, tem mais no canal do CodeInit no YouTube — e por aqui você encontra também o guia de como configurar uma API Node.js com TypeScript, que é justamente o tipo de serviço que costuma virar esses pods.

Perguntas frequentes

O que significa .svc.cluster.local no Kubernetes? É o sufixo do nome DNS interno de um Service. O formato completo é <service>.<namespace>.svc.cluster.local: svc indica que o registro é de um Service e cluster.local é o domínio interno padrão do cluster. Esse nome só resolve de dentro do cluster.

Qual a diferença entre Service e Ingress no Kubernetes? O Service dá um endereço interno estável para um grupo de pods e trabalha na camada 4, alcançável apenas de dentro do cluster. O Ingress é uma porta de entrada HTTP na camada 7, alcançável da internet, que roteia por host e caminho e encaminha o tráfego para um Service.

Posso usar o Ingress para comunicação entre pods? Tecnicamente funciona, mas é o erro que este artigo descreve. O tráfego sai do cluster e volta, custando latência, tarifa de saída e uma dependência da internet — e pode nem completar se o load balancer não suportar hairpin NAT. Entre pods, use o nome do Service.

Por que http://pagamentos funciona sem o nome completo? Porque o /etc/resolv.conf de todo pod traz uma lista search que começa pelo namespace do próprio pod. O resolver completa pagamentos para pagamentos.<namespace>.svc.cluster.local automaticamente. Isso só vale dentro do mesmo namespace.

Como chamar um serviço em outro namespace? Use pelo menos <service>.<namespace> — por exemplo pagamentos.financeiro. O nome curto falha porque a busca começa pelo namespace de quem chama. O nome completo pagamentos.financeiro.svc.cluster.local funciona sempre.

O que é ndots:5 e por que ele deixa o DNS lento? É a opção do resolver que manda tentar a lista search para qualquer nome com menos de 5 pontos. Domínios externos como api.loja.com têm 2 pontos, então geram três consultas que falham antes da consulta correta. Terminar o domínio com ponto (api.loja.com.) pula a lista.

Preciso de HTTPS entre pods dentro do cluster? Por padrão não, porque o tráfego não deixa a rede interna. Se houver exigência de criptografia ponta a ponta, o caminho recomendado é um service mesh com mTLS transparente, em vez de configurar TLS manualmente em cada serviço.

Cadastre-se para novos posts