Pods e Services
Os três recursos que você vai escrever todos os dias.
Pods, Services e Deployments
Um Pod tem um endereço IP. Esse endereço muda a cada recriação do Pod, e um Pod é recriado com frequência: a cada deploy, a cada falha, a cada vez que o scheduler decide movê-lo. Nenhuma aplicação pode, portanto, chamar outra pelo endereço. É o problema que esta lição resolve, e ele é mais profundo do que parece.
Os três recursos a seguir formam uma cadeia em que cada elo tem uma razão de ser precisa. O Pod executa. O Deployment garante que exista o número certo de Pods na versão certa, e sabe passar de uma versão para outra sem interrupção. O Service fornece um nome estável e um endereço estável na frente de um conjunto de Pods cuja identidade varia o tempo todo.
O que liga esses três objetos não é uma referência direta, e é esse o ponto que desorienta no início: eles não se conhecem pelo nome, mas por labels. Um Service não sabe quais Pods ele expõe; ele declara um critério de seleção, e o sistema associa a ele continuamente todos os Pods que o atendem. Esse acoplamento indireto é o que permite que um Pod desapareça e que seu substituto seja servido imediatamente, sem nenhuma reconfiguração.
Entender esse mecanismo de labels é entender 80 % do Kubernetes. Um seletor mal escrito produz exatamente o sintoma mais opaco para o iniciante: um Service que responde, mas com um erro de conexão, porque não aponta para nada.
O Deployment em vez do Pod
Não se cria um Pod à mão, exceto em um teste descartável. Um Pod sozinho não tem nenhuma resiliência: se morrer, continua morto. Declara-se um Deployment, que assume essa responsabilidade.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web # deve corresponder aos labels do template abaixo
template: # o molde a partir do qual os Pods são fabricados
metadata:
labels:
app: web # os labels que os Pods criados terão
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
Duas armadilhas neste arquivo, ambas ligadas ao mesmo ponto.
A primeira: spec.selector.matchLabels e spec.template.metadata.labels devem corresponder. O selector diz “os Pods pelos quais sou responsável têm este label”, o template diz “os Pods que eu fabrico têm este label”. Se divergirem, o API server rejeita o manifesto, o que é uma boa notícia: o erro é detectado na escrita.
A segunda: spec.selector é imutável após a criação. Para modificá-lo, é preciso excluir e recriar o Deployment. Escolha, portanto, seus labels com um pouco de atenção.
kubectl apply -f deployment.yaml
kubectl get pods -l app=web -w # observar o aumento de capacidade ao vivo
kubectl get deployment web # réplicas prontas sobre réplicas desejadas
O intermediário ReplicaSet
Um Deployment não controla os Pods diretamente. Ele cria um ReplicaSet, e é esse ReplicaSet que mantém a contagem de Pods. Essa camada parece gratuita até entendermos para que serve:
kubectl get replicasets
# NAME DESIRED CURRENT READY AGE
# web-7d4b8c9f5 3 3 3 2m
Cada versão do template produz um ReplicaSet distinto. Durante uma atualização, o Deployment reduz o ReplicaSet antigo e aumenta o novo. O antigo é mantido com zero réplicas — é exatamente isso que torna o rollback possível e instantâneo: a definição anterior ainda está lá, basta escalá-la de volta.
Verifique a hierarquia: exclua um Pod à mão, e ele será recriado em poucos segundos pelo ReplicaSet, que constatou que a contagem não correspondia mais.
kubectl delete pod web-7d4b8c9f5-xk2mp
kubectl get pods -l app=web # um novo Pod, com um novo sufixo
Os labels e os seletores
Um label é um par chave-valor livre, aplicado a qualquer objeto. Não é documentação: é o mecanismo de ligação do sistema.
metadata:
labels:
app: web
tier: frontend
version: v1
environnement: production
O kubectl sabe consultar esses labels, o que substitui com vantagem a leitura manual de listas:
kubectl get pods -l app=web # igualdade
kubectl get pods -l app=web,tier=frontend # conjunção
kubectl get pods -l 'app!=web' # negação
kubectl get pods -l 'environnement in (staging,production)' # conjunto
kubectl get pods -l version # presença da chave
kubectl get pods --show-labels # exibir os labels
O conselho que evita mais bagunça: reserve os labels do seletor para a identidade da aplicação (app, tier) e coloque tudo o que varia com frequência — um número de versão, um identificador de commit — em labels fora do seletor ou em annotations. Um seletor que contém version: v1 quebra a continuidade do Service a cada entrega.
O Service: um endereço que não muda
Um Service cria um endereço IP virtual e um nome DNS estáveis, e distribui o tráfego para todos os Pods que correspondem ao seu seletor.
ClusterIP, o caso padrão
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # valor padrão: acessível de dentro do cluster
selector:
app: web # todos os Pods com app=web tornam-se alvos
ports:
- name: http
port: 80 # porta do Service
targetPort: 80 # porta escutada no contêiner
protocol: TCP
A distinção port / targetPort é constante e vale a pena fixá-la: port é o que os clientes chamam, targetPort é o que o contêiner escuta. Nada obriga que sejam iguais, e raramente são na prática.
O Service é acessível pelo nome a partir de qualquer Pod do cluster, graças ao DNS interno:
web # mesmo namespace
web.atelier # namespace explícito
web.atelier.svc.cluster.local # nome completo
Verifique a partir de um Pod efêmero, é o teste que tira a dúvida mais rápido:
kubectl run debug --image=busybox:1.36 --rm -it --restart=Never -- \
wget -qO- http://web:80
O diagnóstico essencial: os endpoints
Um Service que não encontra nenhum Pod não devolve um erro explícito. Ele existe, tem um endereço, e simplesmente recusa as conexões. A causa é quase sempre um seletor que não corresponde a nada.
kubectl get endpoints web # os Pods realmente por trás do Service
# Lista vazia? O seletor não corresponde a nenhum Pod. Compare os dois:
kubectl get service web -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels
Guarde este reflexo: conexão recusada em um Service, olhe os endpoints antes de qualquer outra coisa. Isso transforma meia hora de tentativas às cegas em trinta segundos.
NodePort
NodePort abre a mesma porta em todos os nós do cluster, na faixa 30000-32767.
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080 # opcional; caso contrário, o Kubernetes atribui um
# o minikube fornece a URL diretamente acessível
minikube service web-nodeport --url
É prático em desenvolvimento e deve ser evitado em produção: a porta não é padrão, não há TLS nem nome de domínio, e a exposição ocorre em todos os nós ao mesmo tempo. Para tráfego HTTP real, a resposta certa é um Ingress, visto na lição 6.
LoadBalancer e sua dependência do provedor
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
Vamos ser precisos, porque este ponto engana muita gente: o Kubernetes não sabe criar um balanceador de carga. Ele emite uma solicitação e espera que um controlador externo a atenda. Na AWS, no Google Cloud ou no Azure, esse controlador existe e provisiona um recurso cobrado. Em um cluster local ou em uma instalação sem integração com a nuvem, ninguém responde, e o Service fica indefinidamente com um endereço externo <pending>.
kubectl get service web-lb
# EXTERNAL-IP permanece <pending> sem controlador: não é um bug
# Com o minikube, um comando simula o comportamento (deixe rodando)
minikube tunnel
| Tipo | Alcance | Uso realista |
|---|---|---|
ClusterIP | interno ao cluster | comunicação entre serviços: o caso comum |
NodePort | porta aberta em cada nó | desenvolvimento, acesso para depuração |
LoadBalancer | endereço externo fornecido pela nuvem | ponto de entrada público, depende do provedor |
As atualizações graduais
É o serviço que o Kubernetes entrega melhor, e é quase gratuito. Mude a imagem de um Deployment: os Pods são substituídos em lotes, mantendo o serviço disponível durante toda a transição.
# Fragmento a inserir no spec do Deployment, ao lado de replicas.
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # no máximo 1 Pod indisponível durante a troca
maxSurge: 1 # no máximo 1 Pod além dos 4 desejados
Com esses valores, há sempre entre 3 e 5 Pods durante a transição, dos quais pelo menos 3 disponíveis. maxUnavailable: 0 garante zero perda de capacidade, ao preço de um deploy mais lento, já que é preciso primeiro criar antes de destruir.
kubectl apply -f deployment.yaml # após modificar a imagem no manifesto
# Variante imperativa, útil para uma correção urgente
kubectl set image deployment/web nginx=nginx:1.27.1-alpine
kubectl rollout status deployment/web # termina quando a troca é concluída
O rollback
Como os ReplicaSets antigos são mantidos, voltar atrás não exige nenhuma reconstrução:
kubectl rollout history deployment/web # histórico das revisões
kubectl rollout undo deployment/web # voltar à anterior
kubectl rollout undo deployment/web --to-revision=2 # a uma revisão específica
# Pausar e depois retomar uma troca em andamento, se os erros aumentarem
kubectl rollout pause deployment/web
kubectl rollout resume deployment/web
Uma observação honesta sobre o histórico: por padrão ele não mostra o que mudou entre duas revisões, apenas o número delas. Para tornar o histórico legível, adicione uma annotation a cada entrega:
metadata:
name: web
annotations:
kubernetes.io/change-cause: "Passage à nginx 1.27.1 — correctif CVE"
O escalonamento
kubectl scale deployment/web --replicas=6 # imperativo
kubectl apply -f deployment.yaml # declarativo, após editar o arquivo
Prefira o declarativo. Um kubectl scale é sobrescrito no próximo apply do manifesto, o que produz uma regressão silenciosa e difícil de explicar.
As probes: liveness e readiness
Sem probe, o Kubernetes considera um contêiner saudável assim que seu processo principal está rodando. É uma aproximação grosseira: uma aplicação pode ter iniciado seu servidor HTTP sem ser capaz de responder — conexão com o banco não estabelecida, cache não carregado. O Service enviará então tráfego para um Pod que responderá com erros.
Duas probes respondem a duas perguntas distintas, e confundi-las tem consequências sérias.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: mon-api:1.0.0
ports:
- containerPort: 8080
# « Este Pod pode receber tráfego agora? »
# Em falha: removido dos endpoints do Service, mas NÃO reiniciado.
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
# « Este processo está definitivamente travado? »
# Em falha: o contêiner é MORTO e reiniciado.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
# « A inicialização foi concluída? »
# Enquanto falhar, a livenessProbe fica suspensa.
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30
Os erros clássicos, para conhecer antes de cometê-los:
- Fazer a liveness apontar para um endpoint que verifica o banco de dados. Se o banco ficar lento, todos os Pods falham na probe, são mortos ao mesmo tempo, e uma lentidão passageira vira uma queda total. A liveness deve testar apenas o próprio processo; as dependências externas são assunto da readiness.
- Um
initialDelaySecondscurto demais na liveness. A aplicação é morta antes de terminar de iniciar, em loop: é oCrashLoopBackOffinexplicável. AstartupProbeé a resposta adequada para inicializações lentas. - Não declarar nenhuma readiness. O tráfego chega a um Pod que não está pronto, e cada deploy provoca uma onda de erros 502.
Existem três formas de probes, conforme o que a aplicação expõe:
# HTTP: qualquer código de 200 a 399 é um sucesso
readinessProbe:
httpGet: { path: /ready, port: 8080 }
---
# TCP: a conexão abre?
readinessProbe:
tcpSocket: { port: 5432 }
---
# Comando: código de saída 0 = sucesso
livenessProbe:
exec: { command: ["sh", "-c", "test -f /tmp/sain"] }
Diagnosticar um Pod
Os comandos para saber de cor, na ordem em que são usados:
# 1. Visão geral: STATUS e RESTARTS contam o essencial
kubectl get pods -o wide
# 2. A seção Events no fim da saída dá a causa real
kubectl describe pod web-7d4b8c9f5-xk2mp
# 3. Os logs da aplicação
kubectl logs web-7d4b8c9f5-xk2mp
kubectl logs -f deployment/web # acompanhamento ao vivo
kubectl logs web-7d4b8c9f5-xk2mp --previous # logs do contêiner ANTES do crash
# 4. Entrar no contêiner
kubectl exec -it web-7d4b8c9f5-xk2mp -- sh
# 5. Acessar o Pod a partir da máquina local, sem expô-lo
kubectl port-forward pod/web-7d4b8c9f5-xk2mp 8080:80
A opção --previous é daquelas que a gente descobre tarde demais. Em um Pod em CrashLoopBackOff, kubectl logs mostra o contêiner atual, muitas vezes vazio porque acabou de reiniciar. --previous mostra a instância que realmente falhou e, portanto, a mensagem de erro.
Os status mais frequentes e seus significados:
| Status | Causa habitual |
|---|---|
Pending | o scheduler não encontra um nó: recursos insuficientes ou volume não vinculado |
ImagePullBackOff | imagem não encontrada, tag incorreta ou registry privado sem credenciais |
CrashLoopBackOff | o contêiner inicia e para em loop: veja logs --previous |
Running mas 0/1 READY | a readinessProbe está falhando |
OOMKilled | limite de memória excedido (veja a lição 6) |
Pontos-chave
- Declare um Deployment, nunca um Pod manualmente. Ele cria um ReplicaSet por versão do template, e é a conservação dos ReplicaSets antigos que torna o
kubectl rollout undoinstantâneo. - Os objetos se vinculam por labels, não por nomes. Mantenha estáveis os labels do seletor (
app,tier) e tire do seletor tudo o que varia com frequência — versões, commits —, já que ele é imutável de qualquer forma. - Um Service inacessível se diagnostica com
kubectl get endpoints: uma lista vazia significa um seletor que não corresponde a nenhum Pod. ClusterIPpara uso interno,NodePortpara desenvolvimento,LoadBalancersomente onde existe um controlador de nuvem — caso contrário, o endereço externo permanece<pending>indefinidamente.- readiness retira do tráfego sem reiniciar, liveness mata e reinicia. Nunca coloque uma dependência externa em uma liveness: uma lentidão no banco de dados se transformaria em uma falha geral.
Dica: execute
kubectl get pods -wem um terminal e, em seguida, dispare uma atualização em outro. Ver os Pods aparecerem e desaparecerem em lotes fixa o mecanismo melhor do que qualquer explicação.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão