0%
Pods, Services e Deployments

Pods e Services

Os três recursos que você vai escrever todos os dias.

15-20 min

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
TipoAlcanceUso realista
ClusterIPinterno ao clustercomunicação entre serviços: o caso comum
NodePortporta aberta em cada nódesenvolvimento, acesso para depuração
LoadBalancerendereço externo fornecido pela nuvemponto 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 initialDelaySeconds curto demais na liveness. A aplicação é morta antes de terminar de iniciar, em loop: é o CrashLoopBackOff inexplicável. A startupProbe é 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:

StatusCausa habitual
Pendingo scheduler não encontra um nó: recursos insuficientes ou volume não vinculado
ImagePullBackOffimagem não encontrada, tag incorreta ou registry privado sem credenciais
CrashLoopBackOffo contêiner inicia e para em loop: veja logs --previous
Running mas 0/1 READYa readinessProbe está falhando
OOMKilledlimite 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 undo instantâ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.
  • ClusterIP para uso interno, NodePort para desenvolvimento, LoadBalancer somente 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 -w em 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

Link copiado!