Pipeline CI/CD Python: 5 stages do zero em 8 dias, 13 ADRs registrados
OCR jurídico FastAPI sem CI/CD virou 5 stages GitLab (validate→test→review→build→deploy), Kaniko + GitLab Registry, 13 ADRs, smoke /health 200.
DevOps Engineer (Estagiário) · Consultoria AWS Select Partner
- GitLab CI
- Python 3
- FastAPI
- Docker
- Kaniko
- Proxmox VE
- Ubuntu 24.04
TL;DR
- Pipeline GitLab CI com 5 stages (validate → test → review → build → deploy) entregue do zero para projeto Python FastAPI sem CI/CD prévio.
- 93% de entrega (14/15 tarefas) em 9 sessões / ~8 dias, com 13 ADRs registrados incluindo uma revisada in-flight quando a decisão original não sobreviveu ao contato com a realidade.
- Build reprodutível via Kaniko e GitLab Container Registry (após migração mid-project saindo do Docker Hub), deploy por SSH em VM Proxmox com smoke test
/health200 //docs200.
Contexto
O projeto era um serviço Python de OCR para documentos jurídicos — FastAPI na borda, embeddings e busca via pgvector no Postgres — mantido por um dev único numa VM Proxmox (2 vCPU, 4GB RAM, 30GB disco, Ubuntu 24.04.3 LTS). O cliente é da área jurídica; o serviço lê documentos, extrai entidades nomeadas e faz busca semântica.
O problema não era ausência de código — era ausência de processo de entrega. Cada merge dependia de revisão manual no GitLab sem gate automático; cada deploy era SSH, git pull, rebuild local, reinício do systemd. Builds não eram reprodutíveis: a imagem no servidor dependia do estado da VM no momento do build.
A entrega aconteceu em 9 sessões focadas em ~8 dias (23/fev → 02/mar de 2026), com um objetivo explícito: deixar o pipeline “verde-todas-stages” antes do deploy de próxima feature, sem congelar o fluxo do time que já mergia MRs.
Ação
O escopo foi fatiado em 15 tarefas e 5 stages do pipeline. Três frentes práticas.
Stages do pipeline
# .gitlab-ci.yml — trecho ilustrativo, anonimizado
stages:
- validate
- test
- review
- build
- deploy
validate:
stage: validate
image: python:3.12-slim
script:
- pip install --no-cache-dir ruff==0.5.0
- ruff check app/
- python -c "import tomllib; tomllib.load(open('pyproject.toml','rb'))"
# tempo real observado: ~113s
sast:
stage: review
image: registry.gitlab.com/security-products/sast:latest
script:
- /analyzer run
artifacts:
reports:
sast: gl-sast-report.json
# tempo real observado: ~243s
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.20.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
Tempos reais observados no runner: validate 113s, SAST 243s, mr-review 111s. Runner rodava em VM dedicada com tag próprio para isolar o contexto CI do resto da infra do cliente.
Build com Kaniko (e a migração de registry)
A escolha de Kaniko em vez de docker build foi deliberada: runner sem --privileged (regra de segurança do cliente), sem Docker daemon exposto, build containerizado honesto. Em contrapartida, Kaniko não tem cache local entre runs — o primeiro build fica mais lento até subir cache em registry.
Uma das decisões foi revisada in-flight: a primeira ADR apontava Docker Hub como registry por familiaridade. Depois de dois builds, ficou claro que:
- Limites de pull anônimo iam bater em horário de pico.
- Credencial compartilhada em secret CI significava mais uma rotação para gerenciar.
- O próprio GitLab já oferecia Container Registry integrado com IAM do projeto.
A ADR foi reescrita, não apagada — ficou registrado o “porque mudamos” para o próximo dev não refazer a mesma pergunta.
Deploy e debugging
Deploy final é via SSH key dedicada com ssh-agent no runner + docker compose pull && docker compose up -d na VM alvo. Smoke test roda logo em seguida:
# smoke-test.sh — executado no stage deploy após docker compose up
set -euo pipefail
curl --fail --max-time 10 http://localhost:8000/health
curl --fail --max-time 10 http://localhost:8000/docs
echo "smoke test: /health 200 /docs 200"
Até chegar nesse deploy limpo foram 4 iterações de debug — anotadas numa seção deploy-troubleshooting.md que ficou no repo:
- Primeira versão do job usava
sshinline semssh-agent: host key não cacheava, job falhava na segunda run. docker compose pullsem login autenticado no registry GitLab:manifest unknown..envlido no host não tinha as vars que odocker-compose.ymlreferenciava: serviço subia mas caía no health.- Health check batia em
localhost:8000enquanto compose mapeava127.0.0.1:8001(conflito de porta com outro serviço legado na VM).
Nenhum desses é exótico. Todos estão documentados como sequência real, porque o valor pra quem vai vir depois é ver a ordem dos erros, não o pipeline “pronto”.
Trade-offs e o que NÃO fiz
- Kaniko em vez de
docker build: ganhei reprodutibilidade e segurança (runner sem--privileged); perdi cache local rápido. Vale a pena enquanto a política de segurança do cliente proibir Docker daemon exposto no runner — se mudar, reavaliar. - GitLab Container Registry em vez de Docker Hub: decisão revisada in-flight (1 ADR reescrita). O custo foi uma tarde refazendo a parte de auth; o ganho foi eliminar uma rotação de credencial e sumir com risco de rate-limit.
- Deploy por SSH em vez de
kubectl/Helm: só existe uma VM alvo e o cliente não opera Kubernetes. Introduzir k8s pra isso seria overhead puro. Limite escrito: se virarem 3+ VMs ou vier HA no escopo, migrar para Ansible antes de pensar em k8s. - SAST com 293 findings “high”: a maioria era falso positivo de dependências em cache (Python transitive deps). Em vez de suprimir em bloco, a decisão foi: criar baseline, triar em janela dedicada e só então configurar
allow_failure: false. Entregar SAST hoje com gate frouxo é melhor que prometer gate estrito e não conseguir mergiar nada. - Não escrevi testes de integração nessa fase: só unit + lint. A cobertura de integração ficou explícita como próximo ciclo, não como “esqueci”.
- Não automatizei rollback: deploy novo sobe, deploy velho fica como tag
:previousno registry — rollback édocker compose pull <:previous> && up -d. Suficiente pra escala do projeto; automatizar seria prematuro.
Resultado
O estado final: pipeline verde nos 5 stages em MR normal, imagem versionada por CI_COMMIT_SHORT_SHA em GitLab Container Registry, deploy automatizado com smoke test retornando /health 200 e /docs 200, e 13 ADRs registrados explicando cada decisão não-óbvia.
Sendo honesto sobre o que não há: não existe baseline de “lead time antes” — o deploy manual nunca foi cronometrado formalmente. O ganho visível está na reprodutibilidade (qualquer MR passa pelos mesmos gates), não num gráfico “antes vs depois” que eu não tenho como provar.
A entrega fechou em 93% (14/15 tarefas) porque a 15ª — rollback automatizado — foi decisão consciente de deixar fora: o custo de implementar contra uma VM única não compensava o ganho marginal sobre o rollback manual de 2 comandos. Escrito na ADR final, não esquecido.