Aplicações complexas
Juntando as peças: um ponto de entrada, cotas, elasticidade.
Implantação de aplicações complexas
Agora você já sabe escrever um Deployment, expô-lo, injetar sua configuração e dar a ele um volume. Uma aplicação real, por sua vez, é composta por vários serviços que precisam se comunicar, ser acessíveis pela Internet sob um único nome de domínio, não esgotar os recursos uns dos outros e absorver um aumento de tráfego sem intervenção humana.
Três problemas novos aparecem nessa escala. O primeiro é o ponto de entrada: criar um Service do tipo LoadBalancer para cada microsserviço significa pagar por vários endereços públicos, sem roteamento por caminho nem certificado compartilhado. O segundo é o compartilhamento de recursos: sem nenhuma restrição declarada, um serviço com vazamento de memória pode derrubar os Pods vizinhos no mesmo nó. O terceiro é a elasticidade: dimensionar para o pico equivale a pagar pelo pico o tempo todo.
O quarto problema não é técnico, mas organizacional, e costuma ser o que dói mais rápido. Uma aplicação com cinco serviços representa cerca de trinta manifestos, que precisam ser duplicados para cada ambiente alterando três valores. O copiar e colar se torna, então, uma grande fonte de falhas: alguém modifica o arquivo de homologação e esquece o de produção.
Esta lição junta as peças em um exemplo completo e, em seguida, responde a esse último ponto com uma visão geral do Helm. O objetivo não é acrescentar conceitos, mas mostrar como os que você já conhece funcionam em conjunto.
A aplicação de referência
Três componentes em um namespace dedicado: um banco de dados PostgreSQL, uma API e uma interface web. O namespace não é decorativo: ele carrega as cotas, as regras de acesso e permite apagar tudo de uma só vez.
apiVersion: v1
kind: Namespace
metadata:
name: boutique
labels:
environnement: production
O banco de dados
Um StatefulSet com seu Service headless, exatamente conforme o modelo da lição anterior: serviceName: postgres, clusterIP: None, um volumeClaimTemplates de 10 Gi. Duas adições próprias de uma implantação completa, a serem inseridas no contêiner:
# Uma reserva generosa: um banco de dados agendado em um nó
# já carregado ficará lento sem que nenhum alerta seja disparado.
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
memory: "1Gi"
# pg_isready evita que a API receba tráfego antes da inicialização.
readinessProbe:
exec:
command: ["pg_isready", "-U", "boutique", "-d", "boutique"]
initialDelaySeconds: 10
periodSeconds: 5
A API
Este é o manifesto mais instrutivo: ele reúne configuração, secret, probes e recursos.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: boutique
data:
APP_ENV: "production"
LOG_LEVEL: "info"
# O nome do Service basta: o DNS interno o resolve dentro do namespace.
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "boutique"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: boutique
labels:
app: api
tier: backend
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # a capacidade nunca diminui durante a troca
maxSurge: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
tier: backend
spec:
containers:
- name: api
image: exemple/boutique-api:1.4.0
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: db-credentials
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: boutique
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 8080
targetPort: 8080
name: http
A interface web
Mesma estrutura: um Deployment com duas réplicas e um Service ClusterIP na porta 80. Um único ponto exige atenção, e ele costuma pegar muita gente:
env:
# O navegador NÃO está no cluster: ele não resolve
# http://api:8080. A URL deve ser um caminho público do Ingress.
- name: API_BASE_URL
value: "/api"
Apenas o código executado dentro do cluster se beneficia do DNS interno. O JavaScript do navegador, por sua vez, passa obrigatoriamente pelo ponto de entrada público — daí a próxima seção.
O Ingress
Um Ingress é uma regra de roteamento HTTP: um nome de domínio, caminhos e Services de destino. Um único ponto de entrada atende toda a aplicação.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: boutique
namespace: boutique
annotations:
# Reescreve /api/produits em /produits antes do encaminhamento.
# Esta annotation é ESPECÍFICA do ingress-nginx.
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
tls:
- hosts:
- boutique.example.com
secretName: boutique-tls # Secret do tipo kubernetes.io/tls
rules:
- host: boutique.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
O ponto a entender antes de passar uma hora depurando: o objeto Ingress não faz nada por conta própria. Ele é uma declaração inerte enquanto um controlador de entrada (ingress controller) não a tiver lido. Sem um controlador instalado, o apply funciona e nada é roteado.
minikube addons enable ingress
kubectl get pods -n ingress-nginx # o controlador está rodando?
kubectl get ingress -n boutique # ADDRESS vazio = regra não atendida
minikube ip # o endereço a colocar em /etc/hosts
A segunda ressalva, mais estrutural: as annotations são específicas de cada controlador. nginx.ingress.kubernetes.io/rewrite-target não tem nenhum significado para Traefik, HAProxy ou o AWS Load Balancer Controller, que têm seu próprio prefixo e sua própria sintaxe. Apenas a estrutura rules é padronizada; reescrita, timeouts, limitação de taxa e redirecionamento HTTPS são dialeto de cada controlador. Foi essa divergência que motivou a Gateway API, destinada a substituir o Ingress no longo prazo.
Há três valores de pathType: Exact (correspondência estrita), Prefix (por segmentos de caminho, o caso comum) e ImplementationSpecific, delegado ao controlador e necessário para padrões com captura como o acima.
Requests e limits de recursos
Dois valores, dois papéis distintos. Confundi-los produz falhas difíceis de interpretar.
resources:
requests: # RESERVA: usada no agendamento
cpu: "100m" # 100 millicores = 0,1 core
memory: "128Mi"
limits: # TETO: usado para conter desvios
memory: "256Mi"
requests é o que o scheduler usa para escolher um nó: ele soma os requests dos Pods já presentes e só coloca um novo Pod se a soma continuar abaixo da capacidade. Um Pod sem requests é colocado em qualquer lugar, inclusive em um nó já saturado.
limits é o teto aplicado durante a execução, e CPU e memória não reagem a ele da mesma forma:
- Ultrapassar o limite de CPU provoca throttling: o contêiner é desacelerado. A aplicação fica lenta, sem erro nem reinício — um sintoma traiçoeiro, muitas vezes atribuído erroneamente à rede ou ao banco de dados.
- Ultrapassar o limite de memória provoca um
OOMKilled: o contêiner é encerrado imediatamente. A memória não é compressível.
kubectl describe pod api-7d4b8c9f5-xk2mp | grep -A5 "Last State"
kubectl top pods -n boutique # consumo real, se o metrics-server estiver presente
kubectl top nodes
Daí a prática recomendada, contraintuitiva à primeira vista: um limite de memória, mas nenhum limite de CPU — é a escolha feita nos manifestos acima. O limite de memória protege o nó de um vizinho com vazamento, pois a saturação de memória provoca evicções em massa. O limite de CPU, por sua vez, deixa processador ocioso quando uma aplicação poderia usá-lo: o request já garante sua parte em caso de contenção.
O namespace pode impor salvaguardas, em vez de depender da disciplina de cada autor de manifesto:
apiVersion: v1
kind: LimitRange
metadata:
name: valeurs-par-defaut
namespace: boutique
spec:
limits:
- type: Container
default: # limits aplicados se ausentes no Pod
memory: "256Mi"
defaultRequest: # requests aplicados se ausentes no Pod
cpu: "100m"
memory: "128Mi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-boutique
namespace: boutique
spec:
hard:
requests.cpu: "4"
requests.memory: "8Gi"
limits.memory: "16Gi"
persistentvolumeclaims: "10"
Uma consequência a antecipar: assim que um ResourceQuota abrange um recurso de computação, todo Pod do namespace deve declarar o valor correspondente, caso contrário sua criação é recusada. O LimitRange é justamente o que evita que essa regra quebre as implantações existentes.
O autoscaling horizontal
O HorizontalPodAutoscaler ajusta o número de réplicas de acordo com uma métrica observada.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: boutique
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 70 % do REQUEST, não do limit
behavior:
scaleUp: # subir rápido para absorver um pico
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown: # descer devagar, senão oscila
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
O detalhe decisivo está no comentário: averageUtilization: 70 incide sobre o request, não sobre o limit. Com requests.cpu: 100m, o escalonamento é disparado com 70 millicores de consumo médio. Um request mal calibrado torna, portanto, o autoscaling errático: baixo demais, ele adiciona réplicas o tempo todo; alto demais, nunca reage.
Dois pré-requisitos rígidos, que explicam quase todos os HPAs que «não funcionam»: o metrics-server deve estar instalado, e os contêineres-alvo devem declarar requests para a métrica escolhida — sem request de CPU, a porcentagem não pode ser calculada.
minikube addons enable metrics-server
kubectl get hpa -n boutique # TARGETS em <unknown> = métricas ausentes
kubectl describe hpa api -n boutique
Observe por fim que o HPA adiciona Pods, mas não cria nós: se o cluster estiver cheio, os novos Pods ficam em Pending. Adicionar capacidade de máquina é papel do Cluster Autoscaler ou do Karpenter, específicos de cada provedor de nuvem.
Implantar e verificar o conjunto
A ordem de criação não precisa ser orquestrada: os loops de reconciliação convergem, e um Pod que ainda não encontra sua dependência será reiniciado.
kubectl apply -f k8s/ --recursive
kubectl rollout status deployment/api -n boutique
kubectl rollout status statefulset/postgres -n boutique
kubectl get all -n boutique
kubectl get events -n boutique --sort-by=.metadata.creationTimestamp
O diagnóstico de uma aplicação com vários serviços segue sempre o mesmo caminho, de fora para dentro:
kubectl describe ingress boutique -n boutique # 1. a regra está sendo atendida?
kubectl get endpoints -n boutique # 2. os Services apontam para Pods?
kubectl get pods -n boutique # 3. READY 3/3, e não apenas Running?
kubectl port-forward -n boutique service/api 8080:8080 # 4. contornar o Ingress
Essa progressão isola a falha em quatro comandos. Se o port-forward funciona, mas o Ingress não, o problema está no roteamento e não na aplicação — uma hora de leitura de logs da aplicação economizada.
Uma visão geral do Helm
Esses manifestos funcionam. O problema surge no segundo ambiente: a mesma coisa com duas réplicas, outro nome de domínio e uma imagem de homologação. O copiar e colar cria duas fontes de verdade que vão divergir.
O Helm é um gerenciador de pacotes para Kubernetes. Um chart reúne manifestos parametrizados cujos valores são extraídos para um arquivo separado: Chart.yaml descreve o pacote, values.yaml contém os valores padrão e templates/ contém os manifestos.
# values.yaml
api:
image:
repository: exemple/boutique-api
tag: "1.4.0"
replicas: 3
ingress:
host: boutique.example.com
# templates/deployment-api.yaml — trecho
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.api.replicas }}
template:
spec:
containers:
- name: api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
# Verificar a renderização SEM enviar nada ao cluster: faça isso sempre
helm template boutique ./boutique -f values-production.yaml
helm install boutique ./boutique -n boutique --create-namespace
helm upgrade boutique ./boutique -n boutique -f values-production.yaml
helm history boutique -n boutique
helm rollback boutique 1 -n boutique
O que o Helm oferece: um único conjunto de manifestos para vários ambientes, versionamento das entregas e rollback em escala de toda a aplicação — enquanto o kubectl rollout undo trata apenas um objeto por vez.
O custo, e é preciso dizer: a templatização prejudica a legibilidade. Um chart muito parametrizado vira um exercício de lógica de template em que não se enxerga mais o YAML gerado. É por isso que alternativas como o Kustomize, que sobrepõe patches a um YAML que continua legível, conquistaram seu público. Adquira o reflexo de rodar helm template antes de qualquer helm upgrade: você verá o que o cluster realmente vai receber.
Pontos-chave
- O Ingress centraliza um ponto de entrada HTTP para todos os seus Services. Ele fica inerte sem um controlador de entrada (Ingress controller) instalado, e suas anotações são específicas de cada controlador: apenas a estrutura
rulesé portável. requestsserve para o agendamento (scheduling),limitspara a execução. Ultrapassar o limite de CPU causa lentidão silenciosa (throttling); ultrapassar o limite de memória mata o contêiner (OOMKilled). Daí o uso comum: sempre um limite de memória, raramente um limite de CPU.- O HPA raciocina em porcentagem da requisição (request), não do limite. Ele exige o
metrics-servererequestsdeclaradas, e adiciona Pods sem criar nós. - Faça o diagnóstico de fora para dentro: Ingress, depois endpoints, depois estado dos Pods e, por fim,
port-forwardpara contornar o roteamento e isolar a camada com problema. - O Helm evita a duplicação de manifestos entre ambientes e oferece um rollback global. O preço é a legibilidade: sempre confira o resultado com
helm templateantes de aplicar.
Para ir além: os assuntos que prolongam naturalmente este curso são RBAC, NetworkPolicy, observabilidade (Prometheus, Grafana) e entrega contínua com GitOps. A documentação oficial kubernetes.io/fr/docs/concepts/ continua sendo a referência a consultar em primeiro lugar.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão