Anatomia de um pipeline CI
Esta aula explora a anatomia de um pipeline de Integração Contínua (CI), detalhando seus estágios, triggers, artefatos e cache. Você aprenderá como estruturar um pipeline eficiente, desde a execução de builds até a distribuição de artefatos, com exemplos práticos em YAML.
Um pipeline de CI é a espinha dorsal da automação em DevOps: ele orquestra as etapas necessárias para transformar código-fonte em um artefato pronto para deploy, garantindo qualidade a cada commit. Entender sua anatomia — estágios, triggers, artefatos e cache — é essencial para projetar pipelines eficientes, rápidos e confiáveis.
Nesta aula, vamos dissecar cada componente, mostrando como configurá-los em ferramentas como GitLab CI, GitHub Actions ou Jenkins. Usaremos exemplos em YAML (formato padrão) para ilustrar as práticas recomendadas.
Estágios
Os estágios representam as fases lógicas do pipeline, executadas em sequência (ou em paralelo quando configuradas). Cada estágio contém um ou mais jobs que realizam tarefas específicas. A separação em estágios permite controle de fluxo, fail fast e reutilização de cache.
Por exemplo, um pipeline típico de CI pode ter os estágios: build, test, package e deploy. Se o build falhar, os estágios seguintes não são executados, economizando recursos. Em ferramentas como GitLab CI, você define estágios no topo do arquivo .gitlab-ci.yml:
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "Compilando..."
- make build
test-job:
stage: test
script:
- echo "Rodando testes..."
- make test
deploy-job:
stage: deploy
script:
- echo "Deploy para produção"
Cada job é associado a um estágio via a diretiva stage. Jobs de um mesmo estágio podem ser executados em paralelo (se a ferramenta suportar), acelerando o pipeline. É importante manter os estágios coesos e com responsabilidades claras.
Triggers
Triggers (ou gatilhos) definem quando o pipeline deve ser executado. Eles podem ser baseados em eventos como push de commits, abertura de pull requests, agendamento temporal (cron) ou até mesmo chamadas de API externas. A escolha correta dos triggers evita execuções desnecessárias e garante que o pipeline rode apenas quando relevante.
No GitLab CI, os triggers são configurados com a diretiva only ou rules (recomendada). Exemplo com rules:
job:
script: echo "Executando job"
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
when: always
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
No GitHub Actions, os triggers são definidos no campo on:
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * 1' # toda segunda-feira às 2h
Triggers também podem ser combinados com condições (como nome de branch, tags ou paths) para filtrar ainda mais as execuções. Por exemplo, executar apenas quando arquivos em src/ forem alterados.
Artefatos
Artefatos são arquivos gerados durante o pipeline que são preservados para uso em estágios posteriores ou para download. Eles podem ser binários compilados, relatórios de teste, imagens Docker, pacotes, etc. A gestão de artefatos é crucial para pipelines multi-estágio e para auditoria.
No GitLab CI, você define artefatos com a diretiva artifacts:
build-job:
stage: build
script:
- mkdir build
- echo "binary" > build/app.bin
artifacts:
paths:
- build/
expire_in: 1 week
deploy-job:
stage: deploy
script:
- ls build/ # artefato disponível aqui
needs:
- build-job
No GitHub Actions, usa-se a ação actions/upload-artifact e actions/download-artifact:
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: my-artifact
path: build/
Artefatos podem ter políticas de expiração (para liberar espaço) e podem ser compartilhados entre jobs do mesmo workflow ou entre workflows diferentes. É boa prática incluir metadados (como número do commit) no nome do artefato para rastreabilidade.
Cache
O cache acelera pipelines reutilizando dependências (como pacotes npm, gems Ruby ou bibliotecas Python) que não mudam frequentemente. Diferente dos artefatos, o cache não é garantido e pode ser invalidado; ele é uma otimização, não uma fonte de verdade.
No GitLab CI, o cache é configurado com a diretiva cache:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
job:
script:
- npm install
No GitHub Actions, usa-se a ação actions/cache:
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
A chave do cache (key) é fundamental: ela determina quando o cache é válido. Idealmente, deve incluir um hash do arquivo de lock (ex.: package-lock.json). Se o arquivo mudar, uma nova chave é gerada e um novo cache é criado. O cache é restaurado automaticamente antes do job e atualizado após a execução (se os paths existirem).
É importante não confundir cache com artefatos: cache é para dependências temporárias (pode ser limpo a qualquer momento), enquanto artefatos são para resultados finais que precisam ser preservados.
Boas Práticas
1. Mantenha os estágios enxutos e com responsabilidade única.
2. Use triggers condicionais para evitar execuções desnecessárias.
3. Defina políticas de expiração para artefatos (ex.: 1 semana).
4. Cacheie apenas diretórios de dependências, não artefatos de build.
5. Utilize chaves de cache baseadas em hash para invalidar automaticamente.
6. Documente o pipeline no repositório (ex.: README com diagrama).
Referências
- GitLab CI Pipelines Documentation
- GitHub Actions Workflow Syntax
- Jenkins Pipeline Documentation
- CircleCI Pipelines Overview
- AWS CodePipeline User Guide
- Continuous Integration (Martin Fowler)
Exercícios
- Explique a diferença entre artefatos e cache em um pipeline CI, e dê um exemplo de uso para cada um.
- Escreva um trecho de configuração de pipeline (em YAML) que execute testes apenas quando houver push na branch main, e faça deploy manualmente.
- Qual é a finalidade da diretiva
needsem um pipeline? Dê um exemplo. - Por que é importante definir uma chave de cache baseada em hash de um arquivo de lock? O que acontece se a chave for estática?
- Projete um pipeline com três estágios (build, test, deploy) onde o estágio test executa em paralelo com build, e deploy só roda após ambos terminarem. Use
needse estágios.
stages:
- test
- deploy
test-job:
stage: test
script:
- echo "Running tests"
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
deploy-job:
stage: deploy
script:
- echo "Deploying"
when: manual
needs permite que um job execute antes mesmo de todos os jobs do estágio anterior terminarem, desde que os jobs dos quais ele depende tenham sido concluídos. Exemplo: se o job de deploy precisa apenas do job de build (e não de test), pode-se usar needs: [build] para iniciar mais cedo.
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "Build"
test-job:
stage: test
script:
- echo "Test"
needs: [] # não depende de nenhum job do estágio anterior, pode iniciar imediatamente
deploy-job:
stage: deploy
script:
- echo "Deploy"
needs:
- build-job
- test-job