Pular para o conteúdo

Migrations em entrega contínua

Avançado18 min de leituradados

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.

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.

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 depois
ALTER 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ção

Para mudança incompatível — renomear coluna, dividir tabela, trocar tipo — use três deploys:

  1. Expandir. Adicione a estrutura nova. A aplicação escreve nas duas e lê da antiga. Nada quebra, e o rollback é trivial.
  2. 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).
  3. Contrair. Depois de dias de estabilidade, pare de escrever na antiga e só então remova.
-- 1. expandir
ALTER TABLE clientes ADD COLUMN email_normalizado text;
-- 2. migrar em lotes, para não segurar lock nem inflar o WAL
UPDATE 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ável
ALTER 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ã.

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: migracao

Regras que evitam as dores clássicas:

  • Nunca rode migration no initContainer de 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 COLUMN com surpreendente facilidade.
  • Migration longa (backfill) não é migration: é job de dados, com progresso, retomada e limite de taxa.

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.