BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Construindo um pipeline CI/CD + Liquibase no GitLab

Construindo um pipeline CI/CD + Liquibase no GitLab

Pontos Principais

  • Em um cenário de DevOps é fundamental realizar automatizações nas aplicações para que o foco do time seja na evolução do produto.
  • Automatizar o CI/CD das aplicações é primordial para iniciar uma cultura DevOps. Há inúmeras ferramentas para CI/CD que hoje são baseadas em container, o que traz maior flexibilidade e facilidade na aplicação do CI/CD.
  • O banco de dados é uma parte pouco explorada nos processos de automatização do CI/CD. Porém, quando conseguimos englobar esta etapa na esteira, usufruímos ainda mais da automatização.
  • A automatização do banco de dados é feita através de uma ferramenta de gerenciamento de mudanças. A partir de então, todas as alterações estruturais e possíveis cargas de dados serão versionadas e executadas automaticamente.
  • O principal motivo para utilizar CD/CI é, além do maior foco no produto, evitar intervenções manuais, mitigando eventuais falhas e dando maior velocidade na entrega de funcionalidades para o cliente final.

Neste artigo, mostraremos como configurar o pipeline de CI/CD (Continuous Integration/Continuous Deployment) no GitLab CI para uma aplicação Java Web. Esta aplicação deverá utilizar o Maven para o gerenciamento de pacotes. O deploy da aplicação será numa instância do WildFly. Para completar nossa pipeline, realizaremos a gestão de mudanças num banco de dados MariaDB, utilizando a ferramenta Liquibase.

Sobre o GitLab CI, maiores detalhes sobre recursos e funcionalidades estão disponíveis no endereço da documentação oficial do GitLab.

Automatização da Integração Contínua (CI)

A parte de CI compreende a etapa de geração do pacote da aplicação que será implantado. O primeiro passo para automatizá-la é saber quais são as etapas realizadas manualmente para gerar o pacote da aplicação.

Neste caso, as etapas manuais são: build, testes unitários e empacotamento. Para realizar essas três atividades é utilizado o Maven com o comando: mvn package.

Para entendermos o que este comando faz, precisamos conhecer o ciclo de vida do Maven, conforme a figura abaixo:

Portanto, quando executamos o mvn package, o Maven também executa os comandos: compile, test e package.

Como vamos construir um pipeline para executar as etapas de build, testes unitários e empacotamento, iremos executá-las isoladas. Teremos que executar o comando Maven referente a cada etapa, conforme:

Etapa

Comando

Build

mvn compile

Teste Unitário

mvn test

Empacotamento

mvn -DskipTests=true package

 

Um detalhe sobre o comando da etapa de empacotamento: a utilização do argumento -DskipTests=true diz para o Maven não executar os testes, pois já foram executados anteriormente.

 

No final, teremos:

Agora que já sabemos as etapas e quais são os comandos de cada uma, podemos construir nosso pipeline no GitLab CI. Para isso, é necessário criar um arquivo em YAML na raiz do nosso projeto com o nome .gitlab-ci.yml. Veja como ficou a estrutura de pastas do projeto:

Atenção: todo arquivo YAML deve respeitar o alinhamento de cada comando descrito no arquivo.

 

Dentro do arquivo YAML temos:

1 image: maven:latest
2 
3 stages:
4      - build
5      - test
6      - package
7 
8 build:
9      stage: build
10  script:
11    - mvn compile
12
13 test:
14  stage: test
15  script:
16   - mvn test
17
18 package:
19  stage: package
20  script:
21    - mvn -DskipTests=true package
22  artifacts:
23    paths:
24      - target/app.war

O GitLab CI é uma ferramenta de CI/CD baseada em container Docker. Na primeira linha do código acima, informamos ao GitLab CI que iremos utilizar a imagem Docker do Maven como base na execução das etapas do CI. Da linha 3 a 6, descrevemos as etapas que farão parte do pipeline, neste caso: build, test e package.

Da linha 8 a 11, temos a execução da primeira etapa do pipeline, o build. Na linha 8, temos o nome da etapa e na linha 9, a etapa do pipeline. A partir da linha 11, colocamos todos os comandos que correspondem a etapa de build onde executamos o comando mvn compile.

Da linha 13 a 16, é realizada a etapa de teste unitário. Na linha 13, temos o nome da etapa e na linha 14 a etapa do pipeline. A partir da linha 15, colocamos os comandos para realizar o teste - neste caso, o comando mvn test executa o teste unitário.

Por fim, nas linhas 18 a 24 temos a etapa de empacotamento. Na linha 18, temos o nome da etapa e na linha 19, a etapa do pipeline. Na linha 21, executamos o comando mvn -DskipTests=true package. Nas linhas 22 a 24, utilizamos a funcionalidade artifacts, onde conseguimos armazenar o pacote gerado - neste caso, o app.war, - para ser utilizado nas próximas etapas do pipeline. O pacote será utilizado na etapa de deploy (CD).

Automatização da Entrega Contínua (CD)

A parte de CD corresponde ao deploy da aplicação no servidor. Da mesma forma como feito anteriormente, precisamos entender como é o deploy manualmente para depois automatizar.

O deploy manual da aplicação é feito através da interface web do WildFly. Através da interface web não temos uma automatização confiável. Para resolver isso, o WildFly fornece também uma linha de comando (CLI) através do script jboss-cli.[sh|bat] (a extensão do arquivo dependerá do sistema operacional).

Normalmente, o jboss-cli.[sh|bat] encontra-se dentro da pasta bin do WildFly. O primeiro passo é conectar-se a uma instância do WildFly. Para isso, utilizamos o comando connect. Ao digitar este comando, deveremos informar o usuário e senha para acesso, da mesma forma como é feito para acessar a interface web.

Conectado a uma instância do WildFly, conseguimos realizar o deploy com o comando deploy <arquivo>.war. Porém, esse comando funcionará apenas no primeiro deploy da aplicação. Nos próximos, deveremos informar para o WildFly que desejamos sobrescrever o pacote utilizando a opção --force. Assim, o comando nas próximas execuções será deploy <arquivo>.war --force.

Conseguimos realizar o deploy através do CLI fornecido pelo WildFly, mas agora temos dois problemas. O primeiro é que tivemos que digitar vários comandos e informar o usuário e senha para conectar no WildFly. O segundo é que temos uma condição para realizar o deploy.

Para resolver o problema de vários comandos é possível realizar com uma única linha o comando jboss-cli.sh --connect --controller=<HOST> --user=<USER> --password=<PASSWORD> --commands="deploy target/quiz.war".

Agora, precisamos saber se o deploy já foi realizado no WildFly. Para isso, criamos um Shell Script, conforme código abaixo, que verifica se o deploy já foi realizado ou não no WildFly. Este script é versionado junto com o código fonte da aplicação:

#!/bin/bash

RESULT_DEPLOYMENTS=$(/opt/jboss/wildfly/bin/jboss-cli.sh --connect --controller=${HOST_WILDFLY} --user=${USER_WILDFLY} --password=${PASS_WILDFLY} --commands="deployment list")

MY_DEPLOY=false

for f in $RESULT_DEPLOYMENTS; do
	if [ "$f" == "quiz.war" ]; then
		MY_DEPLOY=true
	fi
done

echo "Executando o deploy no WildFly na AWS..."

if [ ${MY_DEPLOY} == true ]; then
	$(/opt/jboss/wildfly/bin/jboss-cli.sh --connect --controller=${HOST_WILDFLY} --user=${USER_WILDFLY} --password=${PASS_WILDFLY} --commands="deploy target/quiz.war --force")
else
    $(/opt/jboss/wildfly/bin/jboss-cli.sh --connect --controller=${HOST_WILDFLY} --user=${USER_WILDFLY} --password=${PASS_WILDFLY} --commands="deploy target/quiz.war --server-groups=other-server-group")
fi

echo "Deploy finalizado"

E o que algumas variáveis estão fazendo no código se não foram declaradas? Essas são variáveis de ambiente declaradas dentro do GitLab CI.

Precisamos completar nosso pipeline com o deploy da aplicação. Colocaremos no arquivo .gitlab-ci.yml o seguinte conteúdo:

1 deploy:
2  stage: deploy
3  dependencies:
4    - build
5    - test
6    - package
7  image: anardy/wildfly
8  before_script:
9    - chmod +x ./deploy.sh
10  script:
11    - ./deploy.sh

Na primeira linha, temos o nome da etapa e na linha 2, a etapa do pipeline, conforme fizemos anteriormente. Da linha 3 a 6, temos a primeira novidade, o uso do recurso dependencies, que faz com que a etapa deploy só seja executada em caso de sucesso das etapas anteriores (build, test e package).

A segunda novidade é na linha 7, onde indicamos que queremos utilizar a imagem anardy/wildfly. Essa imagem foi criada para utilizar o script jboss.cli.sh, porém ele não é um simples arquivo e precisa de alguns módulos do WildFly. Por isso, criamos a imagem com todos os arquivos necessários para o funcionamento do jboss-cli.sh.

Para finalizar, nas linhas 10 a 11, executamos o Shell Script de deploy descrito anteriormente.

Neste ponto, temos a parte da esteira CI/CD pronta conforme abaixo:

Vamos concluir nosso pipeline configurando a ferramenta Liquibase.

Executando Liquibase no pipeline

O Liquibase é responsável por gerenciar as mudanças do banco de dados, pois possui a capacidade de saber quais mudanças foram executadas no banco. Sendo assim, a partir deste momento, qualquer alteração no banco deverá gerar um script (arquivo SQL), que será armazenado junto com o código-fonte da aplicação.

Vamos continuar com a nossa disciplina de identificar o que é feito manualmente para depois realizarmos a automatização. Para isso, teremos que entender um pouco mais sobre o funcionamento do Liquibase.

Primeiro, faça o download do Liquibase. Após realizar o download, coloque os arquivos dentro da pasta liquibase na raiz do projeto, conforme abaixo:

A pasta sdk pode ser removida, deixando o conteúdo da pasta liquibase conforme abaixo:

Como o Liquibase é escrito em Java, precisamos de uma biblioteca que interaja com o banco de dados MariaDB. Para isso, devemos utilizar o Driver JDBC do MariaDB. Após o download, devemos colocar os arquivos na pasta lib, conforme a imagem:

Por fim, é necessário configurar dois arquivos: changelog.xml e liquibase.properties.

No changelog.xml, devemos informar ao Liquibase cada alteração do banco de dados. Para o Liquibase, cada alteração no banco é chamada de changeSet, e para cada changeSet informamos um identificador único, autor e comentário da alteração. Abaixo, temos um exemplo do changelog.xml com um changeSet.

<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">

   <changeSet id="1" author="Autor da Mudança">
     <comment>Criação da tabela Funcionarios</comment>
     <createTable tableName="funcionarios">
          <column name="nome" type="varchar(255)" />
     </createTable>
     <rollback>
          <dropTable tableName="funcionarios"/>
     </rollback>
   </changeSet>
</databaseChangeLog>

O changelog.xml acima cria uma tabela com o nome funcionários e essa tabela contém a coluna nome. Logo mais entenderemos a tag <rollback>.

No liquibase.properties, devemos configurar as informações do banco de dados: hostname, username e password do banco. Além de informar também a localização do arquivo changelog.xml e do Driver JDBC configurado anteriormente. Abaixo temos um exemplo de um arquivo liquibase.properties configurado.

#liquibase.properties
changeLogFile=com/example/changelog.xml
driver: org.mariadb.jdbc.Driver
classpath: ./lib/mariadb-java-client-2.3.0.jar

url: jdbc:mariadb://<HOST_DB>:3306/<NOME_DB>?allowMultiQueries=true&createDatabaseIfNotExist=true
username: <USER_DB>
password: <PASS_DB>

Sempre utilize o hostname da máquina e não o IP nos processos de automatização, pois o IP é dinâmico.

Todas as configurações do Liquibase foram realizadas. Agora precisamos executar o Liquibase através do comando ./liquibase status. Ao executar o comando devemos receber uma mensagem de sucesso:

Liquibase command 'status' was executed successfully.

Com a configuração correta, podemos executar o script no banco de dados através do comando ./liquibase update.

Antes de continuarmos com a configuração do Liquibase no pipeline, precisamos entender como funciona a tag <rollback>. O termo rollback apresentando pelo Liquibase é diferente do rollback que estamos acostumados no mundo de banco de dados. O rollback do Liquibase é voltar para um estado anterior.

Vimos que toda atualização de banco de dados quando utilizamos o Liquibase deve ser feita através de um changeSet, portanto antes de qualquer execução do Liquibase, devemos registrar cada estado do banco de dados, como se fosse uma foto. O Liquibase chama esse registro de estado de tag.

./liquibase tag 1
./liquibase update

Ao executar a nova mudança no banco de dados e verificar alguma falha na execução, basta voltar o banco de dados para o estado (tag) anterior com o comando abaixo.

./liquibase rollback 1

Quando executamos este comando o Liquibase irá executar o conteúdo da tag <rollback> para conseguir voltar para o estado desejado.

Agora que já entendemos como o Liquibase funciona, podemos finalizar nosso pipeline. Mas antes, deixaremos dentro da pasta liquibase o arquivo changelog.xml, conforme imagem abaixo:

Mais tarde, entenderemos o motivo da exclusão dos arquivos e o surgimento do arquivo lb.sh. Por hora, adicionaremos a parte do Liquibase no pipeline adicionando o conteúdo abaixo no arquivo .gitlab-ci.yml.

1  liquibase:
2   stage: liquibase
3   dependencies:
4     - deploy
5   image: anardy/liquibase
6   before_script:
7     - chmod +x ./liquibase/lb.sh
8   script:
9     - ./liquibase/lb.sh
10    - cd /liquibase
11    - ./liquibase tag $CI_COMMIT_SHA
12    - ./liquibase clearCheckSums
13    - ./liquibase update
14
15  liquibase_rollback:
16  stage: liquibase_rollback
17   image: anardy/liquibase
18   before_script:
19     - chmod +x ./liquibase/lb.sh
20  script:
21     - ./liquibase/lb.sh
22     - cd /liquibase
23     - ./liquibase rollback $CI_COMMIT_SHA
24  when: on_failure

Na primeira linha temos o nome da etapa e, na linha 2, a etapa do pipeline. Nas linhas 3 e 4, usamos novamente o recurso dependencies, que faz a etapa liquibase ser executada apenas em caso de sucesso da etapa anterior, nesse caso, o deploy.

Na linha 5, indicamos que queremos utilizar a imagem anardy/liquibase. Lembra que apagamos todos esses arquivos da pasta liquibase? O motivo é porque a imagem Docker anardy/liquibase contém todos os arquivos necessários para executar o Liquibase.

Sendo assim, temos que nos preocupar apenas em configurar o arquivo changelog.xml com as alterações do banco de dados. Já o arquivo liquibase.properties que também já está na imagem anardy/liquibase, contém somente a configuração das três primeiras linhas, conforme mostrado abaixo:

changeLogFile=changelog.xml
driver: org.mariadb.jdbc.Driver
classpath: ./lib/mariadb-java-client-2.3.0.jar

O arquivo changelog.xml deve ser evoluído gradativamente, portanto iremos versioná-lo dentro da pasta liquibase. Porém, precisamos levar o conteúdo deste documento para dentro da imagem anardy/liquibase durante a execução do pipeline. Já o arquivo liquibase.properties precisa passar as informações do banco de dados, que são: hostname, username e password. Mas não podemos deixar essas informações em texto puro no arquivo, muito menos versionar no controle de versão.

Temos dois problemas com os dois arquivos, e para resolvê-los criamos um novo Shell Script, o lb.sh, que adiciona conteúdo nos arquivos dentro da imagem anardy/liquibase. Abaixo, segue o conteúdo do script lb.sh:

#!/bin/bash

PROPERTIES_FILE=/liquibase/liquibase.properties
CHANGELOG_FILE=/liquibase/changelog.xml

echo    >>  /liquibase/liquibase.properties
echo "url: jdbc:mariadb://${IP_DB}:3306/${NOME_DB}?allowMultiQueries=true&createDatabaseIfNotExist=true"  >>  $PROPERTIES_FILE
echo "username: ${USER_DB}"                                     >>  $PROPERTIES_FILE
echo "password: ${PASS_DB}"                                     >>  $PROPERTIES_FILE

cat ./lb/changelog.xml >> $CHANGELOG_FILE

Neste Shell Script também utilizamos o recurso de variáveis do GitLab, para não deixar visíveis as informações sensíveis versionadas no controle de versão.

Voltando para o arquivo .gitlab-ci.yml, nas linha 6 e 7, antes de executar as ações da etapa alteramos a permissão do script lb.sh para execução.

Já nas linhas 8 a 13, executamos o script lb.sh descrito anteriormente e executamos o Liquibase com três comandos. Primeiro criamos o estado da alteração através da tag com o comando ./liquibase tag $CI_CONCURRENT_ID, onde $CI_COMMIT_SHA é o ID do job no Gitlab-CI. Em seguida executamos a limpeza dos checkSums ./liquibase-sdk.sh clearCheckSums e por fim o comando ./liquibase-sdk.sh update executa o Liquibase no banco de dados.

Para finalizar, das linhas 15 a 24 temos a parte do rollback. Na linha 15 linha temos o nome da etapa, na linha 16, a etapa do pipeline e, na linha 17, indicamos a imagem que queremos executar na etapa.

Nas linhas 18 e 19, antes da execução do Liquibase alteramos a permissão do script lb.sh para execução. E da linha 20 a 23, é executado o rollback com o comando ./liquibase rollback $CI_COMMIT_SHA.

Porém este estágio de rollback só será executado em caso de falha, conforme indicamos na linha 24.

Desta forma concluímos a nosso pipeline conforme a imagem abaixo:

Abaixo, nosso arquivo .gitlab-ci.yml completo:

image: maven:latest

stages:
 - build
 - test
 - package
 - deploy
 - liquibase
 - liquibase_rollback

build:
 stage: build
 script:
   - mvn compile

test:
 stage: test
 script:
   - mvn test

package:
 stage: package
 script:
   - mvn -DskipTests=true package
 artifacts:
   paths:
 	- target/quiz.war

deploy:
 stage: deploy
 dependencies:
   - build
   - test
   - package
 image: anardy/wildfly
 before_script:
   - chmod +x ./deploy.sh
 script:
   - ./deploy.sh

liquibase:
  stage: liquibase
  image: anardy/liquibase
  before_script:
	- chmod +x ./lb/lb.sh
  script:
	- ./lb/lb.sh
	- cd /liquibase
	- ./liquibase tag $CI_COMMIT_SHA
	- ./liquibase clearCheckSums
	- ./liquibase update

liquibase_rollback:
  stage: liquibase_rollback
  image: anardy/liquibase
  before_script:
	- chmod +x ./lb/lb.sh
  script:
	- ./lb/lb.sh
	- cd /liquibase
	- ./liquibase rollback $CI_COMMIT_SHA
  when: on_failure

Agradecimentos

Gostaria de agradecer a Ângelo Rafael da Silva, Edivan Sousa Júnior, Henrique Lages Repulho e Samuel Barreto pela ajuda na elaboração deste artigo.

Sobre o autor

André Mack Nardy é Arquiteto de Solução na TecBan, onde elabora arquiteturas de soluções no setor financeiro e é líder técnico de um dos times responsáveis em realizar exploração de novas tecnologias dentro da TI.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT