Introdução ao Kubernetes: Orquestrando Containers em Escala

Introdução ao Kubernetes: Orquestrando Containers em Escala

A orquestração de containers além de uma única máquina: os componentes do plano de controle e dos nós worker, um cluster local com kind, os objetos Pod, Deployment, Service, ConfigMap, Secret e Ingress, os comandos essenciais do kubectl, o autoscaling por HPA e o isolamento por namespaces.
DevOps

20 min de leitura

O Docker resolve o problema de empacotar e executar uma aplicação em um único container. O Docker Compose resolve o problema de orquestrar múltiplos containers em uma única máquina. Mas quando a aplicação cresce além de uma máquina — quando são necessários dezenas de instâncias distribuídas em múltiplos servidores, com failover automático, atualizações sem downtime e escala dinâmica baseada em demanda — uma nova categoria de problema emerge: a orquestração de containers em escala.

O Kubernetes — frequentemente abreviado como K8s — é o sistema de orquestração de containers que se tornou o padrão da indústria para gerenciar cargas de trabalho containerizadas em ambientes de produção. Originado no Google, que orquestrou containers em escala por mais de uma década antes de abrir o projeto, o Kubernetes foi doado à Cloud Native Computing Foundation em 2014 e desde então se tornou o projeto de infraestrutura de código aberto mais ativo do mundo.

A proposta central do Kubernetes é declarativa: o engenheiro descreve o estado desejado do sistema — "quero cinco réplicas desta aplicação, cada uma com 512MB de memória, acessíveis pela porta 80" — e o Kubernetes trabalha continuamente para garantir que o estado real corresponde ao declarado. Se um container morre, o Kubernetes inicia um novo. Se um nó falha, as cargas de trabalho são reagendadas em nós saudáveis. Se o tráfego aumenta, novas réplicas são criadas automaticamente.

Arquitetura do Kubernetes

Um cluster Kubernetes é composto por dois tipos de máquinas com responsabilidades distintas.

O Plano de Controle

O plano de controle é o cérebro do cluster — o conjunto de componentes que gerencia o estado desejado e toma decisões sobre o cluster.

kube-apiserver — o único ponto de entrada para todas as operações no cluster. Toda comunicação — do kubectl, dos outros componentes do plano de controle, dos agentes nos nós — passa pela API. É stateless e pode ser escalado horizontalmente.

etcd — o banco de dados distribuído de chave-valor que armazena todo o estado do cluster. É o componente mais crítico — perder o etcd sem backup significa perder o cluster inteiro. Em produção, o etcd é executado em um cluster de três ou cinco instâncias para tolerância a falhas.

kube-scheduler — observa pods recém-criados que ainda não têm um nó atribuído e seleciona o nó mais adequado baseado em recursos disponíveis, afinidades, taints e tolerations.

kube-controller-manager — executa os controladores que implementam a lógica de reconciliação. O controlador de réplicas garante que o número correto de pods está em execução. O controlador de endpoints atualiza a lista de IPs nos Services. O controlador de jobs garante que jobs completam com sucesso.

Os Nós Worker

Os nós worker são as máquinas que executam as cargas de trabalho — os containers da aplicação.

kubelet — o agente que roda em cada nó. Recebe especificações de pods do kube-apiserver e garante que os containers descritos estão rodando e saudáveis. Reporta o estado dos containers de volta ao plano de controle.

kube-proxy — mantém as regras de rede em cada nó, implementando o conceito de Services — abstrações que expõem um conjunto de pods como um endpoint de rede estável.

Container Runtime — o software que executa os containers. O Kubernetes suporta qualquer runtime compatível com a CRI — Container Runtime Interface. O containerd é o runtime padrão na maioria das distribuições modernas.

┌─────────────────────────────────────────────────────────────┐
│                     PLANO DE CONTROLE                        │
│                                                             │
│  ┌──────────────┐  ┌──────┐  ┌────────────┐  ┌──────────┐ │
│  │ kube-apiserver│  │ etcd │  │ scheduler  │  │controller│ │
│  └──────────────┘  └──────┘  └────────────┘  └──────────┘ │
└─────────────────────────────────────────────────────────────┘
            │                          │
     ┌──────┴──────┐             ┌─────┴──────┐
     │    NÓ 1     │             │    NÓ 2    │
     │             │             │            │
     │  kubelet    │             │  kubelet   │
     │  kube-proxy │             │ kube-proxy │
     │             │             │            │
     │ ┌─────────┐ │             │ ┌────────┐ │
     │ │  Pod A  │ │             │ │ Pod B  │ │
     │ │ Pod C   │ │             │ │ Pod D  │ │
     │ └─────────┘ │             │ └────────┘ │
     └─────────────┘             └────────────┘

Instalando um Cluster Local com kind

Para aprendizado e desenvolvimento local, o kind (Kubernetes in Docker) cria um cluster Kubernetes completo usando containers Docker como nós — sem precisar de máquinas virtuais:

# Instala o kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

# Instala o kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/kubectl

# Cria um cluster com configuração personalizada
cat > kind-config.yml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=true"
    extraPortMappings:
      - containerPort: 80
        hostPort: 80
        protocol: TCP
      - containerPort: 443
        hostPort: 443
        protocol: TCP
  - role: worker
  - role: worker
EOF

kind create cluster --config kind-config.yml --name meu-cluster

# Verifica que o cluster está funcionando
kubectl cluster-info
kubectl get nodes

Os Objetos Fundamentais do Kubernetes

O Kubernetes é operado inteiramente através de objetos — recursos declarados em arquivos YAML que descrevem o estado desejado do sistema. Cada objeto tem quatro campos obrigatórios: apiVersion, kind, metadata e spec.

Pod: A Unidade Atômica

O Pod é o objeto mais básico do Kubernetes — um grupo de um ou mais containers que compartilham o mesmo namespace de rede e podem compartilhar volumes. Containers dentro de um pod se comunicam via localhost.

Na prática, pods raramente são criados diretamente. Eles são gerenciados por objetos de nível superior — Deployments, StatefulSets, Jobs — que garantem que o número correto de pods está sempre em execução.

# pod.yml — apenas para entender a estrutura; use Deployments em produção
apiVersion: v1
kind: Pod
metadata:
  name: minha-api
  namespace: default
  labels:
    app: minha-api
    versao: "1.5.0"
spec:
  containers:
    - name: api
      image: ghcr.io/minha-empresa/minha-api:1.5.0
      ports:
        - containerPort: 3000
          protocol: TCP
      env:
        - name: NODE_ENV
          value: production
        - name: PORT
          value: "3000"
      resources:
        requests:       # Mínimo garantido pelo scheduler
          memory: "128Mi"
          cpu: "100m"   # 100 millicores = 0.1 CPU
        limits:         # Máximo permitido
          memory: "512Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /health
          port: 3000
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /health
          port: 3000
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3
  restartPolicy: Always

Deployment: Gerenciando Réplicas com Atualizações Controladas

O Deployment é o objeto mais usado para cargas de trabalho stateless. Ele gerencia um ReplicaSet — que por sua vez gerencia os pods — e orquestra atualizações com estratégias de rollout configuráveis:

# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minha-api
  namespace: producao
  labels:
    app: minha-api
  annotations:
    kubernetes.io/change-cause: "Deploy versão 1.5.0 — feature: checkout redesign"
spec:
  replicas: 3

  selector:
    matchLabels:
      app: minha-api

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Máximo de pods extras durante atualização
      maxUnavailable: 0  # Zero pods indisponíveis — zero downtime

  template:
    metadata:
      labels:
        app: minha-api
        versao: "1.5.0"
    spec:
      # Distribui pods entre nós diferentes para resiliência
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: minha-api

      # Graceful shutdown — aguarda conexões ativas terminarem
      terminationGracePeriodSeconds: 30

      containers:
        - name: api
          image: ghcr.io/minha-empresa/minha-api:1.5.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000

          env:
            - name: NODE_ENV
              value: production
            - name: PORT
              value: "3000"
            # Injeta variáveis de ambiente a partir de Secrets
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: minha-api-secrets
                  key: database-url
            - name: REDIS_URL
              valueFrom:
                secretKeyRef:
                  name: minha-api-secrets
                  key: redis-url

          resources:
            requests:
              memory: "256Mi"
              cpu: "200m"
            limits:
              memory: "512Mi"
              cpu: "1000m"

          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 3

          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3

          # Garante que o processo recebe SIGTERM ao terminar
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

          # Contexto de segurança — sem root
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]

          # Volume para arquivos temporários (filesystem read-only)
          volumeMounts:
            - name: tmp
              mountPath: /tmp

      volumes:
        - name: tmp
          emptyDir: {}

      # Pull de imagem privada
      imagePullSecrets:
        - name: ghcr-credentials

Service: Expondo Pods com IP Estável

Pods têm IPs efêmeros — quando um pod morre e é recriado, recebe um IP diferente. O Service resolve esse problema fornecendo um IP e nome DNS estáveis para um conjunto de pods selecionados por labels:

# service.yml
apiVersion: v1
kind: Service
metadata:
  name: minha-api
  namespace: producao
spec:
  selector:
    app: minha-api  # Seleciona pods com este label

  ports:
    - name: http
      port: 80          # Porta do Service
      targetPort: 3000  # Porta do container
      protocol: TCP

  type: ClusterIP  # Acessível apenas dentro do cluster

Os tipos de Service disponíveis:

ClusterIP — padrão. Acessível apenas dentro do cluster. Usado para comunicação interna entre serviços.

NodePort — expõe o Service em uma porta estática em cada nó do cluster. Acessível externamente via IP_DO_NO:NODEPORT. Raramente usado em produção diretamente.

LoadBalancer — solicita ao cloud provider a criação de um load balancer externo. Na AWS, cria um Network Load Balancer ou Classic Load Balancer. É a forma mais comum de expor serviços em clusters gerenciados como EKS.

ConfigMap e Secret: Configuração Separada do Código

# configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: minha-api-config
  namespace: producao
data:
  NODE_ENV: "production"
  LOG_LEVEL: "info"
  PORT: "3000"
  # Arquivo de configuração completo como entrada no ConfigMap
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://localhost:3000;
      }
    }
# secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: minha-api-secrets
  namespace: producao
type: Opaque
# Valores em base64 — não é criptografia, apenas encoding
# Em produção, use External Secrets Operator ou Sealed Secrets
data:
  database-url: cG9zdGdyZXNxbDovL...  # base64 da string
  redis-url: cmVkaXM6Ly9...
  jwt-secret: c2VjcmV0b211aXRvc2VndXJv...

Ingress: Roteamento HTTP de Entrada

O Ingress define regras de roteamento HTTP/HTTPS para o tráfego de entrada no cluster. Requer um Ingress Controller — um pod que implementa as regras definidas pelo Ingress:

# ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minha-api
  namespace: producao
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
    cert-manager.io/cluster-issuer: "letsencrypt-producao"
spec:
  ingressClassName: nginx

  tls:
    - hosts:
        - api.empresa.com
      secretName: api-empresa-tls

  rules:
    - host: api.empresa.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: minha-api
                port:
                  number: 80

Comandos Essenciais do kubectl

# ── Contextos e clusters ──────────────────────────
kubectl config get-contexts          # Lista clusters configurados
kubectl config use-context meu-cluster  # Muda de cluster

# ── Inspecionando recursos ────────────────────────
kubectl get pods -n producao         # Lista pods no namespace producao
kubectl get pods -A                  # Lista pods em todos os namespaces
kubectl get all -n producao          # Todos os recursos do namespace
kubectl describe pod minha-api-abc -n producao  # Detalhes de um pod
kubectl logs minha-api-abc -n producao          # Logs de um pod
kubectl logs minha-api-abc -n producao -f       # Logs em tempo real
kubectl logs minha-api-abc -n producao --previous  # Logs da instância anterior

# ── Aplicando configurações ───────────────────────
kubectl apply -f deployment.yml      # Cria ou atualiza recursos
kubectl apply -f ./manifests/        # Aplica todos os YAMLs de um diretório
kubectl delete -f deployment.yml     # Remove recursos descritos no arquivo

# ── Operações em pods ─────────────────────────────
kubectl exec -it minha-api-abc -n producao -- /bin/sh  # Shell interativo
kubectl port-forward pod/minha-api-abc 8080:3000        # Port forward local

# ── Rollout e atualizações ────────────────────────
kubectl rollout status deployment/minha-api -n producao
kubectl rollout history deployment/minha-api -n producao
kubectl rollout undo deployment/minha-api -n producao   # Rollback
kubectl set image deployment/minha-api api=ghcr.io/empresa/api:1.6.0 -n producao

# ── Escalando ─────────────────────────────────────
kubectl scale deployment/minha-api --replicas=5 -n producao

# ── Diagnóstico ───────────────────────────────────
kubectl top pods -n producao         # Uso de CPU e memória dos pods
kubectl top nodes                    # Uso de CPU e memória dos nós
kubectl get events -n producao --sort-by='.lastTimestamp'

Horizontal Pod Autoscaler

O HPA escala automaticamente o número de réplicas baseado em métricas — CPU, memória ou métricas customizadas via Prometheus Adapter:

# hpa.yml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: minha-api
  namespace: producao
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: minha-api

  minReplicas: 3
  maxReplicas: 20

  metrics:
    # Escala por CPU
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

    # Escala por memória
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

    # Escala por métrica customizada do Prometheus
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "100"

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60   # Aguarda 60s antes de escalar para cima
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300  # Aguarda 5min antes de escalar para baixo
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60

Namespaces: Isolamento Lógico no Cluster

Os Namespaces dividem um cluster em ambientes virtuais isolados. É uma forma eficiente de separar equipes, ambientes ou projetos sem precisar de clusters separados:

# Cria os namespaces para diferentes ambientes
kubectl create namespace producao
kubectl create namespace staging
kubectl create namespace monitoring

# Define o namespace padrão para o contexto atual
kubectl config set-context --current --namespace=producao
# network-policy.yml — isola o namespace de produção
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isolamento-producao
  namespace: producao
spec:
  podSelector: {}  # Aplica a todos os pods do namespace

  policyTypes:
    - Ingress
    - Egress

  ingress:
    # Permite tráfego apenas do Ingress Controller
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx

  egress:
    # Permite saída para DNS
    - ports:
        - protocol: UDP
          port: 53
    # Permite saída para o namespace de banco de dados
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: databases

O Kubernetes é declarativo até nas consequências: descrever o estado desejado significa abrir mão de controlar o caminho até ele. Boa parte da frustração inicial vem de tentar tratar Pod como servidor — ele é descartável por projeto, e o que persiste são os objetos que o descrevem. Entendido isso, kubectl describe e os eventos do cluster passam a explicar quase todo problema, porque contam o que o controlador tentou fazer e por que desistiu.

Fontes e leituras recomendadas

Documentação oficial

Aprendizado prático

Livros e cursos de referência

  • Kubernetes: Up and Running — O'Reilly — Livro de referência escrito pelos criadores do Kubernetes, cobrindo desde os fundamentos até operações avançadas em produção.
  • CNCF Landscape — landscape.cncf.io — Mapa interativo do ecossistema Cloud Native, mostrando todas as ferramentas e projetos organizados por categoria. Essencial para entender o ecossistema ao redor do Kubernetes.

Exercícios

Exercício 1

No deployment.yml, livenessProbe e readinessProbe apontam para o mesmo caminho /health. Qual a diferença de comportamento entre as duas sondas quando falham? E o que acontece com as três réplicas se esse /health for implementado fazendo um SELECT 1 no banco e o banco ficar indisponível por dois minutos?

Ver resposta

✓ Resposta: As consequências são bem diferentes. Quando o readinessProbe falha, o pod é removido dos endpoints do Service — para de receber tráfego, mas continua rodando. É reversível: assim que a sonda voltar a passar, o pod volta a receber requisições. Quando o livenessProbe falha, o kubelet mata o container e o reinicia. É destrutivo e não pergunta se o problema era do pod.

Com /health consultando o banco, a indisponibilidade de dois minutos derruba a aplicação inteira. O liveness tem periodSeconds: 15 e failureThreshold: 3 — cerca de 45 segundos de falhas e o kubelet reinicia o container. Como o banco está fora para todos os pods ao mesmo tempo, as três réplicas entram em restart simultâneo, sem nenhuma delas ter defeito. Pior: cada reinício zera pool de conexões e cache em memória, então quando o banco voltar, os pods estarão em CrashLoopBackOff, com o kubelet esperando intervalos crescentes antes de tentar de novo — a aplicação demora a se recuperar bem depois do banco já estar de pé.

A regra prática: o liveness só deve verificar o próprio processo (o event loop responde? a thread principal travou?), sem tocar em dependências externas. Dependências pertencem ao readiness, onde a resposta correta é "não me mande tráfego agora", não "me reinicie". Na prática são dois endpoints distintos — /health/live e /health/ready.

Exercício 2

O container declara lifecycle.preStop executando sleep 5, e o pod tem terminationGracePeriodSeconds: 30. Dormir cinco segundos antes de encerrar parece desperdício — que problema esse sleep resolve? E quanto tempo, no total, o Kubernetes espera antes de matar o container à força?

Ver resposta

✓ Resposta: Ele resolve uma corrida entre dois processos assíncronos. Quando um pod é marcado para terminação, duas coisas acontecem em paralelo: o kubelet inicia o encerramento do container, e o controlador de endpoints remove o pod do Service — o que ainda precisa se propagar até as regras de rede de cada nó (kube-proxy) e até o Ingress Controller. A segunda é bem mais lenta.

Sem o preStop, o container recebe SIGTERM e começa a recusar conexões enquanto vários nós ainda o têm na lista de destinos. O resultado são erros 502/504 intermitentes durante cada rollout — o sintoma clássico de deploy "com zero downtime" que ainda assim derruba requisições. O sleep 5 segura o container vivo e servindo normalmente durante a janela de propagação; só depois o SIGTERM é enviado.

Sobre o tempo total: o terminationGracePeriodSeconds engloba o preStop — não é somado a ele. A contagem é: 5 s de preStop, depois SIGTERM, e o processo tem os 25 s restantes para encerrar conexões ativas. Passados os 30 s, vem o SIGKILL. Vale notar que o hook não é gratuito: um preStop maior que o grace period consome todo o tempo e o container é morto à força sem nunca ter tratado o SIGTERM.

Exercício 3

O container declara requests de 256Mi/200m e limits de 512Mi/1000m. Descreva o que acontece com o pod em cada caso: (a) o processo tenta usar 900m de CPU; (b) o processo tenta alocar 600Mi de memória.

Ver resposta

✓ Resposta: (a) Nada acontece — 900m está abaixo do limite de 1000m, então o processo usa livremente. O requests de 200m não é um teto: é o que o scheduler reserva ao escolher o nó, e o piso garantido em caso de disputa. Se o processo pedisse 1500m, seria throttled: o kernel simplesmente lhe daria menos fatias de CPU. O processo continua vivo, apenas mais lento — o sintoma aparece como latência alta sem nenhum erro, e é notoriamente difícil de diagnosticar sem olhar as métricas de throttling.

(b) O container é morto na hora, com OOMKilled. Aqui está a assimetria que importa: CPU é compressível, memória não é. Não existe "usar memória mais devagar" — ao ultrapassar o limite, o cgroup dispara o OOM killer do kernel e o processo morre sem chance de tratar nada. Com restartPolicy: Always, o container reinicia; se o consumo for estrutural (não um pico), o ciclo se repete até CrashLoopBackOff.

Duas consequências práticas: o kubectl describe pod mostra Last State: Terminated / Reason: OOMKilled, e kubectl logs --previous é o que recupera os logs da instância morta — o comando está listado no artigo justamente para esse caso. Para aplicações Node.js e JVM, ajustar o limite não basta: o runtime precisa saber do teto (--max-old-space-size, -XX:MaxRAMPercentage), ou tentará crescer até bater no cgroup.

Exercício 4

O securityContext define readOnlyRootFilesystem: true. Logo abaixo, o mesmo container monta um volume emptyDir em /tmp. Por que essa montagem é necessária, e o que aconteceria com ela se o pod fosse reiniciado?

Ver resposta

✓ Resposta: Com readOnlyRootFilesystem: true, todo o sistema de arquivos do container fica somente-leitura — qualquer escrita retorna EROFS: read-only file system. O problema é que muita coisa escreve em /tmp sem que o código da aplicação peça: upload de arquivos em frameworks web, cache de compilação, sockets Unix, bibliotecas de imagem, o próprio runtime. A aplicação quebraria com erros aparentemente sem relação com segurança. O emptyDir devolve uma única fatia gravável, sem abrir mão do resto.

É essa a razão da flag existir: um invasor que consiga execução remota não consegue persistir nada — não escreve binário, não altera arquivo de configuração, não injeta código no diretório da aplicação. Combinada com runAsNonRoot, allowPrivilegeEscalation: false e capabilities.drop: ["ALL"], reduz bastante o que se pode fazer depois de entrar.

Quanto ao reinício: o emptyDir tem o ciclo de vida do pod, não do container. Se o container reinicia (por OOM ou falha de liveness), o pod continua o mesmo e o conteúdo de /tmp sobrevive. Se o pod é removido — rollout, reagendamento em outro nó, escala para baixo — o volume é apagado definitivamente. Por isso emptyDir nunca serve para dado que precisa durar: para isso existem PersistentVolumeClaim e StatefulSets.

Exercício 5

O deployment.yml declara replicas: 3 e o hpa.yml declara minReplicas: 3, maxReplicas: 20 para o mesmo Deployment. Numa terça de pico o HPA escalou para 12 réplicas; nesse momento alguém faz merge de uma mudança de variável de ambiente e o pipeline roda kubectl apply -f deployment.yml. O que acontece com as 12 réplicas?

Ver resposta

✓ Resposta: O cluster cai para 3 réplicas no meio do pico. O apply envia o manifesto com replicas: 3, o Deployment é atualizado, e nove pods são encerrados de uma vez. O HPA percebe a sobrecarga no ciclo seguinte e começa a escalar de volta, mas o behavior.scaleUp deste HPA impõe stabilizationWindowSeconds: 60 e no máximo 4 pods por minuto — voltar de 3 para 12 leva mais de dois minutos, com a aplicação degradada ou fora do ar durante a subida.

A causa é ter dois controladores donos do mesmo campo. A correção é omitir replicas do manifesto quando existe um HPA: o campo passa a ser gerenciado apenas pelo autoscaler, e o apply deixa de sobrescrevê-lo. O minReplicas: 3 do HPA já cumpre o papel de garantir o piso de três réplicas.

Duas notas sobre isso. Se o Deployment for criado sem HPA e sem replicas, o padrão é 1 — por isso a ordem importa, e o HPA deve existir antes. E se a equipe usa GitOps com reconciliação contínua (Argo CD, Flux), o problema é ainda maior: sem essa omissão, a ferramenta enxerga as 12 réplicas como drift e reverte para 3 sozinha, repetidamente, sem que ninguém tenha feito deploy.

Repare que o scaleDown foi configurado deliberadamente mais conservador — stabilizationWindowSeconds: 300 e 1 pod por minuto — justamente para evitar remoção brusca de capacidade. Um apply com replicas fixo passa por cima de toda essa proteção.

Comentários

Mais em DevOps

Navegando pelo Sistema de Arquivos com Confiança
Navegando pelo Sistema de Arquivos com Confiança

A árvore de diretórios do Linux e o que cada ramo guarda: /etc, /var, /home…

O Terminal Não é o Inimigo
O Terminal Não é o Inimigo

Por que um curso de DevOps começa por um mês inteiro de terminal: o que é o…

Terraform + Ansible: Do Provisionamento à Configuração
Terraform + Ansible: Do Provisionamento à Configuração

A fronteira entre provisionar e configurar, e como cruzá-la sem trabalho…