Kubernetes em Produção: Segurança, GitOps e Deploys Avançados

[309] Kubernetes em Produção: Segurança, GitOps e Deploys Avançados

A distância entre funcionar e estar pronto para produção: os perfis do Pod Security Standards, Network Policies com default deny, GitOps com ArgoCD e Kustomize, e as estratégias Blue-Green e Canary com análise automática de métricas pelo Argo Rollouts.
DevOps

23 min de leitura

Criar um cluster EKS e fazer uma aplicação rodar nele é relativamente simples. Operar esse cluster com segurança, confiabilidade e rastreabilidade ao longo do tempo é um desafio de ordem diferente. A distância entre "funciona" e "está pronto para produção" em Kubernetes é medida em três dimensões: quanto o cluster resiste a ataques e erros de configuração, quanto o estado desejado é rastreável e reproduzível, e quão sofisticadas são as estratégias de deploy para minimizar o risco de cada mudança.

Este artigo cobre essas três dimensões: segurança com Pod Security Standards e Network Policies, GitOps com ArgoCD para deploys declarativos e auditáveis, e estratégias avançadas de deploy — Blue-Green e Canary — que reduzem o risco de cada nova versão.

Pod Security Standards: Limitando o Que os Pods Podem Fazer

Por padrão, um pod no Kubernetes pode rodar como root, montar o sistema de arquivos do host, escalar privilégios e acessar namespaces do sistema operacional do nó. Em produção, nenhum desses comportamentos deve ser permitido a menos que explicitamente necessário.

O Pod Security Standards é o mecanismo nativo do Kubernetes — introduzido na versão 1.25 como substituto ao Pod Security Policy — que define três perfis de segurança aplicados no nível de namespace:

Privileged — sem restrições. Permite todos os comportamentos. Usado apenas para workloads de infraestrutura como o próprio CNI e o CSI Driver.

Baseline — previne as configurações mais conhecidamente perigosas. Bloqueia containers privilegiados, HostPID, HostIPC e HostNetwork, mas ainda permite rodar como root.

Restricted — segurança máxima. Segue as melhores práticas de hardening. Exige que os containers rodem como não-root, que o filesystem raiz seja somente leitura e que capabilities sejam explicitamente removidas.

# Aplica o perfil restricted ao namespace producao
# Pods que violarem as políticas serão bloqueados (enforce)
# Pods que violarem serão logados mas não bloqueados (audit/warn)
apiVersion: v1
kind: Namespace
metadata:
  name: producao
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

Para que os pods funcionem no perfil restricted, o Deployment precisa incluir as configurações de segurança correspondentes:

# Deployment compatível com o perfil restricted
spec:
  template:
    spec:
      # SecurityContext no nível do pod
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault

      containers:
        - name: api
          image: ghcr.io/empresa/minha-api:1.5.0

          # SecurityContext no nível do container
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
            # Adiciona apenas capabilities específicas se necessário
            # add: ["NET_BIND_SERVICE"]

          volumeMounts:
            # Diretórios que precisam de escrita montados como emptyDir
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /app/.cache

      volumes:
        - name: tmp
          emptyDir: {}
        - name: cache
          emptyDir: {}

Network Policies: Microsegmentação de Rede

Por padrão, todos os pods em um cluster Kubernetes podem se comunicar livremente entre si — independentemente do namespace. As Network Policies definem regras de firewall no nível de pod, restringindo quais conexões são permitidas.

Uma estratégia eficaz começa com uma política de default deny que bloqueia todo o tráfego, seguida de políticas específicas que abrem apenas o que é necessário:

# Política de default deny — bloqueia todo tráfego não explicitamente permitido
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: producao
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
# Permite que a API receba tráfego do Ingress Controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-ingress-para-api
  namespace: producao
spec:
  podSelector:
    matchLabels:
      app: minha-api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
      ports:
        - protocol: TCP
          port: 3000
---
# Permite que a API acesse o banco de dados e o Redis
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-saida-api
  namespace: producao
spec:
  podSelector:
    matchLabels:
      app: minha-api
  policyTypes:
    - Egress
  egress:
    # DNS — necessário para qualquer comunicação
    - ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Banco de dados PostgreSQL via RDS Proxy
    - to:
        - ipBlock:
            cidr: 10.0.20.0/24  # Subnet do RDS Proxy
      ports:
        - protocol: TCP
          port: 5432
    # Redis via ElastiCache
    - to:
        - ipBlock:
            cidr: 10.0.20.0/24
      ports:
        - protocol: TCP
          port: 6379
    # API externa de pagamento (HTTPS)
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
      ports:
        - protocol: TCP
          port: 443

GitOps com ArgoCD

O GitOps é um modelo operacional em que o estado desejado de toda a infraestrutura e das aplicações é declarado em repositórios Git. O Git torna-se a única fonte de verdade — qualquer mudança no cluster passa por um commit, uma revisão e um merge, criando um histórico auditável de tudo que foi modificado, por quem e quando.

O ArgoCD é o controlador de GitOps mais adotado no ecossistema Kubernetes. Ele monitora um repositório Git e garante continuamente que o estado do cluster corresponde ao declarado no repositório — detectando e revertendo automaticamente desvios causados por mudanças manuais.

Instalando o ArgoCD

# Instala o ArgoCD no cluster
kubectl create namespace argocd
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Aguarda todos os componentes ficarem prontos
kubectl wait --for=condition=available \
  --timeout=300s deployment -l app.kubernetes.io/part-of=argocd \
  -n argocd

# Obtém a senha inicial do admin
kubectl get secret argocd-initial-admin-secret \
  -n argocd \
  -o jsonpath="{.data.password}" | base64 -d

# Acessa a interface via port-forward
kubectl port-forward svc/argocd-server -n argocd 8080:443

Via Terraform e Helm para ambientes de produção:

# argocd.tf
resource "helm_release" "argocd" {
  name             = "argocd"
  repository       = "https://argoproj.github.io/argo-helm"
  chart            = "argo-cd"
  namespace        = "argocd"
  create_namespace = true
  version          = "6.4.0"

  values = [yamlencode({
    global = {
      domain = "argocd.${var.domain_name}"
    }

    server = {
      ingress = {
        enabled          = true
        ingressClassName = "alb"
        annotations = {
          "alb.ingress.kubernetes.io/scheme"      = "internet-facing"
          "alb.ingress.kubernetes.io/target-type" = "ip"
          "alb.ingress.kubernetes.io/backend-protocol" = "HTTPS"
        }
        tls = true
      }
    }

    configs = {
      params = {
        # Serve HTTP internamente — o TLS é terminado no ALB
        "server.insecure" = "true"
      }

      cm = {
        # SSO com GitHub
        "dex.config" = yamlencode({
          connectors = [{
            type = "github"
            id   = "github"
            name = "GitHub"
            config = {
              clientID     = var.github_client_id
              clientSecret = var.github_client_secret
              orgs = [{
                name = var.github_org
              }]
            }
          }]
        })
      }

      rbac = {
        # Time de DevOps tem acesso admin
        # Outros times têm acesso read-only por padrão
        "policy.default" = "role:readonly"
        "policy.csv"     = <<-EOT
          p, role:admin, applications, *, */*, allow
          p, role:admin, clusters, get, *, allow
          p, role:admin, repositories, *, *, allow
          g, ${var.github_org}:devops, role:admin
        EOT
      }
    }

    repoServer = {
      resources = {
        requests = { cpu = "100m", memory = "256Mi" }
        limits   = { memory = "512Mi" }
      }
    }
  })]
}

Estrutura do Repositório GitOps

A estrutura do repositório Git que o ArgoCD monitora segue convenções bem estabelecidas:

gitops-repo/
├── apps/                          # Definições das Applications do ArgoCD
│   ├── minha-api-staging.yml
│   ├── minha-api-producao.yml
│   └── monitoring.yml
│
├── clusters/
│   ├── staging/
│   │   ├── namespaces.yml
│   │   ├── network-policies.yml
│   │   └── resource-quotas.yml
│   └── producao/
│       ├── namespaces.yml
│       ├── network-policies.yml
│       └── resource-quotas.yml
│
└── workloads/
    ├── minha-api/
    │   ├── base/                  # Configuração base (Kustomize)
    │   │   ├── deployment.yml
    │   │   ├── service.yml
    │   │   ├── hpa.yml
    │   │   └── kustomization.yml
    │   ├── staging/               # Overlay de staging
    │   │   ├── kustomization.yml
    │   │   └── patches/
    │   │       └── replicas.yml
    │   └── producao/              # Overlay de produção
    │       ├── kustomization.yml
    │       └── patches/
    │           └── replicas.yml
    └── monitoring/
        └── ...

Application do ArgoCD

Cada Application do ArgoCD define qual repositório Git e qual caminho monitorar, e em qual cluster e namespace aplicar:

# apps/minha-api-producao.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: minha-api-producao
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: producao

  source:
    repoURL: https://github.com/empresa/gitops-repo
    targetRevision: main
    path: workloads/minha-api/producao

  destination:
    server: https://kubernetes.default.svc
    namespace: producao

  syncPolicy:
    automated:
      prune: true      # Remove recursos deletados do Git
      selfHeal: true   # Reverte mudanças manuais no cluster
      allowEmpty: false

    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true

    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  # Ignora campos gerenciados pelo cluster (não pelo Git)
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas  # HPA gerencia réplicas — ignora diffs

Kustomize: Gerenciando Variações por Ambiente

O Kustomize é integrado ao kubectl e ao ArgoCD, permitindo customizar manifestos base para diferentes ambientes sem duplicação:

# workloads/minha-api/base/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yml
  - service.yml
  - hpa.yml
  - service-account.yml

commonLabels:
  app: minha-api
  managed-by: argocd

images:
  - name: ghcr.io/empresa/minha-api
    newTag: latest  # Substituído pelo overlay de cada ambiente
# workloads/minha-api/producao/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: producao

resources:
  - ../base
  - external-secret.yml
  - ingress.yml

images:
  - name: ghcr.io/empresa/minha-api
    newTag: "abc1234"  # SHA do commit — atualizado pelo pipeline de CI

patches:
  - path: patches/deployment-producao.yml
    target:
      kind: Deployment
      name: minha-api
# workloads/minha-api/producao/patches/deployment-producao.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minha-api
spec:
  replicas: 5
  template:
    spec:
      containers:
        - name: api
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "1Gi"
              cpu: "2000m"

O pipeline de CI atualiza a tag da imagem no repositório GitOps após o build:

# No workflow de CI — etapa de atualização do GitOps
- name: Atualiza imagem no repositório GitOps
  run: |
    git clone https://x-access-token:${{ secrets.GITOPS_TOKEN }}@github.com/empresa/gitops-repo.git
    cd gitops-repo

    # Atualiza a tag da imagem com o SHA do commit
    cd workloads/minha-api/producao
    kustomize edit set image ghcr.io/empresa/minha-api:${{ github.sha }}

    git config user.email "pipeline@empresa.com"
    git config user.name "Pipeline CI"
    git add .
    git commit -m "chore: atualiza minha-api para ${{ github.sha }}

    Deploy automático pelo pipeline de CI
    Repositório: ${{ github.repository }}
    Run: ${{ github.run_id }}"
    git push

Estratégias Avançadas de Deploy

Blue-Green Deployment

O deploy Blue-Green mantém duas versões do ambiente em paralelo — Blue (versão atual) e Green (nova versão). O tráfego é transferido instantaneamente de Blue para Green após verificação da saúde do Green. O rollback é igualmente instantâneo — basta redirecionar o tráfego de volta para Blue.

# blue-green/deployment-blue.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minha-api-blue
  namespace: producao
  labels:
    app: minha-api
    slot: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: minha-api
      slot: blue
  template:
    metadata:
      labels:
        app: minha-api
        slot: blue
        versao: "1.4.0"
    spec:
      containers:
        - name: api
          image: ghcr.io/empresa/minha-api:1.4.0
---
# blue-green/deployment-green.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minha-api-green
  namespace: producao
  labels:
    app: minha-api
    slot: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: minha-api
      slot: green
  template:
    metadata:
      labels:
        app: minha-api
        slot: green
        versao: "1.5.0"
    spec:
      containers:
        - name: api
          image: ghcr.io/empresa/minha-api:1.5.0
---
# O Service aponta para blue ou green — alterado no momento da troca
apiVersion: v1
kind: Service
metadata:
  name: minha-api
  namespace: producao
spec:
  selector:
    app: minha-api
    slot: blue  # Muda para "green" no momento da promoção
  ports:
    - port: 80
      targetPort: 3000

Script de troca Blue-Green:

#!/bin/bash
# scripts/blue-green-switch.sh

NAMESPACE="producao"
NOVO_SLOT="${1:?Uso: $0 <blue|green>}"
VERSAO="${2:?Uso: $0 <blue|green> <versao>}"

echo "Verificando saúde do slot $NOVO_SLOT antes da troca..."

# Verifica que o deployment do novo slot está saudável
kubectl rollout status deployment/minha-api-$NOVO_SLOT \
  -n $NAMESPACE --timeout=120s

READY=$(kubectl get deployment minha-api-$NOVO_SLOT \
  -n $NAMESPACE -o jsonpath='{.status.readyReplicas}')
DESIRED=$(kubectl get deployment minha-api-$NOVO_SLOT \
  -n $NAMESPACE -o jsonpath='{.spec.replicas}')

if [ "$READY" != "$DESIRED" ]; then
  echo "ERRO: $READY/$DESIRED pods prontos no slot $NOVO_SLOT"
  exit 1
fi

echo "Slot $NOVO_SLOT saudável ($READY/$DESIRED pods). Transferindo tráfego..."

# Transfere o tráfego para o novo slot
kubectl patch service minha-api -n $NAMESPACE \
  -p "{\"spec\":{\"selector\":{\"app\":\"minha-api\",\"slot\":\"$NOVO_SLOT\"}}}"

echo "Tráfego transferido para $NOVO_SLOT (versão $VERSAO)"
echo "Para rollback: kubectl patch service minha-api -n $NAMESPACE -p '{...slot anterior...}'"

Canary Deployment com Argo Rollouts

O Argo Rollouts estende o Kubernetes com estratégias de deploy avançadas — Canary e Blue-Green com análise automática de métricas:

# Instala o Argo Rollouts
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
  -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# rollout.yml — substitui o Deployment para uso com Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: minha-api
  namespace: producao
spec:
  replicas: 10
  selector:
    matchLabels:
      app: minha-api
  template:
    metadata:
      labels:
        app: minha-api
    spec:
      containers:
        - name: api
          image: ghcr.io/empresa/minha-api:1.5.0
          # ... resto da spec do container

  strategy:
    canary:
      # Services separados para stable e canary
      stableService: minha-api-stable
      canaryService: minha-api-canary

      # Análise automática de métricas a cada step
      analysis:
        templates:
          - templateName: analise-taxa-erros
        startingStep: 2
        args:
          - name: service-name
            value: minha-api-canary

      steps:
        # Step 1: 10% do tráfego para o canary
        - setWeight: 10
        # Step 2: Pausa e analisa métricas por 5 minutos
        - pause: { duration: 5m }
        # Step 3: 30% do tráfego
        - setWeight: 30
        - pause: { duration: 5m }
        # Step 4: 50% do tráfego
        - setWeight: 50
        - pause: { duration: 10m }
        # Step 5: 100% — promoção completa
        - setWeight: 100
---
# Template de análise de métricas
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: analise-taxa-erros
  namespace: producao
spec:
  args:
    - name: service-name

  metrics:
    - name: taxa-de-sucesso
      interval: 1m
      successCondition: result[0] >= 0.99
      failureLimit: 3

      provider:
        prometheus:
          address: http://kube-prometheus-stack-prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{
              service="{{args.service-name}}",
              status!~"5.."
            }[5m]))
            /
            sum(rate(http_requests_total{
              service="{{args.service-name}}"
            }[5m]))

    - name: latencia-p99
      interval: 1m
      successCondition: result[0] <= 0.5
      failureLimit: 3

      provider:
        prometheus:
          address: http://kube-prometheus-stack-prometheus.monitoring:9090
          query: |
            histogram_quantile(0.99,
              sum(rate(http_request_duration_seconds_bucket{
                service="{{args.service-name}}"
              }[5m])) by (le)
            )

Gerenciando o rollout via CLI do Argo:

# Instala o plugin kubectl para Argo Rollouts
kubectl argo rollouts version

# Visualiza o status do rollout em tempo real
kubectl argo rollouts get rollout minha-api -n producao --watch

# Promove o canary manualmente (ignora pauses)
kubectl argo rollouts promote minha-api -n producao

# Aborta o rollout e faz rollback para a versão estável
kubectl argo rollouts abort minha-api -n producao

# Retoma um rollout pausado
kubectl argo rollouts resume minha-api -n producao

Encerrando o Módulo 8

Com este artigo encerra-se o tema Kubernetes. Foram cobertos os fundamentos do Kubernetes, o provisionamento de clusters EKS com Terraform, IRSA para permissões seguras, Karpenter para auto scaling de nós, e neste artigo as práticas que separam um cluster funcional de um cluster pronto para produção: segurança com Pod Security Standards, GitOps com ArgoCD e estratégias avançadas de deploy com Argo Rollouts.

Os próximos temas entram no território de Segurança e Compliance em DevOps: como integrar segurança em cada etapa do pipeline, como garantir conformidade com regulações como LGPD e SOC2, e como construir uma cultura DevSecOps que trata segurança como responsabilidade de todos.

Referências para Aprofundamento

Documentação oficial

Segurança

GitOps

  • OpenGitOps — opengitops.dev — Definição formal dos princípios do GitOps mantida pela CNCF, com recursos educacionais e referências de implementação.

Exercícios

Exercício 1

Compare os dois blocos abaixo. O primeiro é o da política permitir-ingress-para-api do artigo; o segundo tem uma diferença de indentação de dois espaços. Eles autorizam a mesma coisa?

# A — como está no artigo
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx

# B — podSelector recuado como item próprio da lista
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
Ver resposta

✓ Resposta: Não. São regras bem diferentes, e a distinção é puramente de indentação — um dos erros mais traiçoeiros em Network Policies.

Em A, os dois seletores estão no mesmo item da lista from, e portanto se combinam com E: só é permitida a origem que seja um pod com o label app.kubernetes.io/name: ingress-nginx e esteja no namespace ingress-nginx. É o que se quer.

Em B, são dois itens da lista, e itens de from se combinam com OU. A regra passa a permitir: qualquer pod do namespace ingress-nginxinclusive um pod qualquer que alguém suba ali — ou qualquer pod com aquele label, e aqui vem o detalhe: um podSelector sem namespaceSelector refere-se ao namespace da própria política. Ou seja, B também libera qualquer pod dentro de producao que carregue esse label, coisa que qualquer pessoa com permissão de criar pods no namespace pode fazer.

O que torna o erro perigoso é que ele não quebra nada: o tráfego legítimo do Ingress Controller continua passando, os testes passam, e a política parece funcionar. O que mudou foi só o que ela deixa de bloquear — e isso ninguém percebe olhando o sistema em operação. Vale o hábito de testar Network Policies pelo caminho negativo: subir um pod avulso e confirmar que o acesso é recusado.

Exercício 2

Na política permitir-saida-api, a primeira regra de egress libera as portas 53 TCP e UDP sem nenhum to — para qualquer destino. Por que essa regra precisa existir, se a aplicação só conversa com o RDS Proxy, o Redis e uma API de pagamento? E o que o desenvolvedor observaria se ela fosse esquecida?

Ver resposta

✓ Resposta: Porque a default-deny-all bloqueia todo o egress, inclusive as consultas ao CoreDNS — e sem resolução de nomes a aplicação não chega a lugar nenhum. As outras regras liberam destinos por ipBlock, mas a aplicação não usa IPs: ela usa db.interno.empresa.com, cache.interno..., api.pagamento.com. A tradução desses nomes para IP é o primeiro passo de toda conexão, e é ela que a regra de DNS autoriza.

O sintoma de esquecê-la é enganoso: não é um erro de rede claro, é lentidão seguida de falha. O resolver não recebe resposta (o pacote é descartado em silêncio, não recusado), espera o timeout, tenta o próximo servidor da lista, esgota as tentativas — tipicamente 5, 10, 30 segundos — e só então falha com ENOTFOUND ou EAI_AGAIN. Nos logs parece problema no banco ou no serviço de pagamento; o kubectl exec com curl por IP funciona, e por nome não. O /etc/resolv.conf do pod com ndots: 5 agrava: cada nome vira várias tentativas de busca antes da consulta final.

Sobre a ausência de to: é uma liberação ampla — qualquer destino na porta 53 — aceita aqui pela simplicidade. A versão mais restrita aponta para o CoreDNS explicitamente, e é o que se recomenda em ambientes mais rígidos:

    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Repare que aqui vale exatamente a lição do exercício anterior: os dois seletores precisam ficar no mesmo item da lista.

Exercício 3

No Blue-Green do artigo, os dois Deployments têm replicas: 3 e ambos carregam o label app: minha-api; o que os distingue é o label slot. Responda: (a) quantos pods rodam durante a janela de transição, para uma capacidade útil de 3; (b) o que acontece se alguém editar o Service e remover a linha slot: blue do selector.

Ver resposta

✓ Resposta: (a) Seis pods — o dobro. É o custo estrutural do Blue-Green: a versão nova sobe inteira, com a capacidade completa, antes de receber qualquer tráfego. Para uma aplicação com 3 réplicas leves isso é irrelevante; para 40 réplicas de um serviço pesado, significa dobrar a infraestrutura durante toda a janela — e é justamente por isso que o Canary, que substitui gradualmente, costuma vencer em escala.

(b) O Service passa a selecionar todos os pods com app: minha-api, sem distinção de slot — os 3 pods da versão 1.4.0 e os 3 da 1.5.0. O resultado é tráfego dividido entre as duas versões ao mesmo tempo, com o kube-proxy balanceando entre os 6 endpoints. Não há erro, alerta ou pod em estado ruim: metade dos usuários simplesmente cai numa versão e metade na outra.

O efeito é péssimo justamente por ser silencioso. Se as versões diferirem no formato de resposta ou no schema, o comportamento se torna intermitente — o mesmo usuário recebe respostas diferentes a cada requisição, e o bug é irreproduzível. E se a 1.5.0 tiver uma migration incompatível, ela roda contra o mesmo banco que a 1.4.0 ainda está usando.

É a fragilidade do Blue-Green feito na unha: a separação inteira depende de um label no selector do Service, e o Kubernetes não tem como saber que aquele campo é o interruptor do deploy. O mesmo kubectl patch que promove a versão nova, com um erro de digitação em slot, cria um selector que não casa com nada — e aí o Service fica sem endpoints e a aplicação sai do ar. Ferramentas como o Argo Rollouts existem em boa parte para tirar essa manobra das mãos de um script.

Exercício 4

O Rollout declara analysis.startingStep: 2 e a lista de steps começa com setWeight: 10 e pause: { duration: 5m }. Em que momento a análise automática de métricas efetivamente começa? Qual o risco dessa configuração?

Ver resposta

✓ Resposta: Os steps são indexados a partir de zero: índice 0 é setWeight: 10, índice 1 é a pausa de 5 minutos, índice 2 é setWeight: 30. Com startingStep: 2, a AnalysisRun só é disparada quando o rollout chega aos 30% de tráfego.

O risco é direto: durante o primeiro estágio — 10% do tráfego por 5 minutos inteiros — ninguém está olhando as métricas. Se a versão nova estiver retornando 500 em todas as requisições, o rollout atravessa essa fase sem qualquer verificação; 10% dos usuários recebem erro por cinco minutos e o rollout avança sozinho para 30%. Só então a análise começa a medir, precisa de pelo menos um interval de 1 minuto para o primeiro resultado, e ainda tolera até failureLimit: 3 falhas antes de abortar. Somando tudo, a versão quebrada pode ficar servindo tráfego real por bem mais de dez minutos.

Isso inverte o propósito do canary, que é justamente falhar cedo com exposição mínima. A correção é começar a análise no primeiro estágio (startingStep: 0), quando o dano possível está limitado a 10% do tráfego.

Duas notas sobre a análise em si. As duas métricas usam successCondition como condição de sucessoresult[0] >= 0.99 para a taxa de sucesso e <= 0.5 para a latência p99 —, então o rollout aborta quando elas deixam de ser verdadeiras, três vezes. E há uma armadilha estatística no início: com pouquíssimo tráfego no canary, a query do Prometheus pode devolver resultado vazio ou uma taxa instável calculada sobre meia dúzia de requisições. Um count mínimo na condição, ou um pause curto antes do primeiro cálculo, evita abortar (ou promover) por ruído.

Exercício 5

A Application do ArgoCD usa selfHeal: true e prune: true. Descreva o que o ArgoCD faz em cada uma destas situações: (a) alguém roda kubectl scale deployment/minha-api --replicas=8 -n producao durante um incidente; (b) alguém deleta por engano o arquivo service.yml do repositório GitOps e faz merge.

Ver resposta

✓ Resposta: (a) Nada — e isso é intencional. Normalmente o selfHeal reverteria a mudança manual em segundos, restaurando o valor do Git. Mas esta Application declara ignoreDifferences para /spec/replicas em Deployments, justamente porque quem manda nesse campo é o HPA, não o repositório. O ArgoCD deixa de comparar o campo, e o scale manual sobrevive — até o HPA recalcular e ajustar por conta própria.

Se esse ignoreDifferences não existisse, o resultado seria o pior dos mundos: o operador escala para 8 no meio do incidente, o ArgoCD reverte para o valor do Git em segundos, e a pessoa fica escalando de novo sem entender o que desfaz sua ação. Vale notar que qualquer outra mudança manual — imagem, variável de ambiente, limites — continua sendo revertida normalmente.

(b) O ArgoCD deleta o Service do cluster. É exatamente o que prune: true significa: recurso que sai do Git sai do cluster. O Deployment continua rodando, os pods continuam saudáveis — e o tráfego para de chegar, porque o Ingress aponta para um Service que não existe mais. Um kubectl get pods mostra tudo verde enquanto a aplicação está fora do ar.

O ponto importante é que o GitOps não é uma proteção contra erro humano — é um deslocamento de onde o erro acontece. Sem GitOps, apaga-se um Service com um kubectl delete distraído; com GitOps, apaga-se com um merge distraído. A diferença real é a rastreabilidade (há um commit, um autor e um revisor) e a reversibilidade (git revert restaura o recurso na sincronização seguinte). É o que faz da proteção de branch e do code review parte da infraestrutura, e não uma formalidade.

Duas defesas úteis: o PruneLast=true já configurado garante que remoções aconteçam depois de todo o resto da sincronização, evitando derrubar dependências no meio do caminho; e para recursos que jamais devem ser removidos automaticamente existe a annotation argocd.argoproj.io/sync-options: Prune=false, que exclui aquele objeto do prune.

Comentários

Mais em DevOps

Gerando e Revisando Pipelines com IA
Gerando e Revisando Pipelines com IA

Prompting aplicado a infraestrutura: os quatro elementos de contexto que…

Shell Script do Zero
Shell Script do Zero

Shell script do zero: shebang e permissão de execução, variáveis e…

Estudar e realmente Aprender DevOps & Cloud
Estudar e realmente Aprender DevOps & Cloud

Um panorama de DevOps e Cloud: o problema real que deu origem à prática, por…