DevOps Admin

Dominando Fundamentos de CI/CD: Pipelines, Stages e Boas Práticas em Projetos Reais Já leu

Entendendo CI/CD: Do Conceito à Prática CI/CD é um conjunto de práticas que automatiza a entrega de software, eliminando etapas manuais e reduzindo erros humanos. O acrônimo representa Continuous Integration (Integração Contínua) e Continuous Delivery ou Continuous Deployment (Entrega ou Implantação Contínua). A diferença entre eles é sutil mas importante: CI/CD é o conjunto completo, CI é a prática de integrar código frequentemente, e CD pode significar tanto entregar código pronto para produção quanto implantar automaticamente. A razão pela qual CI/CD é tão valorizado na indústria é simples: reduz o tempo entre a escrita do código e sua chegada aos usuários, aumenta a confiabilidade do software e permite feedback rápido sobre problemas. Em um projeto tradicional sem CI/CD, um desenvolvedor pode trabalhar por semanas em uma branch isolada, e quando tenta integrar seu código, descobre conflitos complexos e bugs não detectados. Com CI/CD, pequenas mudanças são integradas diariamente, testadas automaticamente e, se passarem, podem ir para produção em horas. O

Entendendo CI/CD: Do Conceito à Prática

CI/CD é um conjunto de práticas que automatiza a entrega de software, eliminando etapas manuais e reduzindo erros humanos. O acrônimo representa Continuous Integration (Integração Contínua) e Continuous Delivery ou Continuous Deployment (Entrega ou Implantação Contínua). A diferença entre eles é sutil mas importante: CI/CD é o conjunto completo, CI é a prática de integrar código frequentemente, e CD pode significar tanto entregar código pronto para produção quanto implantar automaticamente.

A razão pela qual CI/CD é tão valorizado na indústria é simples: reduz o tempo entre a escrita do código e sua chegada aos usuários, aumenta a confiabilidade do software e permite feedback rápido sobre problemas. Em um projeto tradicional sem CI/CD, um desenvolvedor pode trabalhar por semanas em uma branch isolada, e quando tenta integrar seu código, descobre conflitos complexos e bugs não detectados. Com CI/CD, pequenas mudanças são integradas diariamente, testadas automaticamente e, se passarem, podem ir para produção em horas.

O Fluxo Básico

Imagine este cenário: você faz um commit em seu repositório. Imediatamente, um servidor observa essa mudança, baixa o código, compila, executa testes automatizados, analisa qualidade de código, e se tudo passar, empacota a aplicação em um container ou artefato pronto para ser executado. Tudo isso sem nenhuma intervenção manual. Este é o CI/CD em ação.

Pipelines: A Coluna Vertebral da Automação

Um pipeline é uma série de passos executados em sequência ou paralelo para transformar código-fonte em um produto entregável. Cada passo é uma etapa específica que valida, testa ou prepara o código. A beleza de um pipeline é sua previsibilidade: você sabe exatamente o que acontece quando faz um commit, porque está tudo documentado e automatizado.

Anatomia de um Pipeline

Um pipeline típico possui três partes principais: source (onde o código vive), build (onde o código é compilado e testado), e deploy (onde o código é colocado em produção). Cada uma dessas partes pode ter múltiplos passos internos. O source é geralmente um repositório Git. O build é onde a mágica acontece: dependências são baixadas, testes rodam, artefatos são criados. O deploy leva esse artefato para servidores reais.

Vamos ver um exemplo concreto com GitHub Actions, que é gratuito e integrado ao GitHub:

name: CI/CD Pipeline

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'

    - name: Install Dependencies
      run: npm install

    - name: Run Unit Tests
      run: npm run test

    - name: Run Linter
      run: npm run lint

    - name: Build Application
      run: npm run build

    - name: Upload Artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-output
        path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/download-artifact@v3
      with:
        name: build-output

    - name: Deploy to Production
      run: |
        echo "Deploying to production..."
        # Aqui você colocaria seu script de deploy real

Observe a estrutura: primeiro fazemos checkout do código, configuramos o Node.js, instalamos dependências, rodamos testes, linting, build, e se tudo passar, armazenamos os artefatos. Depois, em um job separado que só executa na branch main, fazemos o deploy. Isso garante que apenas código que passou em todos os testes vai para produção.

Triggers: Quando o Pipeline Executa

Triggers definem quando um pipeline começa a rodar. No exemplo acima, o pipeline executa quando há um push nas branches main ou develop, ou quando há um pull request. Você pode ser mais específico ainda: executar apenas quando arquivos em diretórios específicos mudam, em horários agendados, ou manualmente.

on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - '.github/workflows/**'
  schedule:
    - cron: '0 2 * * *'  # Executa diariamente às 2 da manhã
  workflow_dispatch:  # Permite execução manual

Stages: Dividindo Responsabilidades

Um stage é um agrupamento lógico de passos dentro de um pipeline. Enquanto um passo é uma ação individual (como "rodar testes"), um stage é uma fase maior do pipeline que agrupa relacionados. Stages são cruciais para organização e para entender em qual fase o pipeline falhou.

Estrutura de Stages Recomendada

A maioria dos pipelines modernos usa uma estrutura de stages assim: Checkout (preparação), Build (compilação), Test (testes unitários, integração), Quality (análise estática, cobertura), Package (criar artefatos), Deploy Staging (ambiente de teste), Smoke Tests (testes rápidos pós-deploy), Deploy Production (produção real).

Vamos ver isso com GitLab CI, que é excelente para stage management:

stages:
  - build
  - test
  - quality
  - package
  - deploy_staging
  - deploy_production

variables:
  DOCKER_IMAGE: registry.gitlab.com/mycompany/myapp

build_app:
  stage: build
  image: node:18-alpine
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour
  only:
    - branches

unit_tests:
  stage: test
  image: node:18-alpine
  dependencies:
    - build_app
  script:
    - npm install
    - npm run test:unit
  coverage: '/Coverage: \d+\.\d+%/'

integration_tests:
  stage: test
  image: node:18-alpine
  dependencies:
    - build_app
  script:
    - npm run test:integration
  services:
    - postgres:14
    - redis:7

sonarqube_analysis:
  stage: quality
  image: sonarsource/sonar-scanner-cli:latest
  script:
    - sonar-scanner -Dsonar.projectKey=myapp -Dsonar.host.url=$SONAR_HOST
  only:
    - merge_requests

package_docker:
  stage: package
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
    - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
    - docker push $DOCKER_IMAGE:latest
  only:
    - main

deploy_staging:
  stage: deploy_staging
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA
  environment:
    name: staging
    url: https://staging.myapp.com
  only:
    - main

smoke_tests_staging:
  stage: deploy_staging
  image: curlimages/curl:latest
  script:
    - curl -f https://staging.myapp.com/health || exit 1
    - curl -f https://staging.myapp.com/api/version || exit 1
  when: on_success
  retry:
    max: 3
    when: script_failure

deploy_production:
  stage: deploy_production
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA --namespace=production
  environment:
    name: production
    url: https://myapp.com
  only:
    - main
  when: manual

Observe como os stages são claramente separados. O estágio test tem dois jobs que rodam em paralelo (unit_tests e integration_tests). O estágio deploy_production tem when: manual, o que significa que alguém precisa clicar um botão para deployer para produção — uma boa prática de segurança.

Dependências Entre Stages

Stages podem depender um do outro. Um stage só inicia quando todos os jobs do stage anterior completam com sucesso. Se um test falhar, o package não executa. Se o package falhar, nenhum deploy acontece. Isso garante que código quebrado nunca chega a produção.

Boas Práticas Essenciais

Ter um pipeline é bom. Ter um pipeline bem feito é excelente. Existem padrões e práticas que separam empresas que entregam qualidade daquelas que entregam caos frequentemente.

1. Feedback Rápido

Um pipeline que leva 2 horas para rodar destrói a produtividade. Desenvolvedores precisam de feedback em minutos, não horas. A estratégia é paralelizar: coloque testes independentes em jobs paralelos, divida testes em suites que rodem separadamente. Se seu pipeline leva muito tempo, identifique os gargalos e otimize ou divida.

# Exemplo: testes em paralelo
test_unit:
  stage: test
  script:
    - npm run test:unit
  parallel: 5  # Divide os testes em 5 execuções paralelas

test_integration:
  stage: test
  script:
    - npm run test:integration
  parallel: 3

2. Idempotência: Deploy Deve Ser Seguro

Um deploy deve ser seguro de rodar múltiplas vezes. Se você executa o deploy duas vezes seguidas, o resultado final deve ser o mesmo. Isso significa usar declarative configuration (Kubernetes, Terraform) ao invés de scripts imperativos que modificam estado.

# Terraform - declarativo e idempotente
resource "aws_ecs_service" "app" {
  name            = "myapp"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 3
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = aws_subnet.private[*].id
    security_groups  = [aws_security_group.app.id]
    assign_public_ip = false
  }
}

Se você executar isso duas vezes, o Terraform reconhece que o estado já existe e faz nada. Isso é seguro.

3. Controle de Acesso e Segredos

Nunca coloque senhas, tokens, chaves de API em repositórios. Use secrets management. GitHub Actions, GitLab CI, e Jenkins têm formas de armazenar segredos com segurança.

# GitHub Actions com secrets
- name: Deploy
  env:
    DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
    API_KEY: ${{ secrets.EXTERNAL_API_KEY }}
  run: ./deploy.sh

Os secrets são mascarados nos logs (aparecem como ***), então você não expõe credenciais acidentalmente quando compartilha logs com o time.

4. Rastreabilidade e Auditoria

Cada deploy deve ser rastreável. Você deve saber exatamente qual commit foi deployado, por quem, quando, e para onde. Armazene essa informação.

#!/bin/bash
# Script de deploy com auditoria

COMMIT_SHA=$(git rev-parse --short HEAD)
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
DEPLOYED_BY=${CI_COMMIT_AUTHOR:-unknown}
DEPLOYMENT_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)

echo "Deployment Log"
echo "=============="
echo "Commit: $COMMIT_SHA"
echo "Message: $COMMIT_MESSAGE"
echo "By: $DEPLOYED_BY"
echo "Time: $DEPLOYMENT_TIME"
echo "Environment: $DEPLOY_ENV"

# Armazene isso em um banco de dados ou arquivo de auditoria

5. Ambiente de Staging Idêntico à Produção

Erros só aparecem em produção se seu staging não é idêntico. Use containers (Docker) para garantir que o mesmo artefato roda em qualquer lugar. Tenha dados de teste realistas em staging.

# Dockerfile - mesmo para todos os ambientes
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY dist ./dist

EXPOSE 3000

CMD ["node", "dist/server.js"]

O container roda igual em seu notebook, em staging, e em produção. Nenhuma surpresa.

6. Testes em Múltiplas Camadas

Não confie apenas em testes unitários. Uma pirâmide de testes é: muitos testes unitários (rápidos, baratos), alguns testes de integração (validam componentes juntos), poucos testes E2E (validam o fluxo completo do usuário).

// Teste unitário - função pura
describe('calculatePrice', () => {
  it('should apply discount correctly', () => {
    const price = calculatePrice(100, 0.1);
    expect(price).toBe(90);
  });
});

// Teste de integração - com banco de dados
describe('User Service', () => {
  it('should save and retrieve user', async () => {
    const user = await userService.create({ name: 'John' });
    const retrieved = await userService.getById(user.id);
    expect(retrieved.name).toBe('John');
  });
});

// Teste E2E - browser automatizado
describe('Login Flow', () => {
  it('should login user and redirect to dashboard', async () => {
    await page.goto('https://app.com/login');
    await page.fill('[name="email"]', 'user@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForNavigation();
    expect(page.url()).toContain('/dashboard');
  });
});

7. Configuração por Ambiente

Sua aplicação provavelmente precisa de configurações diferentes em dev, staging e produção (URLs, log levels, features flags). Use variáveis de ambiente, não hardcode.

// config.js
const config = {
  development: {
    apiUrl: 'http://localhost:3001',
    logLevel: 'debug',
    enableFeatures: ['beta-feature'],
  },
  staging: {
    apiUrl: 'https://api-staging.myapp.com',
    logLevel: 'info',
    enableFeatures: ['beta-feature'],
  },
  production: {
    apiUrl: 'https://api.myapp.com',
    logLevel: 'warn',
    enableFeatures: [],
  },
};

module.exports = config[process.env.NODE_ENV || 'development'];

8. Monitoramento e Rollback Automático

Após um deploy, monitore a aplicação. Se taxa de erros subir, métricas caírem, ou alertas disparam, faça rollback automático para a versão anterior. Isso protege usuários de deploys ruins.

# Exemplo conceitual de rollback automático
deploy_and_monitor:
  stage: deploy
  script:
    - kubectl rollout status deployment/myapp  # Aguarda conclusão do deploy
    - sleep 60  # Aguarda 1 minuto
    - ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query?query=error_rate | jq '.data.result[0].value[1]')
    - |
      if [ $(echo "$ERROR_RATE > 5" | bc) -eq 1 ]; then
        echo "Error rate above 5%, rolling back..."
        kubectl rollout undo deployment/myapp
        exit 1
      fi

Conclusão

Aprendemos que CI/CD é muito mais que apenas rodar testes automaticamente. É uma cultura e um conjunto de práticas que aceleram entrega, aumentam confiabilidade e reduzem estresse de deployments. Os três pontos principais são: Pipelines bem estruturados automatizam e organizam o fluxo de código, garantindo que cada commit passa por validações rigorosas antes de chegar aos usuários. Stages dividem responsabilidades e permitem feedback granular, dizendo exatamente em qual fase algo falhou. Boas práticas como feedback rápido, idempotência, segredos seguros e testes em múltiplas camadas transformam um pipeline básico em um sistema confiável que a empresa inteira pode confiar.

Referências


Artigos relacionados