Ir para o conteúdo principal
Produto

Ramificar bancos de dados como código: um padrão de CI/CD para o Lakebase, em produção na Glaspoort

Como a Glaspoort implementa mudanças no banco de dados com o mesmo rigor do código de aplicação, ramificando cada ambiente a partir da produção, criando bancos efêmeros por PR e tratando as migrações como a única fonte da verdade.

por Hadi Farhat, Gideon Spierings, Ricardo de Vries e Raymon Veldman

  • Como a Glaspoort projetou sua estrutura de ramificação do Lakebase, com desenvolvimento e homologação ramificados diretamente da produção em vez de empilhados uns sobre os outros, para evitar a "armadilha de reset-from-parent", onde a atualização de um ambiente desatualizado força você a excluir e reconstruir tudo abaixo dele.
  • O fluxo de CI/CD por PR: como cada pull request recebe seu próprio branch descartável e atualizado do Lakebase copiado da produção, como as migrações são executadas novamente e testadas em uma imagem de aplicativo ativa antes que qualquer coisa toque em um ambiente real, e por que as próprias migrações (não os bancos de dados) são tratadas como a fonte da verdade.
  • O trade-off entre dois modelos de promoção: fazer o merge assim que o CI é aprovado versus fazer o merge apenas após o PR ser totalmente promovido pela homologação. O post explica por que a Glaspoort escolheu a abordagem focada em velocidade e as salvaguardas adicionadas (revalidação de stack e um pipeline de crise) para manter o processo seguro.

O problema que não podíamos ignorar

Glaspoort constrói e opera infraestrutura de fibra óptica nos Países Baixos. Tudo gira em torno do aumento do número de conexões de fibra e, por muito tempo, a equipe de dados passava os dias criando relatórios de BI para apoiar essa meta, enquanto a pergunta por trás de cada relatório já estava desatualizada no momento em que o relatório era concluído. O resultado foi uma proliferação de relatórios pontuais e usuários que não tinham a quem recorrer para tirar suas dúvidas de acompanhamento.

Então, quebramos essa dependência. Em vez de entregar o próximo relatório, criamos um aplicativo front-end personalizado no qual os gerentes de projeto veem diretamente onde estão as oportunidades para seus projetos. A novidade está sob o capô: usamos os produtos da Databricks diretamente como blocos de construção no aplicativo. Genie, para conversar com os dados e gerar análises rápidas. AI/BI Dashboards, para insights e análises de autoatendimento. Workflows automatizados com o Agent Bricks, que alertam os gerentes de projeto no momento em que algo se destaca em seus projetos, e o Lakebase, o banco de dados OLTP da Databricks, para os dados transacionais do aplicativo.

Essa combinação une dois mundos que até recentemente viviam separados: o ambiente analítico e um front-end operacional, onde os dados analíticos encontram os dados transacionais. A equipe de dados agora dedica seu tempo a espaços do Genie e metadados, em vez de relatórios pontuais. Mas nada disso continua rápido sem uma base sólida por baixo: CI/CD, testes de qualidade de dados, Infraestrutura como Código e governança de dados. Uma parte dessa base exigiu o design mais cuidadoso, e é sobre ela que trata o restante desta história: como enviamos alterações para o banco de dados por trás do aplicativo.

Por trás desse aplicativo está um banco de dados Databricks Lakebase. Trata-se de um Postgres OLTP serverless, executado ao lado do lakehouse, em vez de ser acoplado a ele. O fluxo de dados é simples de descrever e, como descobrimos, mais interessante de operar do que parece:

  • Os dados tratados do lakehouse são sincronizados em uma branch de produção do Lakebase, onde entram em um esquema de aplicativo somente leitura.
  • O aplicativo grava seu próprio estado de volta em um esquema separado nessa mesma branch, de modo que os dados lidos do lakehouse e os dados gravados pelo aplicativo coexistam lado a lado sem colidir.
  • Além disso, executamos três ambientes lógicos: desenvolvimento, homologação e produção. Enviamos alterações para esse banco de dados da mesma forma que enviamos alterações para o código do aplicativo, por meio de pull requests, CI e promoção controlada.

image3.png

É nesse último ponto que reside a questão interessante. No momento em que você decide que um banco de dados OLTP merece o mesmo rigor que o código do aplicativo, precisa responder a uma pergunta difícil:

Como permitimos que cada PR faça testes em um banco de dados idêntico ao de produção, sem que as pessoas interfiram no trabalho umas das outras e sem perder os dados recentes que dão sentido ao teste?

Os modos de falha aqui são familiares para quem já compartilhou um banco de dados com uma equipe. PRs que passam isoladamente e quebram quando são integrados. Ambientes de desenvolvimento e homologação que se distanciaram silenciosamente do que a produção realmente é. E aquele dia de atualização que ninguém quer, em que recuperar dados limpos significa derrubar ambientes, refazer cada string de conexão e reaplicar cada permissão manualmente.

Este post é sobre como evitamos a maior parte disso e sobre a única decisão que ainda estamos debatendo ativamente.

Branching no Lakebase em 60 segundos

Se você ainda não usou as branches do Lakebase, aqui está o único modelo mental de que precisa para este post.

Uma branch do Lakebase é uma branch Postgres copy-on-write derivada de uma branch pai. Criar uma branch não copia os dados; ela faz o fork do estado de forma barata e instantânea, e a branch só diverge da branch pai conforme você grava nela. Cada branch é totalmente isolada, com seu próprio endpoint e seus próprios dados. Você pode criar uma em segundos e descartá-la com a mesma rapidez.

Se isso parece com o Git, essa é a ideia. A analogia que organiza tudo abaixo é simples: uma feature branch no Git é mapeada para uma branch de banco de dados no Lakebase. Um PR ganha sua própria branch de código e sua própria branch de banco de dados, os testes são executados em ambas e, quando o trabalho está correto, ele é promovido para a produção.

Há uma restrição a ser considerada na próxima seção, pois ela é uma distinção crítica em relação ao branching do Git. Para redefinir uma branch a partir de sua branch pai, primeiro você precisa excluir as próprias branches filhas dessa branch. Uma branch pai não pode ser redefinida se houver branches que dependam dela.

A armadilha da redefinição a partir do pai

Aqui está o design que a maioria das equipes adota primeiro, porque reflete a maneira como desenhamos os ambientes em um quadro branco:

  • Uma única branch de desenvolvimento de longa duração derivada da produção.
  • Uma branch de homologação derivada do desenvolvimento.
  • Feature branches derivadas do desenvolvimento.

É uma hierarquia limpa: produção na raiz, depois desenvolvimento, com homologação e features abaixo dela. Isso também introduz dois modos de falha previsíveis: o desvio (drift) das branches, e a correção para esse desvio é cara o suficiente para que as equipes deixem de aplicá-la.

Primeiro, o desenvolvimento e a homologação se desviam da produção. A produção continua recebendo dados sincronizados recentes e gravações reais do aplicativo; as branches de desenvolvimento e homologação de longa duração não. Em uma ou duas sprints, você estará testando em um banco de dados que não se parece mais com aquele para o qual está enviando as alterações.

A correção óbvia é atualizar o desenvolvimento a partir da produção, e é aqui que a restrição da seção anterior se torna um fardo. Para redefinir o desenvolvimento a partir de sua branch pai, primeiro você deve excluir as branches filhas do desenvolvimento, o que nesta topologia significa a homologação e cada feature branch derivada do desenvolvimento. Assim, um "me dê dados recentes" rotineiro se transforma em uma cascata: excluir a homologação e todas as feature branches, redefinir o desenvolvimento, recriar os ambientes, refazer cada string de conexão que apontava para as branches antigas e reaplicar cada permissão (grant) do Postgres, pois as permissões vivem na branch que você acabou de excluir.

image1.png

Nenhuma dessas etapas é difícil por si só. Juntas, de forma recorrente, elas são uma maneira infalível de fazer com que toda a equipe evite silenciosamente a atualização, o que significa que todos voltam a testar com dados desatualizados e divergentes. Esse era o problema original. A topologia ingênua não custa apenas uma tarde ruim; ela desestimula a higiene que mantém o ambiente íntegro.

O design da Glaspoort: sempre crie branches a partir da produção

A correção é uma mudança simples na forma como você pensa sobre a topologia, e ela tem efeitos gigantescos: cada branch de ambiente de longa duração é filha da produção, não de outro ambiente.

O desenvolvimento e a homologação são branches extraídas diretamente da produção. Elas ficam lado a lado sob a produção, não empilhadas uma sobre a outra. Nenhuma é pai da outra, portanto, atualizar uma nunca força você a excluir a outra.

Essa única mudança neutraliza a armadilha. Quando o desenvolvimento ou a homologação se desviam, nós os redefinimos a partir da produção (uma operação de UI hoje), aproximadamente uma vez por sprint ou sempre que queremos dados recentes. Como nada fica abaixo do desenvolvimento ou da homologação, não há branches filhas para excluir primeiro, nem strings de conexão para refazer em toda a equipe, nem maratona de reaplicação de permissões. A redefinição é barata, então nós realmente a fazemos, mantendo os ambientes íntegros. Se uma redefinição por acaso perder uma alteração, a próxima reprodução de migração simplesmente a aplicará novamente.

image2.gif

Animado: o ciclo de vida por PR. É reproduzido no blog publicado; exibido como um quadro estático dentro deste documento.

O fluxo por PR

O dia a dia do desenvolvedor adiciona branches efêmeras sobre essa topologia estável:

  1. Um desenvolvedor abre um PR efêmero (TTL definido como 1 hora).
  2. O CI cria uma nova branch pr-xxxx efêmera a partir da produção. Criamos branches de tudo a partir da produção e nunca fazemos o merge de bancos de dados de volta. Essas branches por PR são descartáveis: nós as arquivamos no momento em que o PR é fechado, o que nos mantém abaixo do limite padrão do Lakebase de 10 branches não arquivadas por projeto.
  3. O CI reproduz as migrações nessa nova branch. Uma verificação de git diff decide se um ensaio de migração é realmente necessário, de modo que os PRs que não alteram as migrações pulam a reprodução.
  4. Não testamos apenas a migração isoladamente. O CI implanta a nova imagem do aplicativo em um slot de staging, aponta-a para a branch recém-migrada e executa a suíte de testes completa nessa dupla.
  5. Cada PR valida a migração e a nova imagem do aplicativo juntas, antes que qualquer uma delas toque em um ambiente real.
  6. Quando o PR é mesclado, o CI faz tudo novamente. Ele recria a branch a partir da produção, executa novamente as migrações e refaz os testes, pois o cenário pode ter mudado desde que o PR foi aprovado pela primeira vez. Se as migrações forem bem-sucedidas, elas serão aplicadas à branch de destino, diretamente no local. Para a primeira mesclagem, esse destino é o desenvolvimento.
  7. A promoção para a homologação e depois para a produção ocorre por meio de etapas de aprovação manual, cada uma executando novamente as migrações na próxima branch da fila.

Um detalhe que vale a pena destacar para equipes que vêm de um mundo centrado no git. Mantemos apenas uma branch master no git. Não há branch de desenvolvimento ou lançamento de longa duração no controle de versão. O caminho de promoção do desenvolvimento para a homologação e depois para a produção é expresso inteiramente por meio de etapas de aprovação manual no Azure DevOps, que são o que iniciam o CI e movem uma alteração de uma branch para a próxima. A topologia do ambiente reside no Lakebase e no pipeline; o git permanece simples.

Por que a reexecução de migrações é a única fonte da verdade

O que torna tudo isso seguro é que nunca mesclamos bancos de dados de volta uns nos outros. Não promovemos uma alteração copiando dados de uma branch de funcionalidade (feature branch) para o desenvolvimento. Nós a promovemos executando novamente as migrações na branch de destino. Usamos o vitest para executar o teste de fumaça no slot de staging.

As branches, neste modelo, são deliberadamente de curta duração e descartáveis. A descrição durável e autoritativa de como o esquema deve ser não é um banco de dados de longa execução que possa ter sofrido desvios. É o conjunto ordenado de migrações. É por isso que uma redefinição é segura mesmo quando algo é esquecido: se uma branch atualizada estiver sem uma alteração, a próxima reexecução da migração a aplicará novamente. As migrações são centrais o suficiente para que os bancos de dados ativos sejam sempre reproduzíveis a partir delas, em vez de serem preciosos por causa delas.

Esse é o verdadeiro diferencial. Criar branches a partir da produção mantém os dados atualizados. Tratar as migrações como a única fonte da verdade mantém o esquema correto. Juntos, eles significam que nenhum ambiente é especial demais para não poder ser descartado e reconstruído.

A bifurcação: dois modelos de promoção

Esta é a parte que achamos mais útil para outras equipes, porque é uma bifurcação real em vez de uma prática recomendada com uma única resposta certa. Depois de ter a topologia acima, você ainda precisa decidir quando um PR pode ser mesclado. Avaliamos dois modelos, e eles trazem vantagens e desvantagens em direções opostas.

Opção 1: Mesclar após a aprovação do CI (empilhamento de PRs)Opção 2: Mesclar após o CD para desenvolvimento e homologação (promoção por PR)
FuncionamentoUm PR pode ser mesclado assim que o CI em sua branch pr-xxxx estiver aprovado (verde).Um PR não pode ser mesclado até que tenha sido promovido pelo desenvolvimento e homologação.
PrósOs colegas de equipe podem aproveitar as alterações uns dos outros imediatamente.Cada PR se move no seu próprio ritmo; as correções rápidas (hotfixes) são prioritárias e não ficam presas em filas.
ContrasVários PRs mesclados se acumulam ao serem promovidos para desenvolvimento e homologação. Um hotfix não pode ignorar a fila sem um pipeline paralelo.Os colegas de equipe precisam esperar até que a homologação esteja aprovada antes de poderem trabalhar com base em uma alteração.

Resumindo: a Opção 1 prioriza a velocidade do desenvolvedor dentro de uma equipe unida. A Opção 2 prioriza a independência e um caminho direto para hotfixes. Se a sua equipe trabalha em alterações que se sobrepõem e confia no CI, o empilhamento fará você avançar mais rápido. Se você precisa frequentemente lançar uma correção que não pode esperar por outras tarefas em andamento, a promoção por PR compensa o esforço.

Escolhemos a Opção 1. Somos uma equipe pequena trabalhando em alterações que se sobrepõem. Esperar que a homologação seja aprovada antes que qualquer pessoa possa aproveitar seu trabalho é um preço que pagaríamos todos os dias, enquanto o risco de empilhamento só se materializa ocasionalmente. E quando isso acontece, ele é controlado: cada etapa de promoção executa novamente as migrações empilhadas em uma bifurcação limpa da produção antes de tocar na branch real, de modo que o lote empilhado é validado como uma unidade. Para casos realmente urgentes, mantemos um pipeline de crise separado que vai direto para a produção com a mesma verificação prévia. Velocidade por padrão, rota de fuga de prontidão.

Observações: os pontos que surgiram ao longo do caminho

Alguns tópicos surgiram repetidamente nas sessões de trabalho. Eles não são o núcleo da história, mas são os detalhes que determinam se o padrão sobrevive ao contato com a produção.

Autenticação de banco de dados e rotação de tokens. O aplicativo não armazena um segredo de banco de dados de longa duração. Uma senha estática no Azure Key Vault é usada para gerar uma credencial de banco de dados de curta duração (um token com TTL de 60 minutos) via `databricks postgres generate-database-credential`. Como o token expira após uma hora, o cliente precisa atualizá-lo proativamente; lidamos com isso usando uma função de senha assíncrona no pool de conexões que armazena a credencial em cache e a rotaciona antes que ela expire.

Permissões e provisionamento de acesso. Não gerenciamos o DDL das tabelas por meio de migrações, e as permissões no nível do objeto (grants) são aplicadas manualmente hoje, em vez de por código. É o item mais evidente em nossa lista de melhorias. Mas como aplicamos essas permissões na produção e as branches de ambiente são redefinidas ao serem novamente bifurcadas da produção, as branches filhas as herdam automaticamente a cada atualização. A etapa de permissão manual ocorre uma vez na produção, e não repetidamente em cada branch filha, por isso continua sendo rara o suficiente para lidarmos com ela por enquanto.

Acesso ao banco de dados: um único usuário de aplicativo. Não provisionamos uma função (role) do Postgres por usuário final. O aplicativo se conecta por meio de um único usuário de aplicativo, e a autorização para usuários individuais reside na camada do aplicativo, e não no banco de dados. Isso mantém o provisionamento simples; a desvantagem é que o controle de acesso por usuário é aplicado acima do Postgres, e não por ele.

O que isso proporcionou para a Glaspoort

Um caso de uso que costumava exigir uma equipe inteira e meses de trabalho agora entra em produção com uma equipe pequena em questão de dias. Nossas iterações aumentaram dez vezes, e uma única equipe entrega valor continuamente em uma plataforma que cresce junto com a organização.—Raymon Veldman, Gerente de TI e Controle de Negócios

A mudança mais visível é a velocidade com menos ansiedade. Agora, cada PR ganha seu próprio banco de dados com estrutura idêntica à de produção em minutos, de modo que os desenvolvedores pararam de se coordenar em torno de ambientes compartilhados de desenvolvimento e homologação, e as conversas sobre "quem quebrou a homologação?" simplesmente desapareceram. Como as branches de ambiente são sempre filhas da produção, nunca mais tivemos que lidar com um dia de redefinição a partir do pai: sem excluir branches filhas, sem refazer strings de conexão, sem tardes perdidas reaplicando permissões.

As migrações são executadas novamente a cada etapa de promoção, e implantamos a nova imagem do aplicativo em um slot de staging na branch recém-migrada e executamos a suíte de testes completa antes que qualquer coisa toque em um ambiente real, de modo que o que chega à produção já se provou eficaz duas vezes. E quando surge algo urgente, os hotfixes seguem o mesmo caminho seguro que todo o resto, apenas mais rápido, em vez de ignorar o pipeline.

Está escolhendo o Lakebase para um banco de dados de aplicativo e lidando com as mesmas questões de CI/CD? Comece por aqui: ambientes de longa duração ramificados diretamente da produção, branches efêmeras por PR e migrações como a única fonte da verdade. Saiba mais sobre o Databricks Lakebase

(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original

Receba os posts mais recentes na sua caixa de entrada

Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.