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.