Boas práticas de CI
Esta aula aborda as boas práticas essenciais para implementar integração contínua (CI) de forma eficaz, incluindo builds rápidos, falha precoce, pipeline como código e idempotência. Com exemplos práticos em bash e GitHub Actions, o aluno aprenderá a otimizar pipelines e garantir confiabilidade.
Integração Contínua (CI) é uma prática fundamental em DevOps que consiste em integrar código frequentemente, executando builds e testes automatizados a cada commit. Para que a CI seja realmente eficiente, é necessário adotar boas práticas que garantam rapidez, confiabilidade e manutenibilidade. Nesta aula, exploraremos quatro pilares: builds rápidos, falhe cedo, pipeline como código e idempotência. Cada um desses conceitos contribui para um pipeline de CI robusto, que detecta problemas rapidamente e permite que as equipes entreguem software com qualidade.
Vamos mergulhar em cada prática, com exemplos práticos usando bash e GitHub Actions, mostrando como implementá-las no dia a dia.
Builds rápidos
Builds rápidos são essenciais para manter o ciclo de feedback curto. Se um build demora muito, os desenvolvedores tendem a integrar com menos frequência, o que aumenta o risco de conflitos e bugs. O objetivo é que o pipeline completo leve minutos, não horas. Para isso, é importante otimizar cada etapa: usar cache de dependências, paralelizar tarefas, e escolher ferramentas eficientes.
Por exemplo, no GitHub Actions, podemos usar cache para dependências do Node.js:
name: CI Rápido
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Instalar dependências
run: npm ci
- name: Build
run: npm run buildOutra técnica é paralelizar testes. Em vez de executar todos os testes sequencialmente, divida-os em jobs paralelos. Exemplo com matrix:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 18]
steps:
- uses: actions/checkout@v4
- name: Testar com Node ${{ matrix.node-version }}
run: npm testAlém disso, evite builds monolíticos: se o projeto tem múltiplos componentes, considere pipelines separados ou dependências seletivas.
Falhe cedo
O princípio de "falhe cedo" significa que o pipeline deve executar as etapas mais rápidas e que detectam erros comuns primeiro. Assim, um commit problemático é rejeitado rapidamente, sem desperdiçar tempo em etapas mais longas. Por exemplo, linting e verificação de tipo são rápidos e devem vir antes dos testes de integração.
Exemplo de ordem de etapas em um pipeline:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint
run: npm run lint
- name: Testes unitários
run: npm run test:unit
- name: Build
run: npm run build
- name: Testes de integração
run: npm run test:integrationSe o lint falhar, o pipeline para imediatamente, economizando tempo de build e testes. Outra prática é usar gates de qualidade: por exemplo, se a cobertura de testes cair abaixo de um limite, falhe o pipeline. Ferramentas como SonarQube podem ser integradas para análises estáticas.
No bash, podemos simular uma falha precoce com verificações condicionais:
#!/bin/bash
set -e
echo "Executando lint..."
npm run lint || { echo "Lint falhou"; exit 1; }
echo "Executando testes..."
npm testPipeline como código
Pipeline como código (Pipeline as Code) consiste em definir todo o pipeline de CI/CD em arquivos versionados, geralmente YAML ou JSON, que ficam no repositório. Isso traz rastreabilidade, revisão de código e consistência. Ferramentas como GitHub Actions, GitLab CI/CD e Jenkins declarativo suportam essa abordagem.
Vantagens: qualquer alteração no pipeline é submetida a code review, o histórico de mudanças é mantido, e é fácil reproduzir o pipeline localmente. Exemplo de pipeline como código no GitHub Actions:
name: Pipeline como Código
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: make build
- name: Test
run: make testNo GitLab CI, o arquivo .gitlab-ci.yml define stages:
stages:
- build
- test
build_job:
stage: build
script:
- make build
test_job:
stage: test
script:
- make testBoa prática: manter o pipeline simples e modular, reutilizando trechos com âncoras YAML ou templates.
Idempotência
Idempotência significa que executar o mesmo pipeline várias vezes com a mesma entrada produz o mesmo resultado. Isso é crucial para garantir que builds e testes sejam reproduzíveis, independentemente de execuções anteriores. Para alcançar idempotência, evite dependências de estado externo, como variáveis de ambiente mutáveis ou bancos de dados compartilhados.
Práticas: sempre instalar dependências a partir de lockfiles (package-lock.json, Gemfile.lock); usar containers Docker para ambientes consistentes; e garantir que scripts não dependam de arquivos temporários deixados por execuções anteriores.
Exemplo de script idempotente em bash:
#!/bin/bash
# Idempotente: sempre instala dependências, mas usa cache
npm ci || npm install
# Compila o projeto
npm run build
# Remove artefatos anteriores (se houver)
rm -rf dist/old
No GitHub Actions, a idempotência é facilitada pelo uso de ações oficiais e cache. Exemplo com Docker:
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:18
steps:
- uses: actions/checkout@v4
- name: Instalar dependências
run: npm ci
- name: Build
run: npm run buildTestar a idempotência: execute o pipeline duas vezes seguidas e verifique se o resultado é o mesmo (mesmo hash de artefatos, mesma saída).
Boas práticas adicionais
Além dos quatro pilares, outras práticas importantes incluem: notificações rápidas (Slack, email) quando o pipeline falha; manter o pipeline verde (nunca permitir falhas conhecidas); e usar ambientes efêmeros para testes de integração. Lembre-se: CI é um processo contínuo de melhoria, então revise e otimize periodicamente.
Referências
- GitHub Actions - Workflow syntax
- GitLab CI/CD YAML
- Martin Fowler - Continuous Integration
- Jenkins Pipeline as Code
- The Twelve-Factor App - Build, Release, Run
Exercícios
Explique por que builds rápidos são importantes para a produtividade da equipe. Cite duas técnicas para acelerar o pipeline.
✓ Resposta: Builds rápidos reduzem o ciclo de feedback, permitindo que os desenvolvedores integrem com mais frequência e detectem problemas rapidamente. Duas técnicas: usar cache de dependências (ex: actions/cache) e paralelizar testes com matrix strategies.No princípio "falhe cedo", qual etapa deve vir primeiro: testes de integração ou lint? Justifique.
✓ Resposta: Lint deve vir primeiro, pois é mais rápido e detecta erros de sintaxe ou estilo. Se falhar, evita executar testes de integração que são mais demorados.O que significa "pipeline como código"? Dê um exemplo de arquivo de pipeline (em YAML) que execute build e test.
✓ Resposta: É definir o pipeline em arquivos versionados (YAML, JSON) no repositório. Exemplo (GitHub Actions):name: CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: make build - run: make testPor que a idempotência é importante em pipelines de CI? Como garantir que um script bash seja idempotente?
✓ Resposta: Idempotência garante que múltiplas execuções do pipeline com a mesma entrada produzam o mesmo resultado, essencial para reprodutibilidade. Para scripts bash, use lockfiles (npm ci), evite dependências de estado externo e limpe artefatos temporários.Escreva um trecho de pipeline (GitHub Actions) que implemente as quatro boas práticas discutidas: builds rápidos (cache), falhe cedo (lint antes de test), pipeline como código (YAML versionado) e idempotência (uso de lockfile).
✓ Resposta:name: CI Completo on: [push] jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Cache node_modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - name: Lint run: npm run lint - name: Test run: npm test