Database migrations no CI/CD: Flyway ou Liquibase automatizados

1. Introdução ao Problema: Migrações de Banco de Dados em Ambientes Contêineres

Em pipelines DevOps modernos, a automação de migrações de banco de dados é um dos maiores desafios técnicos. Enquanto o código da aplicação evolui rapidamente através de containers efêmeros, os bancos de dados mantêm estado persistente — e essa assimetria gera conflitos frequentes.

No contexto de Docker e Kubernetes, o problema se agrava: pods são criados e destruídos constantemente, mas o schema do banco precisa evoluir de forma controlada e atômica. Uma migração mal executada pode derrubar toda a aplicação ou, pior, corromper dados em produção.

O fluxo ideal funciona assim: um desenvolvedor altera um arquivo SQL → o CI detecta a mudança → executa a migração no banco de staging → após validação, promove para produção → só então o deploy da nova versão da aplicação é liberado. Este artigo mostra como implementar esse fluxo com Flyway e Liquibase, duas ferramentas maduras de versionamento de banco de dados.

2. Flyway vs. Liquibase: Escolha da Ferramenta

Flyway adota uma abordagem minimalista: você escreve scripts SQL puros (V1__create_table.sql, V2__add_column.sql) e a ferramenta gerencia o versionamento automaticamente. É ideal para equipes pequenas que preferem simplicidade e já dominam SQL.

Liquibase oferece mais controle: os changesets podem ser escritos em XML, YAML ou JSON, com suporte nativo a rollback e pré-condições. É a escolha certa para ambientes regulados (bancos, fintechs) onde cada alteração precisa ser auditada e reversível.

Ambas as ferramentas possuem imagens Docker oficiais:

# Flyway
docker run --rm flyway/flyway:latest migrate -url=jdbc:postgresql://host:5432/db -user=admin -password=secret

# Liquibase
docker run --rm liquibase/liquibase:latest update --url=jdbc:postgresql://host:5432/db --username=admin --password=secret

3. Estrutura de Pipeline CI/CD para Migrações

A chave para pipelines robustos é separar a etapa de migração do build da aplicação. Exemplo de pipeline GitLab CI:

stages:
  - build
  - migrate
  - deploy

migrate-staging:
  stage: migrate
  image: flyway/flyway:latest
  script:
    - flyway -url=$STAGING_DB_URL -user=$DB_USER -password=$DB_PASSWORD migrate
  only:
    - main

deploy-staging:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  needs:
    - migrate-staging

No GitHub Actions, o mesmo princípio se aplica:

jobs:
  migrate:
    runs-on: ubuntu-latest
    container:
      image: liquibase/liquibase:latest
    steps:
      - name: Run migration
        run: |
          liquibase update \
            --url=${{ secrets.DB_URL }} \
            --username=${{ secrets.DB_USER }} \
            --password=${{ secrets.DB_PASS }}

A regra de ouro: nunca faça deploy antes da migração. O pipeline deve bloquear a etapa de deploy se a migração falhar.

4. Execução Segura em Kubernetes com Init Containers

Em Kubernetes, o padrão mais seguro é usar init containers para executar migrações antes do container principal da aplicação subir:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      initContainers:
      - name: flyway-migration
        image: flyway/flyway:latest
        command: ["flyway", "migrate"]
        env:
        - name: FLYWAY_URL
          value: jdbc:postgresql://db-service:5432/mydb
        - name: FLYWAY_USER
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: username
        - name: FLYWAY_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        volumeMounts:
        - name: migrations
          mountPath: /flyway/sql
      containers:
      - name: myapp
        image: myapp:latest

Para evitar concorrência entre réplicas, configure locks de banco:

# Flyway usa locks nativos do banco
# Liquibase requer configuração adicional:
liquibase.command.lockWaitTime: 60000
liquibase.command.lockPollRate: 1000

5. Versionamento e Rollback Automatizados

Versionamento: Flyway usa números sequenciais (V1, V2, V3) enquanto Liquibase sugere timestamps (20240101120000). Ambos funcionam, mas timestamps evitam conflitos em branches paralelos.

Rollback via CI/CD: Liquibase tem suporte nativo a rollback:

# Rollback de uma changeset específica
liquibase rollback --tag=v1.0

# Rollback automático em caso de falha
liquibase update --rollback-on-error=true

Flyway requer a versão Teams/Enterprise para rollback automático. Na versão Community, use scripts manuais:

# Criar script de rollback manual
# U1__revert_create_table.sql
DROP TABLE IF EXISTS users CASCADE;

Feature flags: isole migrações problemáticas com variáveis de ambiente:

- Se a migração falhar, ative a flag DISABLE_NEW_FEATURE=true
- A aplicação lê a flag e desativa a funcionalidade que depende da nova tabela

6. Gerenciamento de Segredos e Conexões

Nunca hardcode credenciais. Use Kubernetes Secrets:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: <base64-encoded>
  password: <base64-encoded>

Referencie no pod:

envFrom:
- secretRef:
    name: db-credentials

Para rotation de senhas sem downtime, use o padrão dual credentials: mantenha duas senhas ativas no banco, troque a senha no secret e remova a antiga após validação.

7. Monitoramento e Validação Pós-Migração

Configure health checks no Kubernetes que aguardam a migração:

livenessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - "curl -f http://localhost:8080/health/db || exit 1"
  initialDelaySeconds: 10
  periodSeconds: 30

O endpoint /health/db deve verificar se a versão esperada do schema está ativa. Exemplo em Node.js:

app.get('/health/db', async (req, res) => {
  const version = await db.query('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1');
  if (version === process.env.EXPECTED_SCHEMA_VERSION) {
    res.status(200).send('OK');
  } else {
    res.status(503).send('Migration pending');
  }
});

No pipeline, adicione alertas:

# GitLab CI - notificação no Slack
after_script:
  - |
    if [ $CI_JOB_STATUS == "failed" ]; then
      curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"Migration failed in $CI_ENVIRONMENT_NAME"}' \
        $SLACK_WEBHOOK_URL
    fi

8. Boas Práticas e Próximos Passos

  1. Teste em staging com dados anonimizados: clone a base de produção, anonimize dados sensíveis e execute as migrações primeiro ali.
  2. Canary releases: faça rollout gradual (10% → 50% → 100%) e monitore erros de banco antes de liberar para todos.
  3. Migrações declarativas com operadores Kubernetes: ferramentas como StackGres e KubeDB gerenciam bancos inteiros via CRDs, incluindo migrações automáticas.

Checklist final para produção:
- [ ] Migração executada em init container (nunca no container principal)
- [ ] Locks de banco configurados para evitar concorrência
- [ ] Rollback testado e documentado
- [ ] Secrets injetados via Kubernetes Secrets
- [ ] Health checks verificam versão do schema
- [ ] Pipeline bloqueia deploy em caso de falha

Com essas práticas, suas migrações de banco de dados se tornam tão confiáveis quanto o deploy de código — mesmo em ambientes Kubernetes dinâmicos e efêmeros.

Referências

  • Flyway Documentation — Documentação oficial completa do Flyway, incluindo exemplos de integração com Docker e Kubernetes.
  • Liquibase Official Docs — Guia oficial do Liquibase com changelogs, rollback e integração CI/CD.
  • Kubernetes Init Containers — Documentação oficial sobre init containers, padrão recomendado para migrações.
  • GitLab CI/CD Database Migrations — Tutorial prático do GitLab sobre como executar migrações no pipeline CI/CD.
  • StackGres Operator — Operador Kubernetes para PostgreSQL que gerencia migrações e backups de forma declarativa.
  • KubeDB by AppsCode — Operador Kubernetes para múltiplos bancos de dados, com suporte a migrações automáticas via CRDs.