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
- GitHub Actions Documentation — Documentação oficial do GitHub Actions com exemplos detalhados
- GitLab CI/CD Documentation — Guia completo de CI/CD com GitLab
- The DevOps Handbook — Livro referência sobre práticas DevOps e pipelines
- 12 Factor App — Metodologia essencial para aplicações cloud-native com boas práticas de configuração
- Kubernetes Deployment Strategies — Documentação sobre estratégias de deploy seguras