Rota interna vs externa no Kubernetes: .svc.cluster.local ou Ingress?
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:

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.

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:
- Um nome de DNS estável —
pagamentos, que o CoreDNS resolve dentro do cluster. - Um IP virtual estável (o ClusterIP) — que não muda enquanto o Service existir, mesmo que todos os pods sejam recriados.
- 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.

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

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.