← Blog
·10 blog.minutes

Construindo um Pipeline CI/CD Moderno: Do Commit à Produção

Um guia prático para projetar pipelines CI/CD que são rápidos, confiáveis e seguros — abordando GitHub Actions, GitLab CI, caching, estratégias de deploy e monitoramento em produção.

Todo time de software usa CI/CD. Poucos times o usam bem. A diferença entre um pipeline que acelera a entrega e um que atrasa todos se resume ao design: como você estrutura os estágios, como lida com falhas, como gerencia ambientes e como faz o deploy. Um pipeline bem projetado transforma cada commit em um caminho seguro e repetível até a produção. Um mal projetado transforma cada deploy em um incêndio.

Este artigo aborda como um pipeline CI/CD moderno se parece na prática. Vamos percorrer a seleção do provedor, o design dos estágios, caching, gerenciamento de segredos, estratégias de deploy e o monitoramento que une tudo. Cada seção inclui exemplos reais de workflow que você pode adaptar para sua própria stack.

Por que um pipeline CI/CD moderno importa

O propósito do CI/CD não é automação por si só. É encurtar o ciclo de feedback entre escrever código e saber se aquele código funciona em produção. Cada minuto economizado nesse ciclo é um minuto que o desenvolvedor pode gastar no próximo problema em vez de alternar contexto para uma build que falhou vinte minutos atrás por razões que ele não lembra mais.

O CI/CD moderno difere da era clássica do Jenkins-and-Travis em vários aspectos importantes. Primeiro, pipelines agora são definidos como código. Um arquivo .github/workflows/deploy.yml vive no mesmo repositório que a aplicação que ele implanta, versionado junto com ela, revisado com ela. A configuração do pipeline não é mais um artefato separado mantido por um time separado. É parte da base de código, sujeita ao mesmo processo de revisão que qualquer outra mudança.

Segundo, pipelines modernos são projetados para velocidade. Eles usam caching, execução paralela de jobs, builds matriciais e pulo condicional de estágios para completar em minutos em vez de dezenas de minutos. Um pipeline que leva mais tempo que a janela de retenção de contexto do desenvolvedor — aproximadamente dez a quinze minutos — falhou em seu propósito principal.

Terceiro, pipelines modernos são conscientes de segurança por design. Segredos são injetados em tempo de execução a partir de repositórios de segredos dedicados, não embutidos em arquivos de configuração. Ataques à cadeia de suprimentos são mitigados através de pinagem de dependências, lockfiles e verificação de assinatura. Credenciais de deploy são escopadas por ambiente e rotacionadas automaticamente.

Essas três propriedades — pipeline-como-código, design priorizando velocidade e padrões conscientes de segurança — definem como o CI/CD moderno se parece. Todo o resto é detalhe de implementação.

Escolhendo seu provedor de CI/CD e configurando portões de qualidade

O cenário de provedores se consolidou em torno de três grandes opções, cada uma com trade-offs distintos. GitHub Actions é a escolha mais popular para times já no GitHub. A integração estreita com GitHub significa que verificações de pull request, filas de merge e ambientes de deploy funcionam de imediato. O marketplace de actions fornece passos pré-construídos para quase toda ferramenta no ecossistema, e os runners hospedados incluem instâncias macOS, Windows, ARM e GPU para builds especializadas.

GitLab CI é a alternativa mais forte, particularmente para times que querem uma plataforma única para controle de versão, CI/CD, registro de contêiner e armazenamento de artefatos. A configuração de pipeline do GitLab é mais expressiva que a do GitHub Actions em vários aspectos — suporte nativo para pipelines de grafo acíclico direcionado (DAG), pipelines filho e pai, e deploys canário embutidos através do agente GitLab para Kubernetes. GitLab também oferece o maior tempo de CI gratuito entre os principais provedores.

Outras opções servem nichos específicos. Jenkins continua relevante para organizações com investimentos extensivos em plugins e requisitos on-premise, embora sua história de configuração-como-código seja mais fraca. CircleCI oferece tempos de build rápidos através de caching inteligente e paralelismo, e seu formato de configuração é limpo e legível. Buildkite ocupa uma posição híbrida interessante: você fornece sua própria infraestrutura, Buildkite fornece a camada de orquestração, dando aos times controle total sobre o ambiente de execução sem gerenciar um servidor CI.

  • GitHub Actions: melhor para times nativos do GitHub, maior ecossistema de ações pré-construídas, gratuito para repositórios públicos.
  • GitLab CI: melhor para plataforma DevOps de ponta a ponta, integração mais forte com Kubernetes, nível gratuito generoso.
  • CircleCI: melhor para times que priorizam velocidade bruta, excelente caching e paralelismo.
  • Buildkite: melhor para times que precisam de infraestrutura personalizada com orquestração gerenciada.
  • Jenkins: melhor para ambientes empresariais legados com investimentos existentes em plugins.

Para este artigo, os exemplos usam GitHub Actions porque é o mais amplamente adotado. Os padrões se traduzem diretamente para outros provedores com mudanças mínimas de sintaxe.

Uma vez escolhido o provedor, o primeiro estágio do pipeline a ser projetado é o portão de qualidade. Linting e testes são a barreira mínima que toda mudança deve superar antes de seguir para o deploy. A decisão chave de design é se esses portões executam antes do merge (verificações obrigatórias em pull requests) ou depois do merge (validação pós-merge). A resposta correta, para qualquer time que faz deploy em produção, é ambos — mas o portão pré-merge é o que protege a main.

Linting deve ser rápido. Verificação de tipos TypeScript, ESLint, Prettier — devem completar em segundos, não minutos. Se seu estágio de linting leva mais de sessenta segundos, você está ou analisando arquivos gerados ou executando regras que deveriam ser corrigidas automaticamente em vez de verificadas. Mantenha o linting focado em correção e aplicação de estilo que não podem ser corrigidos automaticamente, e execute formatadores como um hook de pré-commit.

Testes requerem um design de estágio mais cuidadoso. A suíte de testes unitários executa a cada push — deve ser rápida. A suíte de testes de integração executa quando os testes unitários passam — deve ser completa. A suíte de testes end-to-end executa quando a integração passa — deve ser confiável. Organizar testes nessas camadas e executá-los condicionalmente mantém o tempo total do pipeline previsível enquanto preserva a profundidade da cobertura.

name: CI — Lint e Teste

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  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 typecheck
      - run: npm run lint

  unit:
    needs: [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 test:unit -- --coverage

  integration:
    needs: [unit]
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: app_test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/app_test

Três padrões merecem destaque neste workflow. Primeiro, controle de concorrência com cancel-in-progress: se um desenvolvedor envia um segundo commit enquanto a primeira execução ainda está em andamento, a primeira execução é cancelada automaticamente. Isso evita desperdício de computação e mantém o feedback rápido. Segundo, a cadeia de dependência com needs: testes de integração só executam se os testes unitários passarem. Terceiro, contêineres de serviço para dependências de testes de integração, que sobem bancos de dados limpos sem infraestrutura externa.

Estágio 1: Build e cache — artefatos como unidade de deploy

Assim que os testes passam, o próximo estágio produz um artefato implantável. O artefato deve ser construído exatamente uma vez e promovido através dos ambientes. Reconstruir em cada ambiente é um antipadrão comum que introduz risco desnecessário: o código que passou nos testes pode diferir do código que chega à produção devido a diferenças específicas de ambiente no processo de build.

O estágio de build é onde o caching tem o maior impacto. Dependências — pacotes npm, camadas Docker, wheels Python — podem ser armazenadas em cache entre execuções para reduzir o tempo de build em uma ordem de grandeza. A estratégia para chaves de cache importa: inclua o hash do lockfile para que uma atualização de pacote invalide o cache, mas evite incluir o hash do commit, o que faria cada build perder o cache.

Deploys baseados em Docker adicionam uma camada extra de cache. O Docker layer caching (DLC) armazena em cache camadas intermediárias da imagem para que alterar um arquivo da aplicação não exija reinstalar pacotes do sistema. GitHub Actions suporta DLC através do docker/build-push-action com os parâmetros cache-from e cache-to apontando para um registro ou cache do GitHub Actions.

name: Build e Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Configurar Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Autenticar no registro de contêineres
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extrair SHA curto para tag
        id: vars
        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - name: Build e push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/myorg/myapp:latest
            ghcr.io/myorg/myapp:${{ steps.vars.outputs.sha_short }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

O artefato resultante — uma imagem Docker marcada com latest e o SHA do commit — é a unidade única de deploy. A tag SHA fornece rastreabilidade exata: dado um contêiner em execução, você pode olhar sua tag de imagem e saber exatamente qual commit a produziu. A tag latest fornece uma referência de conveniência para ambientes de desenvolvimento. Produção sempre implanta por SHA explícito, nunca por latest.

Estágio 2: Gerenciamento de ambiente e segredos — configurando sem comprometer

O gerenciamento de ambiente é onde a maioria dos erros de design de pipeline acontece. A abordagem ingênua — manter uma branch de desenvolvimento, uma branch de staging e uma branch de produção, cada uma com sua própria configuração de pipeline — cria problemas em cascata. Conflitos de merge, desvio de configuração e hotfixes que pulam ambientes são inevitáveis quando ambientes estão atrelados a branches.

A abordagem moderna é ambiente-por-deploy, não ambiente-por-branch. Uma única branch main produz um único artefato. Esse artefato é implantado em desenvolvimento para validação, promovido para staging para verificação pré-produção e promovido para produção quando estiver pronto. Se o artefato passa em desenvolvimento mas falha em staging, a correção vai no próximo commit — não em um hotfix que burla os portões de qualidade. Ambientes são deploys separados do mesmo artefato, não bases de código separadas.

O tratamento de segredos segue o mesmo princípio: injete, não incorpore. Segredos nunca devem aparecer em imagens Docker, arquivos de ambiente em repositórios ou logs de pipeline. GitHub Actions fornece segredos criptografados no nível de repositório e ambiente, e o GITHUB_TOKEN fornece credenciais de curta duração para acesso à API do GitHub sem armazenar um token de acesso pessoal.

Para organizações que superam o gerenciamento de segredos embutido, repositórios externos de segredos como HashiCorp Vault, AWS Secrets Manager ou Doppler fornecem capacidades adicionais: rotação automática, auditoria de log e políticas de acesso refinadas. O pipeline recupera segredos em tempo de execução através de requisições autenticadas em vez de armazená-los na configuração do pipeline.

name: Deploy para Ambiente

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Ambiente alvo"
        required: true
        type: choice
        options:
          - staging
          - production
      tag:
        description: "Tag da imagem Docker (SHA do commit)"
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    concurrency:
      group: deploy-${{ inputs.environment }}
      cancel-in-progress: false
    steps:
      - name: Deploy para Kubernetes
        uses: actions-hub/kubectl@master
        env:
          KUBE_CONFIG: ${{ secrets[format('KUBE_CONFIG_{0}', inputs.environment)] }}
        with:
          args: |-
            set image deployment/myapp \
              myapp=ghcr.io/myorg/myapp:${{ inputs.tag }} \
              -n ${{ inputs.environment }}

      - name: Verificar deploy
        run: |
          kubectl rollout status deployment/myapp \
            -n ${{ inputs.environment }} \
            --timeout=5m

      - name: Notificar em caso de falha
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "Deploy para ${{ inputs.environment }} falhou para a tag ${{ inputs.tag }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Este workflow usa segredos com escopo de ambiente: KUBE_CONFIG_STAGING e KUBE_CONFIG_PRODUCTION são armazenados separadamente, e o pipeline seleciona o correto com base no ambiente alvo. O grupo de concorrência no nível de ambiente garante que apenas um deploy execute por vez por ambiente, prevenindo condições de corrida durante o rollout. O comando rollout status bloqueia até que o Kubernetes reporte o deploy como saudável, e a notificação de falha só dispara se esta verificação falhar.

Estágio 3: Estratégias de deploy — blue-green, canário e rollback

Como você implanta é tão importante quanto o que você implanta. A estratégia mais simples — desligar a versão antiga, iniciar a nova — funciona para serviços de baixo risco com tempos de inicialização rápidos e sem estado. Para qualquer outra coisa, a estratégia de deploy determina se uma versão ruim afeta todos os usuários ou alguns, se o rollback leva segundos ou minutos, e se o time faz deploy às sextas-feiras à tarde ou apenas nas manhãs de terça.

Deploys blue-green mantêm dois ambientes idênticos. A qualquer momento, um ambiente (blue) atende o tráfego de produção enquanto o outro (green) executa a nova versão. Quando o ambiente green passa nas verificações de saúde, o balanceador de carga muda o tráfego de blue para green. O ambiente blue permanece pronto para rollback instantâneo: se a nova versão falhar, mudar o tráfego de volta leva uma única alteração de DNS ou balanceador de carga. O trade-off é o custo — dois ambientes completos significa gasto de infraestrutura em dobro durante a janela de troca.

Deploys canário direcionam uma pequena porcentagem do tráfego para a nova versão enquanto a versão antiga atende a maioria. A porcentagem canário aumenta incrementalmente à medida que a observabilidade confirma que a nova versão está saudável: um por cento, depois cinco, depois vinte e cinco, depois cem. Esta estratégia revela problemas com tráfego real antes do rollout completo e limita o raio de explosão quando problemas ocorrem. O trade-off é a complexidade — deploys canário exigem roteamento de tráfego sofisticado, integração de observabilidade e promoção ou rollback automatizados baseados em métricas.

A automação de rollback é a rede de segurança que todo pipeline precisa e a maioria dos pipelines não tem. Um rollback deve ser um único comando ou pressionar um botão, não um processo manual de reimplantar um artefato anterior. O requisito chave de design é que o artefato anterior esteja disponível: se suas imagens são marcadas com SHAs de commit e armazenadas em um registro que não coleta lixo de imagens não referenciadas, reverter significa reimplantar o último SHA conhecido como bom. Se suas imagens são marcadas de forma efêmera — latest, por exemplo — o rollback requer reconstruir a partir de um commit anterior, o que é mais lento e introduz o risco de que a reconstrução produza saída diferente.

O workflow de rollback acima consulta o histórico de rollout do Kubernetes para encontrar a revisão anterior quando nenhum alvo específico é fornecido, e reimplanta aquela imagem. Na prática, a maioria dos times também mantém uma lista de deploys bem-sucedidos recentes em um sistema de rastreamento de deploy para que o rollback nunca dependa do histórico interno do Kubernetes, que pode ser coletado como lixo após um número configurável de revisões.

Estágio 4: Monitoramento e melhoria contínua

Um deploy não termina quando o pipeline fica verde. Termina quando o time sabe que a versão implantada está saudável em produção. Isso requer integração de monitoramento em cada estágio do pipeline: verificações pré-deploy que atestam a saúde do ambiente antes de implantar, verificações pós-deploy que atestam a nova versão dentro de minutos após o rollout, e observabilidade contínua que fornece os dados para decisões de promoção canário e rollback.

A métrica de monitoramento mais importante para a eficácia do pipeline é a frequência de deploy. Se você implanta uma vez por mês, seu pipeline não melhorou sua capacidade de entrega — ele automatizou lançamentos pouco frequentes. O objetivo do design de pipeline CI/CD não são deploys automatizados, mas entrega contínua: a capacidade de lançar qualquer commit de forma segura e confiável a qualquer momento. A frequência de deploy é o melhor proxy para saber se um pipeline está atingindo este objetivo.

A segunda métrica mais importante é o tempo médio para recuperação (MTTR). Quanto tempo leva desde a detecção de um problema em produção até ter a correção implantada? O caminho de recuperação mais rápido é o rollback — segundos ou minutos. O seguinte mais rápido é um hotfix que burla o pipeline completo — minutos ou horas. O mais lento é o pipeline completo do commit ao deploy — que ainda deve ficar abaixo de trinta minutos. Se seu MTTR excede sua frequência de deploy, você tem um problema de design de pipeline.

A frequência de deploy diz se seu pipeline é rápido o suficiente. O tempo médio para recuperação diz se seu pipeline é seguro o suficiente. Se qualquer um dos números está piorando, o design do seu pipeline está indo na direção errada.
  • Monitore a frequência de deploy e o MTTR como indicadores antecedentes da saúde do pipeline, não métricas de vaidade.
  • Configure dashboards de monitoramento pós-deploy que mostrem taxas de erro, latência e tráfego por versão — não apenas por ambiente.
  • Automatize a promoção canário com base no orçamento de erro: promova quando a taxa de erro permanecer abaixo do limite por N minutos, reverta automaticamente quando exceder.
  • Realize post-mortems sem culpados para cada deploy que falhou e atualize o pipeline para prevenir recorrência.
  • Revise a duração do pipeline semanalmente. Se qualquer estágio leva mais tempo que o necessário, invista em caching, paralelismo ou redesenho do estágio antes de adicionar mais funcionalidades ao pipeline.

O pipeline em si deve estar sujeito ao mesmo ciclo de melhoria que qualquer outro produto. Agende revisões regulares do pipeline onde o time examine duração, taxa de falha e pontos de atrito. Trate estágios lentos como bugs com a mesma prioridade que bugs da aplicação. Quando um desenvolvedor diz que o pipeline está muito lento, acredite nele — o pipeline existe para servir o desenvolvedor, não o contrário.

Um pipeline CI/CD bem projetado é invisível. Commits fluem através dele sem que o desenvolvedor pense sobre isso. Deploys acontecem múltiplas vezes ao dia sem incidentes. Rollbacks, quando necessários, são rápidos e tranquilos. O pipeline desaparece no fundo do trabalho de desenvolvimento porque funciona de forma confiável e previsível. Esta invisibilidade é o sinal de bom design — não porque o pipeline é simples, mas porque a complexidade é gerenciada bem o suficiente para que o desenvolvedor não precise pensar sobre isso.

Construir esse pipeline requer design intencional, iteração e investimento. Mas o retorno — feedback mais rápido, deploys mais seguros, maior confiança do desenvolvedor — se acumula a cada commit.