Índice

  1. O que é CI/CD (em palavras simples)
  2. Por que um pipeline precisa "escalar"
  3. As etapas de um pipeline completo
  4. Build: a fundação do pipeline
  5. Testes: a rede de segurança
  6. Deploy automático: do commit à produção
  7. Rollback: o botão "desfazer" profissional
  8. Estratégias para times grandes
  9. Exemplo real com GitHub Actions
  10. Erros comuns
  11. Checklist final

O que é CI/CD (em palavras simples) {#o-que-e}

Antes de usar a sigla, vamos explicar o que ela significa. CI é Continuous Integration (Integração Contínua) e CD é Continuous Delivery (Entrega Contínua) ou Continuous Deployment (Deploy Contínuo), dependendo do nível de automação que você deseja.

Imagine uma cozinha de restaurante. Sem CI, cada cozinheiro prepara seu prato sozinho, no seu canto, e só no final tudo é juntado — frequentemente os temperos não combinam e ninguém percebeu até o cliente reclamar. Com CI, a cada passo todo mundo prova a receita juntos e ajusta na hora. Com CD, o prato sai da cozinha para a mesa do cliente automaticamente, sem precisar de um garçom intermediário que confere tudo de novo.

Em software: CI é integrar o código de todas as pessoas desenvolvedoras várias vezes ao dia em um branch compartilhado, rodando testes automaticamente a cada integração. CD é garantir que esse código integrado esteja sempre pronto para ir para produção — seja com um clique humano (Continuous Delivery) ou sozinho (Continuous Deployment).

Um pipeline é a sequência automática de etapas que executa tudo isso. Pense numa esteira de montagem de uma fábrica: a peça entra crus, passa por várias estações (solda, pintura, teste, embalagem) e sai pronta para entrega. Se qualquer estação detecta defeito, a peça é rejeitada e volta para correção.

Por que um pipeline precisa "escalar" {#por-que-escalar}

Um pipeline que funciona para 2 pessoas em um repositório quase sempre quebra com 50 pessoas em 20 repositórios. Os sintomas clássicos:

  • Builds lentos: o pipeline demora 40 minutos, o time para de esperar e faz push direto na main (pulo do gato para o desastre).
  • Gargalo de infraestrutura: só há 2 runners disponíveis, 10 PRs esperando na fila, ninguém revisa código porque está esperando CI.
  • Falsos negativos: testes instáveis ("flaky tests") quebram aleatoriamente, o time aprende a ignorar o CI vermelho, e bugs reais passam despercebidos.
  • Custo cloud explosivo: cada PR roda testes E2E completos em 10 ambientes, a fatura da AWS triplica sem ninguém perceber.

Um pipeline escalável é aquele que continua rápido, confiável e barato mesmo quando o time, o número de repositórios e a frequência de deploys crescem 5x, 10x, 50x. Isso não acontece por acidente — é projeto.

SintomaPipeline que NÃO escalaPipeline que escala
Tempo de build30-60 min, cresce linearmente5-10 min, paralelizado
Confiabilidade80% verde, 20% flaky99%+ verde, flaky = bloqueio
Custo por PR$5-15$0.50-2
Fila em horário pico20+ min esperando runner< 2 min
Onboarding de novo repo2 dias de config1 comando (template reutilizável)

As etapas de um pipeline completo {#etapas}

Um pipeline moderno tem, no mínimo, estas estações:

[Commit/PR]
    ↓
1. Checkout & cache         ← baixar código e dependências cacheadas
    ↓
2. Lint & format check      ← estilo de código (rapidíssimo)
    ↓
3. Build                    ← compilar, empacotar, criar imagem Docker
    ↓
4. Testes unitários         ← isolados, rápidos (< 1 min)
    ↓
5. Testes de integração     ← sobem dependências (banco, Redis) em containers
    ↓
6. Security scan            ← SAST, dependências vulneráveis, secrets leak
    ↓
7. Artefato + imagem        ← publicar em registry
    ↓
8. Deploy (staging)         ← ambiente de validação
    ↓
9. Testes E2E / smoke       ← fluxo crítico em ambiente real
    ↓
10. Deploy (produção)       ← com estratégia + rollback automático

Cada etapa deve ser paralelizável quando possível e cachear tudo que puder. A regra de ouro: o que muda pouco deve ser reaproveitado, o que muda muito deve rodar em paralelo.

Build: a fundação do pipeline {#build}

O build é onde o código-fonte vira algo executável. Em linguagens compiladas (Go, Rust, Java) é a compilação; em linguagens interpretadas (Python, Node) é a instalação de dependências + transpilação (TypeScript → JavaScript, por exemplo).

Dicas de build escalável:

  1. Cache de dependências: reusar node_modules/, ~/.m2, ~/.cargo entre execuções economiza minutos. GitHub Actions tem actions/cache nativo.
  2. Build em container: garantir que o build rode igual na máquina de qualquer pessoa e no CI. Um Dockerfile de build padroniza o ambiente.
  3. Docker layer caching: em imagens Docker, copie primeiro os arquivos de dependência (package.json, go.mod) e instale antes de copiar o código. Assim a layer de dependências fica em cache e só invalida quando muda.
  4. Build matrix: testar em múltiplas versões (Node 18 e 20, Python 3.10 e 3.12) em paralelo, não em sequência.
# Dockerfile otimizado para cache de layers
FROM node:20-alpine AS builder
WORKDIR /app

# Copia só manifestos primeiro → layer de deps cacheada
COPY package*.json ./
RUN npm ci --omit=dev

# Só agora copia o código → só esta layer invalida a cada commit
COPY . .
RUN npm run build

# Imagem final enxuta (multi-stage)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

Testes: a rede de segurança {#testes}

Sem testes, CI é só um empacotador caro. A pirâmide de testes clássica continua válida e é o que mantém o pipeline rápido:

            ▲
           /E2E\          ← poucos, lentos, em ambiente real
          /──────\
         /Integração\     ← médios, sobem dependências
        /────────────\
       /   Unitários   \   ← muitos, rápidos, isolados
      /──────────────────\

Testes unitários

Rápidos (milissegundos), sem rede nem banco. Devem cobrir a maior parte da lógica de negócio. Se um teste unitário demora mais que 200ms, algo está errado — provavelmente ele não é unitário de verdade.

Testes de integração

Sobem dependências reais em containers (Testcontainers é a ferramenta padrão). Validam que seu código conversa certo com Postgres, Redis, Kafka. Demoram segundos, não milissegundos.

Testes E2E

Simulam um usuário real navegando (Playwright, Cypress). São lentos e frágeis, por isso devem ser poucos e focar nos fluxos críticos: login, checkout, pagamento. Rodar E2E em todo PR é o erro mais comum de custo de CI.

Lidando com testes flaky

Teste flaky é aquele que ora passa, ora falha, sem mudança de código. É o pior inimigo da confiança no pipeline. Estratégia:

  1. Detectar: marcar testes que falham e passam no retry como suspeitos.
  2. Quarentena: mover para uma suite separada que não bloqueia o PR.
  3. Corrigir ou remover: um teste flaky é pior que não ter teste, porque ensina o time a ignorar o CI vermelho.

Regra: um pipeline que pisca verde/vermelho é tratado como enfeite. Preferência absoluta: CI confiável, mesmo que mais lento, do que CI rápido e instável.

Deploy automático: do commit à produção {#deploy}

Deploy automatizado é onde o "CD" brilha. Mas automático não significa irresponsável — significa repetível e auditável.

Ambientes

  • Dev: local da pessoa desenvolvedora.
  • Staging: cópia fiel de produção (mesma versão de libs, dados anonimizados).
  • Produção: onde o usuário real está.

Estratégias de deploy

EstratégiaComo funcionaQuando usar
RollingSubstitui instâncias antigas por novas aos poucosDefault, sem downtime
Blue/GreenDois ambientes idênticos; switch de tráfego num botãoRollback instantâneo
Canary1% → 5% → 25% → 100% do tráfego na nova versãoRisco alto de regressão
Feature flagCódigo novo dorme em produção, liga por flagExperimentação A/B

Pipeline de deploy vs. deploy manual

O deploy manual depende de uma pessoa rodar kubectl apply ou clicar num painel. É lento, propenso a erro e não escalável. O deploy via pipeline é determinístico: as mesmas ações sempre na mesma ordem, com logs auditáveis e gates de qualidade.

# Trecho de deploy com gate de aprovação
- name: Deploy to staging
  run: ./scripts/deploy.sh staging

- name: Smoke tests
  run: ./scripts/smoke.sh staging-url

- name: Aguardar aprovação manual (produção)
  uses: trstringer/manual-approval-gateway@v1
  with:
    approvers: tech-leads
    minimum-approvals: 1

- name: Deploy to production (canary 10%)
  run: ./scripts/deploy.sh production --canary 10

Rollback: o botão "desfazer" profissional {#rollback}

Todo deploy vai falhar um dia. A diferença entre um incidente de 5 minutos e um de 5 horas é ter rollback automático e testado.

Tipos de rollback

  • Rollback de versão (imagem): voltar para a imagem Docker anterior. Rápido, resolve bugs de código.
  • Rollback de schema (banco): mais delicado. Migrações destrutivas (DROP COLUMN) não têm volta trivial. Por isso, migrações devem ser reversíveis e feitas em duas fases (expand → contract).
  • Rollback de feature flag: desligar a flag. O mais barato — não precisa nem redeployar.

Automatizando o rollback

Defina métricas que disparam rollback sozinhas: taxa de erro > 1% por 2 minutos, latência P99 > 500ms, taxa de 5xx acima do baseline. Ferramentas como Argo Rollouts e Flagger fazem isso nativamente no Kubernetes.

# Argo Rollouts: análise automática que reverte se erro subir
analysis:
  templates:
    - templateName: success-rate
  startingStep: 2   # só analisa depois de 10% de tráfego
metrics:
  - name: success-rate
    successCondition: result >= 0.99
    failureLimit: 2   # 2 falhas consecutivas → rollback automático

Estratégias para times grandes {#times-grandes}

Quando o time passa de ~10 pessoas, surgem problemas novos. Aqui vão as estratégias que separam pipelines que escalam de pipelines que travam.

1. Reusable workflows

No GitHub Actions, você pode reutilizar workflows inteiros. Em vez de cada repositório copiar 200 linhas de YAML, chame um workflow central:

# .github/workflows/deploy.yml em qualquer repo
jobs:
  deploy:
    uses: org/inicialize-pipeline/.github/workflows/deploy-template.yml@v3
    with:
      service-name: ${{ github.repository }}
      environment: production
    secrets: inherit

Mudou a versão do template? Atualize @v3 para @v4 e todos os repositórios herdam a melhoria num commit.

2. Monorepo vs. multi-repo

  • Monorepo: um repositório, pipeline precisa ser inteligente para só rodar testes dos pacotes afetados (ferramentas: Nx, Turborepo, Bazel).
  • Multi-repo: cada repo tem seu pipeline, mas compartilha templates reutilizáveis para não repetir configuração.

3. Cache agressivo

  • Cache de build tools (npm, pip, cargo, gradle).
  • Cache de Docker (actions/cache para /var/lib/docker ou BuildKit).
  • Cache de testes (Playwright armazena screenshots e traces).

4. Self-hosted runners / runners sob demanda

Runners do GitHub são limitados e pagos por minuto. Para times grandes, runners auto-hospedados com auto-scaling (Actions Runner Controller no K8s) reduzem custo e fila. Para builds esporádicos pesados, use runners efêmeros subidos on-demand.

5. Gates de qualidade, não de egos

Bons gates: cobertura não pode cair, testes novos obrigatórios para novos arquivos, SAST sem vulnerabilidades críticas. Maus gates: "precisa de 3 approvals de staff engineers" — vira gargalo humano e não escala.

Exemplo real com GitHub Actions {#exemplo-github}

Aqui está um pipeline completo, comentado, pronto para adaptar:

# .github/workflows/ci-cd.yml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:

# Cancela runs anteriores do mesmo PR — economiza runners
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # Job 1: tudo rápido e paralelo
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run lint        # ESLint, Prettier

  test-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm test -- --coverage   # Vitest/Jest

  # Job 2: build só roda se lint+test passarem
  build:
    needs: [lint, test-unit]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: app:${{ github.sha }}
          cache-from: type=gha     # cache de layers no GitHub
          cache-to: type=gha,mode=max
      - uses: actions/upload-artifact@v4
        with: { name: image, path: image.tar }

  # Job 3: deploy só na main, nunca em PR
  deploy:
    if: github.ref == 'refs/heads/main'
    needs: build
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with: { name: image }
      - run: ./scripts/deploy.sh ${{ github.sha }}

Pontos de atenção nesse exemplo:

  • concurrency cancela runs velhas do mesmo PR → economia de runner.
  • needs define a ordem e falha rápida: se lint quebra, build nem roda.
  • cache-from: type=gha reaproveita layers Docker entre execuções.
  • environment: production cria um gate de aprovação configurável no GitHub.
  • Deploy só na main — PRs não tocam produção.

Erros comuns {#erros}

  1. Rodar tudo em série: cada job esperando o anterior terminar. Solução: paralelize jobs independentes com needs apenas onde há dependência real.
  2. Ignorar cache: instalar dependências do zero a cada run. Cada npm ci sem cache custa 1-3 minutos que se somam em milhares de runs por mês.
  3. Segredos no código: colocar chaves AWS direto no YAML. Use secrets do GitHub/OIDC e nunca logue tokens.
  4. Testes E2E em todo PR: lentidão e custo explosivo. Separe E2E para agendamento noturno ou apenas na main.
  5. Sem rollback testado: "vamos voltar manual se quebrar". No pânico do incidente, ninguém lembra os passos. Teste o rollback em staging periodicamente.
  6. Deploy direto na main sem gate: em times grandes, um push errado derruba produção. Use protection rules + revisão + checks obrigatórios.
  7. Migrações de banco no mesmo pipeline do app: quebra se a nova versão do app for rejeitada mas o schema já mudou. Separe migrações em pipeline próprio com deploy antes do app (padrão expand/contract).
  8. Achar que CI/CD é ferramenta, não cultura: comprar Jenkins/GitHub Actions não entrega valor se o time não segue a disciplina de "main sempre verde".

Checklist final {#checklist}

  • Pipeline roda em menos de 10 minutos para 90% dos PRs
  • Dependências cacheadas (build tools + Docker layers)
  • Testes flaky em quarentena, não no caminho crítico
  • Lint, unit, integração, security scan em paralelo
  • Deploy com estratégia (rolling/blue-green/canary) e gate
  • Rollback automático disparado por métricas (erro rate, latência)
  • Migrações de banco separadas e reversíveis (expand/contract)
  • Templates reutilizáveis para novos repositórios
  • Runners com auto-scaling para evitar fila em horário pico
  • Custo de CI monitorado e com alertas (orçamento mensal)
  • main sempre deployable — protection rules + checks obrigatórios
  • Runners e secrets auditados periodicamente

Quer um pipeline que escala com seu time? A Inicialize Tec implementa CI/CD do commit ao deploy, com rollback automático, observabilidade e templates reutilizáveis para múltiplos repositórios. Conversamos sobre seu caso.