Pular para o conteúdo

Build reprodutível

Avançado14 min de leituraentrega

Um build é reprodutível quando a mesma entrada — mesmo commit, mesmas dependências, mesmas instruções — produz exatamente o mesmo artefato, bit a bit, hoje e daqui a seis meses, na sua máquina e no CI.

A maioria dos builds não é. Duas execuções do mesmo commit produzem imagens com digests diferentes, e quase ninguém percebe — até o dia em que a diferença importa.

Reprodutibilidade responde a uma pergunta que nenhuma assinatura responde sozinha: este artefato corresponde mesmo a este código-fonte?

Se o build é reprodutível, qualquer pessoa pode reconstruí-lo a partir do fonte e comparar o digest. Se bater, o artefato não foi adulterado no caminho — nem por um comprometimento do pipeline. Se não é reprodutível, você confia no processo de build por fé: a assinatura prova quem construiu, não o quê foi construído.

Esse é o cerne dos ataques de cadeia de suprimentos mais graves dos últimos anos: o fonte público estava limpo, o artefato distribuído não. Reprodutibilidade é a defesa que torna esse tipo de divergência detectável.

Em ordem de frequência:

  • Dependência sem versão fixa. pip install requests, FROM node:22, apt-get install curl — todos resolvem para versões diferentes conforme o dia.
  • Timestamps embutidos. Data de build no binário, mtime de arquivos, metadados de camada.
  • Ordem não determinística. Iteração sobre map ou glob de arquivos com ordem variável.
  • Caminhos absolutos do diretório de build gravados no artefato.
  • Recursos de rede voláteis: um script que baixa “a última versão” de algo.
  • Aleatoriedade: identificadores gerados, chaves temporárias, sementes não fixadas.
  • Ambiente: variáveis, locale, fuso horário, número de núcleos afetando paralelismo.

O primeiro nível — e o que dá mais retorno — é fixar tudo:

Janela do terminal
npm ci # usa package-lock.json; nunca npm install no CI
pip install -r requirements.txt --require-hashes
go build -mod=readonly # go.sum verifica os hashes
FROM node:22.9.0-slim@sha256:SUBSTITUA_PELO_DIGEST

Commite os lockfiles. Fixe também as actions do CI por commit SHA, não por tag móvel — o pipeline é código com acesso a segredos, e uma tag pode ser reapontada.

Tag é ponteiro mutável: loja:1.8.3 pode apontar para conteúdos diferentes em momentos diferentes. Digest (loja@sha256:...) é a identidade do conteúdo.

Consequência prática: assine, verifique, promova e implante por digest. Toda a argumentação de promover o mesmo artefato depende disso — sem digest, “o mesmo artefato” não tem como ser garantido.

Para chegar ao bit a bit, ainda faltam os detalhes:

# timestamps fixos nas camadas
ARG SOURCE_DATE_EPOCH=0
Janela do terminal
# Go: remova caminhos e informação de build variável
go build -trimpath -ldflags="-s -w -buildid=" -o loja ./cmd/loja
# compare dois builds
diffoscope artefato-1.tar artefato-2.tar

SOURCE_DATE_EPOCH é a convenção adotada por várias ferramentas para normalizar timestamps, e o buildx a respeita. diffoscope é a ferramenta que mostra exatamente onde dois artefatos divergem — costuma ser esclarecedor rodá-la uma vez.

Reprodutibilidade bit a bit é difícil e nem sempre vale o custo. Ecossistemas variam muito: Go e Rust chegam lá com pouco esforço; Java e Node exigem trabalho; imagens com muitas camadas de sistema exigem bastante.

Por isso, pense em níveis, e pare onde o retorno acabar:

  1. Dependências fixas e lockfiles versionados. Barato, e resolve a maior parte dos “funciona aqui e não lá”. Todo mundo deveria estar aqui.
  2. Digest em tudo, do FROM ao manifest de deploy. Barato, e habilita assinatura e promoção confiáveis.
  3. Determinismo de timestamps e caminhos. Esforço moderado.
  4. Bit a bit verificável por terceiros. Caro, e justificável para software distribuído publicamente, base de imagens corporativa e componentes críticos.

Para a maioria dos times, os níveis 1 e 2 entregam quase todo o benefício prático. Os níveis 3 e 4 são investimento de quem publica artefatos que outros consomem — e é exatamente aí que a cadeia de suprimentos é atacada.