Introdução ao Kubernetes: Orquestrando Containers em Escala

[307] 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 Que Vem a Seguir

O próximo artigo aprofunda o Kubernetes em produção com o Amazon EKS — o serviço gerenciado de Kubernetes da AWS. Serão cobertos o provisionamento do cluster via Terraform, a gestão de node groups com Karpenter para auto scaling eficiente e as práticas de segurança essenciais para clusters EKS em produção.

Referências para Aprofundamento

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

State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto
State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto

O arquivo que liga o código à infraestrutura real: a anatomia do…

Docker Volumes e Redes: Persistência e Comunicação
Docker Volumes e Redes: Persistência e Comunicação

Volumes e redes no Docker: por que o sistema de arquivos do container é…

Escrevendo um Dockerfile do Zero
Escrevendo um Dockerfile do Zero

Escrevendo um Dockerfile do zero: o papel de cada instrução, por que a ordem…