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.
Liquibase
by Douglas Parnoff,
Liquibase
by Douglas Parnoff,
Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!
Excelente!
Farei meu hands on!
Pergunta: para um bd que já existe, porém sem o gerenciador de versão, os passos para usar o Liquibase são os mesmos?
Valeu!