Veja como criar uma pipeline menos complexa para banco de dados.
Pipeline + Banco de dados, e agora?!
Quando pensamos em banco de dados e controle de código, normalmente batemos cabeça em várias questões que vão desde a implementação, até o engajamento do time de desenvolvimento para atuar com novas regras de implementação de código.
Uma solução chamada Liquibase
Recentemente pensando em como implementar, recebi a indicação de uma ferramenta chamada Liquibase, que promete controlar código de forma simples, com um hub próprio para isso, mas que tem uma proposta interessante quando falamos de repositório com git somado aos comandos do Liquibase.
Como é possível implementar?
Estudei alguns cases de fora do Brasil (infelizmente não achei materiais aqui), e encontrei uma maneira de implementar uma forma de automação de código que não impacta os times de desenvolvimento, que podem utilizar o Git com um repositório de código sem mudar a forma de escrita (SQL padrão).
Vou citar um caso de uso com Gitlab.
Para execução local, a instalação do Liquibase padrão é necessária e as instruções podem ser obtidas pelo site oficial neste link*
Documentação padrão do Liquibase disponível aqui.
Dentro do projeto é importante haver um arquivo .proprierties que irá conter as configurações de execução "por ambiente". No meu caso eu inseri 3, sendo um para dev, stage e prod:
changeLogFile: liquibase/update.xml
url: jdbc:[database]:thin:@[host]:[ip]/[schema]
username:
password:
driver:
classpath: liquibase/[drive de conexão com o banco]
liquibaseSchemaName: [schema principal para controle de código]
schemas=[separados por ,]
includeSchema=true
outputFile=output_local.sql
loglevel=SEVERE
#liquibase.hub.mode=off
liquibaseProLicenseKey: [chave do Liquibase Pro]
liquibase.hub.apiKey=[chave do projeto inserido no Hub do Liquibase que é opcional]
Como é orquestrado
A execução das etapas do Liquibase são executadas a partir de dois arquivos de configuração:
- liquibase/update.xml contem a configuração de apontamento para o arquivo mestre.
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<include file="./master.xml" />
</databaseChangeLog>
- master.xml contem as etapas de execução do sincronizador DDL com a sequência sendo.:
- changelog.sql que contem os primeiros scripts que serão executados.
- scripts_before/scripts_before.xml que executa os arquivos changelog.ddl.sql para criar tabelas no banco de dados. E por fim o arquivo changelog_constraints.sql para carregar as constraints das tabelas criadas.
- A execução a seguir é tratada conforme os arquivos criados nas pastas de forma ordenada:
- triggers
- views
- types
- procedures
- package_spec
- package_bodies
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<include file="changelog.sql" relativeToChangelogFile="true" />
<include file="scripts_before/scripts_before.xml" relativeToChangelogFile="true" />
<includeAll path="triggers" relativeToChangelogFile="true"/>
<includeAll path="views" relativeToChangelogFile="true"/>
<includeAll path="types" relativeToChangelogFile="true"/>
<includeAll path="procedures" relativeToChangelogFile="true"/>
<includeAll path="package_spec" relativeToChangelogFile="true"/>
<includeAll path="package_bodies" relativeToChangelogFile="true"/>
</databaseChangeLog>
Formato dos arquivos .sql que devem ser incluídos nas pastas de processamento
Os arquivos devem sempre conter o seguinte formato no cabeçalho da execução:
--liquibase formatted sql
--changeset {Nome do desenvolvedor}:{Nome do Changeset} context:{tag do contexto} runOnChange:true stripComments:false
--comment {seu cometário}
Detalhes da instrução (inserido no changeset)
- Nome do desenvolvedor: Trocar pelo dev que está executando a alteração
- Nome do Changeset: É o nome que será dado aos sets que deverão ser compilados no banco de dados.
- tag do contexto: É o contexto que será passado no momento da execução. Atenção, todo item sem contexto definido na execução também será aplicado.
- runOnChange:true: Define que sempre que houver mudança no arquivo de sets, o item será relido e aplicado com as novas alterações.
- stripComments:false: Persiste os comentários do código enviando para compilação ao banco de dados.
- comment: Irá conter os comentários do código enviado ao Liquibase e aplicados no banco de dados desde que a opção stripComments esteja definida como false.
Maiores informações sobre comandos do Liquibase
Obtenha maiores detalhes de execução através da documentação aqui.
Execução do Liquibase
Existe duas formas de utilizar o Liquibase para controlar os objetos do banco de dados.
- Quando o banco possui um objeto
- Quando o objeto é novo e irá partir do Liquibase
1. Quando o banco possui um objeto
Antes de executar as funções de update do liquibase, é necessário primeiro sincronizar o objeto existe no banco de dados. Se for uma procedure pro exemplo, crie na pasta procedures uma procedure igual ao que consta registrado no banco de dados, em seguida é necessário rodar os comandos de sincronização de base de dados.
É interessante tomar essa ação se o desenvolvedor for utilizar o Liquibase para fazer update em objetos existentes inseridos diretamente no banco de dados. Tomar essa ação evita erros de execução.
Comandos de sincronização
- Este comando irá gerar o output_local.sql para análise do que será executado no banco de dados: liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties ChangelogSyncSQL
- Se tudo estiver ok, basta executar liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties ChangelogSync
liquibase --defaultsFile=liquibase_dev.properties updateSQL \
liquibase --defaultsFile=liquibase_dev.properties update
Após os dados serem sincronizados na tabela definida como padrão das migrations em seu schema, os objetos que serão editados podem ser alterados no controle de logs das pastas do Liquibase, e em seguida, o comando de update pode ser executado:
Comandos update
- Este comando irá gerar o output_local.sql para análise do que será executado no banco de dados: liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties updateSQL
- Se tudo estiver ok, basta executar liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties update
2. Quando o objeto é novo e irá partir do Liquibase
O Comando para update pode ser executado diretamente e o controle de versão irá refletir imediatamente na tabela definida como padrão das migrations em seu schema.
Comandos update
- Este comando irá gerar o output_local.sql para análise do que será executado no banco de dados: liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties updateSQL
- Se tudo estiver ok, basta executar liquibase --defaultsFile=[arquivo de propriedades do ambiente].properties update
Opções de rollback
Em todos os arquivos de chagelog é possível inserir uma instrução de rollback. Esta instrução é executada em caso de rollback executado pela linha de comando.
Maiores informações na documentação oficial aqui
** Requer instrução de rollback nos arquivos de changelog, exemplo:
liquibase --defaultsFile=liquibase_dev.properties rollback-count 3
Como automatizar no Gitlab
É necessário criar um runner local que é executado a partir de um docker em uma máquina que tenha acesso ao banco de dados. Será ele que fará a orquestração entre o repositório e o executor instalado.
** No hub do Docker existe uma máquina especifica do Liquibase que pode ser usada sem necessidade de maiores configurações.
A configuração padrão do runner usada no .gitlab-ci.yml para executar por ambiente é a seguinte (usando como exemplo os 3 ambientes que citei acima):
image: liquibase/liquibase:latest
stages:
- developer
- stage
- production
developer-job:
stage: developer
tags:
- docker-liquibase-runner
environment: developer
script:
- echo "Deploying developer application..."
- liquibase --version
- liquibase --defaultsFile=liquibase_dev.properties updateSQL
- cat output_local.sql
- liquibase --defaultsFile=liquibase_dev.properties update
- echo "Application successfully deployed."
only:
- developer
stage-job:
stage: stage
tags:
- docker-liquibase-runner
environment: homol
script:
- echo "Deploying stage application..."
- liquibase --version
- liquibase --defaultsFile=liquibase_stage.properties updateSQL
- cat output_local.sql
- liquibase --defaultsFile=liquibase_stage.properties update
- echo "Application successfully deployed."
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "stage"'
production-job:
stage: production
tags:
- docker-liquibase-runner
environment: prod
script:
- echo "Deploying prod application..."
- liquibase --version
- liquibase --defaultsFile=liquibase_prod.properties updateSQL
- cat output_local.sql
- liquibase --defaultsFile=liquibase_prod.properties update
- echo "Application successfully deployed."
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
Conclusão
Pegando como exemplo de casos de uso que utilizam ORM's (Object-Relational Mapping), o Liquibase é capaz de fazer as mesmas ações, porém ele não está inserido no contexto de uma aplicação especifica, e sim ele é abstraído em lógica de uso externa, permitindo a qualquer time/desenvolvedor, utilizar a dinâmica automática citada neste artigo como forma de controlar o código em um repostório, automatizando toda a entrega sem mudar muito a dinâmica de como o desenvolvedor escreve o código.
Estas ações permitem a criação de uma esteira de CI/CD para banco de dados que é extremamente performática, segura e simples.