Migrations em entrega contínua
Código você reverte em um minuto. Esquema de banco, não — e dado perdido não volta. Essa assimetria é a razão de a migration ser o ponto mais frágil da entrega contínua, e é também o que a torna um problema de projeto, não de ferramenta.
Por que migration quebra deploy
Seção intitulada “Por que migration quebra deploy”Durante qualquer deploy sem downtime existe uma janela em que duas versões da aplicação rodam ao mesmo tempo contra um banco. Rolling update, canário e blue-green: em todos, a versão antiga e a nova convivem por segundos ou horas.
Logo, o esquema precisa servir às duas. Renomear uma coluna e publicar tudo junto quebra a versão antiga na hora — e quebra também o rollback, que é o momento em que você mais precisa dele.
Mudança compatível para trás
Seção intitulada “Mudança compatível para trás”A regra que resolve 90% dos casos: em um deploy, faça só mudança que a versão anterior tolera.
| Seguro | Perigoso |
|---|---|
| Adicionar tabela | Remover tabela |
| Adicionar coluna anulável ou com default | Remover coluna em uso |
| Adicionar índice (concorrente) | Renomear coluna ou tabela |
Ampliar tipo (varchar(50) → varchar(200)) |
Estreitar tipo |
Adicionar constraint como NOT VALID, validar depois |
Adicionar NOT NULL sem default |
-- Postgres: índice sem travar escrita (fora de transação)CREATE INDEX CONCURRENTLY idx_pedidos_status ON pedidos (status);
-- constraint em duas etapas: cria sem varrer a tabela, valida depoisALTER TABLE pedidos ADD CONSTRAINT valor_positivo CHECK (valor > 0) NOT VALID;ALTER TABLE pedidos VALIDATE CONSTRAINT valor_positivo;Cuidado com o bloqueio: em tabela grande e ativa, um ALTER TABLE que pede lock exclusivo
enfileira todas as consultas atrás dele e derruba o serviço mesmo quando “roda em
segundos”. Use lock_timeout para falhar rápido em vez de travar tudo:
SET lock_timeout = '3s'; -- prefira falhar a migration a paralisar a aplicaçãoExpandir e contrair
Seção intitulada “Expandir e contrair”Para mudança incompatível — renomear coluna, dividir tabela, trocar tipo — use três deploys:
- Expandir. Adicione a estrutura nova. A aplicação escreve nas duas e lê da antiga. Nada quebra, e o rollback é trivial.
- Migrar. Preencha o histórico em lotes, fora do horário de pico. Depois inverta a leitura para a estrutura nova (idealmente atrás de uma flag).
- Contrair. Depois de dias de estabilidade, pare de escrever na antiga e só então remova.
-- 1. expandirALTER TABLE clientes ADD COLUMN email_normalizado text;
-- 2. migrar em lotes, para não segurar lock nem inflar o WALUPDATE clientes SET email_normalizado = lower(email) WHERE email_normalizado IS NULL AND id IN ( SELECT id FROM clientes WHERE email_normalizado IS NULL LIMIT 5000 );
-- 3. contrair, dias depois, com a aplicação nova estávelALTER TABLE clientes DROP COLUMN email;Parece trabalhoso porque é. É também a diferença entre uma mudança de esquema rotineira e uma janela de manutenção às duas da manhã.
Onde executar no pipeline
Seção intitulada “Onde executar no pipeline”O padrão que funciona: migration como etapa própria, antes do deploy da aplicação, com o esquema já compatível com a versão antiga (pelo princípio acima).
jobs: migracao: steps: - run: flyway validate # ou alembic/liquibase/atlas - run: flyway migrate # idempotente, versionada, com histórico deploy: needs: migracaoRegras que evitam as dores clássicas:
- Nunca rode migration no
initContainerde cada réplica: dez Pods disputando o mesmo DDL. Use Job único, ou trave por advisory lock. - Migration é idempotente e versionada; ferramenta com tabela de histórico (Flyway, Alembic, Liquibase, Atlas) em vez de script solto.
- Credencial de migration é separada, com permissão de DDL; a da aplicação não deveria ter DDL.
- Revise o SQL no PR como código de produção — inclusive gerado por ORM, que produz
DROP COLUMNcom surpreendente facilidade. - Migration longa (backfill) não é migration: é job de dados, com progresso, retomada e limite de taxa.
“Rollback de esquema” é mito?
Seção intitulada ““Rollback de esquema” é mito?”Quase. Existe down migration, e ela funciona para o caso trivial (criou tabela, apaga
tabela). Para o caso real, não: reverter DROP COLUMN recria a coluna vazia — a estrutura
volta, o dado não.
Por isso a estratégia certa não é reverter, é avançar de forma segura: mudanças sempre compatíveis, expand/contract para o resto, e a remoção acontecendo só quando a volta atrás já não é necessária. Some a isso o que garante recuperação de verdade:
- Backup verificado imediatamente antes de migration destrutiva.
- Ensaio da migration em cópia de produção, com volume e índices reais, medindo tempo e lock.
- Plano escrito de recuperação para a migration específica, com o comando pronto.
O teste honesto antes de aprovar o PR: se isto der errado às 3h, o que a pessoa de plantão executa? Se a resposta não couber em três linhas, a migration ainda não está pronta.
Próximo passo: garanta que a rede de segurança existe em Backup que funciona. O passo a passo está em Migration sem downtime.