Criando um sistema de integração contínua (CI/CD)

Fazendo a integração contínua de Jenkins, Sonatype Nexus, Sonatype, JUnit e Gradle para automatizar processos repetitivos. Prática bastante usada em tecnologias de DevOps.

Postado em 06 julho 2023

Atualizado em 06 julho 2023



Introdução

Esse projeto mostra como automatizar processos repetitivos após modificações de código. Esses processos podem ser teste, build, análise, detecção de bugs e muitas outras coisas. O repositório a ser usado será um sistema de ATM (caixa eletrônico) que foi desenvolvido nesse projeto.

O caixa eletrônico será integrado em um sistema de integração contínua (CI) que é um termo presente em CI/CD. O sistema de integração contínua será configurado na máquina local (localhost) e usará apenas serviços gratuitos de código aberto.

Planejamento do sistema de integração contínua (CI)

Nesse sistema serão usados três serviços de integração:

  • Jenkins
  • Sonatype Nexus Repository
  • Sonarqube

Os três sistemas acima serão configurados na máquina local. Os serviços nexus e sonarqube serão integrados ao jenkins. Quando jenkins iniciar uma tarefa (job), os outros serviços serão acionados.

Queremos que jenkins inicie uma tarefa (job) após enviarmos uma modificação ao repositório remoto (trigger). Uma forma fácil e prática de iniciar tarefas automaticamente é definindo e configurando o endereço em que Jenkins está operando no repositório remoto.

O caixa eletrônico será armazenado no GitHub, cujo serviço será nosso repositório remoto. Pelo fato que vamos operar o Jenkins no nosso ambiente local, ele será acessível apenas na nossa rede interna.

Precisamos disponibilizar o acesso externo ao Jenkins para que ele possa iniciar uma tarefa (job) automaticamente através do GitHub. Para isso, iremos precisar de outro serviço, o webhookrelay. O webhookrelay é um serviço disponível na nuvem que permite a integração de serviços que operam em uma rede interna. Em outras palavras, ele permitirá que Jenkins possa receber instruções de serviços externos mesmo operando em uma rede isolada.

planejamento de um sistema de integração contínua

A imagem acima mostra o ciclo da integração contínua:

  1. O programador envia uma modificação ao repositório remoto no GitHub
  2. Ao receber a modificação, o GitHub aciona o gatilho (trigger), enviando uma mensagem para o webhookrelay
  3. Webhookrelay recebe a mensagem e envia uma notificação para o Jenkins que está operando dentro da rede interna
  4. Jenkins recebe a notificação e inicia uma tarefa (job)
  5. O Job faz o build, executa os testes, analisa o código e armazena o artefato.

Toda modificação enviada ao repositório remoto irá iniciar esse ciclo acima automaticamente.

Instalação dos serviços

Os seguintes serviços precisam ser instalados:

  • Webhookrelay para possibilitar port forwarding
  • Jenkins para gerenciamento dos serviços e das etapas
  • Sonarqube para análise do código
  • Sonatype nexus para o armazenamento de artefatos

Gradle também será necessário para gerenciar o código e as bibliotecas. JUnit será usado para fazer testes. A linguagem de programação usada no caixa eletrônico é java, porém isso não será muito importante nesse projeto.

Esse sistema de integração contínua será rodado em um macOS Montrey versão 12.0.1. Dependendo do sistema operacional, alguns detalhes devem mudar.

Instalando o Jenkins

Jenkins pode ser facilmente instalado no macOS usando o homebrew. Homebrew é um gerenciador de pacotes de código aberto.

$ brew -v
Homebrew 4.0.26-35-g9e257fc

A versão utilizada será a mostrada acima. Usuários de windows podem optar pelo chocolatey.

brew install jenkins-lts

O comando acima é o suficiente para a instalação de jenkins no mac. Para iniciar o serviço usamos os comandos abaixo:

brew services start jenkins-lts

Provavelmente jenkins irá operar na porta 8080. Na primeira vez que o Jenkins for acessado, ele irá exigir a senha inicial. A senha inicial pode ser obtida com o comando abaixo:

cat /Users/$nomedousuario/.jenkins/secrets/initialAdminPassword

O comando acima irá exibir a senha inicial no terminal e permitirá a definição da senha.

Instalação de sonarqube para o análise do código

Sonarqube deve ser baixado diretamente no site oficial. Existe a versão “Community Edition” que é gratuita. Essa é a versão que estaremos usando.

instalação sonarqube

Descompactamos o arquivo e colocamos dentro de um diretório de nossa preferência. No meu caso será em /Users/$nomedousuario. Em seguida, podemos inicializar sonarqube normalmente:

/Users/$nomedousuario/sonarqube-10.1.0.73491/bin/macosx-universal-64/sonar.sh console

Os usuários que usam outro sistema operacional, devem usar o arquivo sh em outro diretório:

$ pwd
/Users/$nomedousuario/sonarqube-10.1.0.73491/bin
$ ls
elasticsearch  linux-x86-64  macosx-universal-64  windows-x86-64  winsw-license

Assim como Jenkins, sonarqube exigirá a criação de uma conta.

Instalação de sonatype nexus para o armazenamento de artefatos

O código do caixa eletrônico será compilado para a linguagem do computador. Alguns módulos de java quando compilados se tornam arquivos jar. Esses arquivos jar serão armazenados no sonatype nexus. Na prática, esses artefatos podem ser baixados por outros membros da equipe para o uso imediato.

Assim como o sonarqube, o sonatype nexus também deve ser instalado pelo site oficial. Após ser baixado, ele deve ser descompactado no local de preferência.

Após a descompactação, sonatype nexus pode ser executado no diretório bin com o comando abaixo:

$ pwd
/Users/$nomedousuario/nexus-3.56.0-01-mac/nexus-3.56.0-01/bin
$ ./nexus start

Após a execução criamos uma conta e prosseguimos com a integração contínua.

Todos os três serviços após serem executados, estarão operando no fundo.

Serviço Endereço e porta
Jenkins localhost:8080
Sonatype Nexus localhost:8081
Sonarqube localhost:9000

Configurando o Jenkins

O repositório que estaremos utilizando já está armazenado no github. Para evitar problemas, estaremos usando outra versão do repositório nesse branch.

Para testar o caixa eletrônico, primeiro temos que escrever o código para o teste. O conteúdo escrito não é muito importante nesse projeto. Basicamente, o conteúdo escrito nas classes de teste será o seguinte:

import org.junit.Test;
import static org.junit.Assert.assertThrows;
import  static org.junit.Assert.assertEquals;

public class TransactionTest {
    @Test
    public void transactionMaximum() {
        byte depositType = (byte) TransactionType.DEPOSIT.index;

        Exception exception = assertThrows(Exception.class,
            () -> TransactionInsertInputValidation.validate("1000", "10000", depositType));
        assertEquals("Quantidade deve ser maior do que zero e menor do que 9999", exception.getMessage());
    }
}

Cada função de teste espera um determinado valor. Caso esse valor não seja o esperado, o teste falha e o erro é reportado no relatório final. O teste é realizado na hora de fazer o build do projeto. Usando o gradle, é possível fazer o teste manualmente:

$ gradle build
BUILD SUCCESSFUL in 22s
7 actionable tasks: 7 executed

Porém, não queremos realizar o teste manualmente. Queremos automatizar esse processo. Para isso, criamos um pipeline no Jenkins.

O pipeline deve ser vinculado ao repositório do GitHub para que possamos obter o conteúdo armazenado (git pull).

Criando o Jenkins com Jenkinsfile

Dentro do diretório absoluto do correio eletrônico, criamos um novo arquivo chamado “Jenkinsfile”. Nesse arquivo configuramos todos os estágios da tarefa (job) em etapas.

Primeiro, criamos a etapa para o build.

node {
    stage('JUnit tests and build') {
        withGradle {
            sh './gradlew build --scan'
        }
    }
}

No código acima adicionamos uma etapa. O comando “gradlew build” tem a mesma função que “gradle build”. A opção “–scan” cria um relatório contendo o resultado dos testes.

Como integrar o sonarqube com Jenkins para escanear o código?

Antes de tudo, na interface de Jenkins (Gerenciar Jenkins > Plugins), instalamos a seguinte extensão:

SonarQube Scanner for Jenkins Versão 2.15

Após adicionar a extensão acima no Jenkins, fazemos a transição para a tela de configurações do sistema (Gerenciar Jenkins > System). Nessa tela, procuramos por “SonarQube servers” e adicionamos a sua configuração. Os detalhes estão documentados no site de sonarqube.

Até aqui, a integração com Jenkins foi realizada. Porém, precisamos configurar o sonarqube para escanear o nosso código e encontrar vulnerabilidades. Dentro do arquivo “build.gradle” adicionamos o plugin de sonarqube:

plugins {
    id "org.sonarqube" version "4.2.1.3168"
}

Ainda no mesmo arquivo, adicionamos algumas propriedades para Sonarqube.

sonar {
    properties {
        property "sonar.projectKey", "ATM"
        property "sonar.projectName", "ATM"
    }
}

A configuração acima será para o uso do gradle. Agora precisamos adicionar algumas etapas no arquivo “Jenkinsfile”.

node {
    stage('SCM') {
        checkout scm
    }

    stage('SonarQube Analysis') {
        withSonarQubeEnv() {
            sh "./gradlew sonar"
        }
    }
    ...
}

A etapa “SCM” é responsável por baixar o conteúdo do projeto dentro do repositório remoto e tornar o código disponível para as próximas etapas. A etapa “SonarQube Analysis” é onde sonarqube entra em ação e faz a checagem no código. A única coisa que falta é criar um projeto no sonarqube e definir a chave do projeto (project key) como definimos no Jenkinsfile (sonar.projectKey).

Como integrar o sonatype nexus com Jenkins?

Sonatype nexus será responsável por armazenar artefatos gerados do aplicativo de caixa eletrônico. Bem parecido com uma nuvem que armazena arquivos.

Adicionamos mais duas etapas no Jenkinsfile, uma para criar o artefato e outra para armazenar o artefato.

node {
    ...
    stage('Assembles jar') {
        withGradle {
            sh './gradlew jar'
        }
    }

    stage('Upload') {
        nexusPublisher nexusInstanceId: 'ATMRespository', nexusRepositoryId: 'maven-releases', packages: [
            [$class: 'MavenPackage', mavenAssetList: [
                [classifier: '', extension: '', filePath: '/Users/$nomedousuario/.jenkins/workspace/ATMSonarqube/app/build/libs/app.jar']
            ], mavenCoordinate: [artifactId: 'atm', groupId: 'com.dicionariotec.atm', packaging: 'jar', version: '1.0.0']]
        ]
    }
}

A etapa “Assembles jar” usa um comando do gradle. A etapa “Upload” usa a extensão do sonatype nexus que adicionaremos daqui pra frente.

Na interface de Jenkins, acessamos a tela de plugins (Painel de controle > Gerenciar Jenkins > Plugins) e instalamos a extensão para sonatype nexus:

Nexus Platform Versão3.16.501.ve3d6b_58f1d37

Após instalar a extensão acima, devemos configurar a conexão entre os dois serviços. Para prosseguir, precisamos ter uma conta criada no Sonatype Nexus Repository que deve estar operando em localhost:8081. Essa conta será usada como credenciais para a conexão entre Jenkins e Sonatype Nexus.

Na tela de configuração do sistema (Gerenciar Jenkins > System), procuramos por “Nexus Repository Manager Servers” e adicionamos um novo servidor de sonatype. Ajuste os valores de acordo com o seu ambiente e contexto.

configuração Sonatype Nexus Repository no jenkins

Após adicionar o servidor, podemos testar a conexão clicando no botão “Test connection”. Se houver problemas, cheque as credenciais e se o serviço está operando corretamente.

Testando o resultado do Jenkins após iniciar a tarefa (job)

Ainda não configuramos o gatilho “trigger” para que a tarefa se inicie automaticamente. Porém, já podemos iniciar o processo manualmente clicando no botão de “build”.

iniciando a tarefa no jenkins

Após apertar o botão acima, as etapas devem ser executadas uma por uma. Caso haja algum problema no meio do caminho o processo é interrompido. Caso não haja problemas, o resultado deve ser semelhante a figura abaixo:

resultado após tarefa jenkins

Agora, a única configuração que está faltando é a automatização da tarefa após a realização de alguma modificação no código do repositório remoto.

Como automatizar uma tarefa no Jenkins após uma modificação no repositório?

Como já descrito no começo desse projeto, temos um pequeno problema em conectar o repositório armazenado no GitHub com o Jenkins que está rodando na nossa rede interna. O GitHub pode ser acessado por qualquer usuário, pois ele está na internet. Porém, Jenkins que está rodando na nossa rede interna só pode ser acessado por usuários que estão conectados dentro da mesma rede interna. Logo, github não pode acessar o nosso endereço (a menos que disponibilizamos a nossa rede para o acesso público). Esse problema pode ser resolvido com um serviço de redirecionamento de portas (Port Forwarding).

Antes de configurar o redirecionamento de portas, precisamos obter um endereço de conexão no Jenkins para possibilitar a conexão externa. Nas configurações da tarefa (job), procure pela opção “Dispare construções remotamente”, e em seguida defina um token arbitrário. Nesse exemplo usaremos “atmsonarqube”. Por isso, o endereço de conexão para trigger será “localhost:8080/job/ATMSonarqube/build?token=atmsonarqube”. Veja a estrutura abaixo:

http://localhost:$portaJenkins/job/$nomedatarefa/build?token=$tokenArbitrario

Após adicionar a configuração de conexão remota acima, podemos testar a conexão usando o curl:

curl -X POST --user $nomeusuario:$tokendousuario http://localhost:8080/job/ATMSonarqube/build?token=atmsonarqube

O parâmetro “tokendousuario” deve ser criado nas configurações do usuário do Jenkins (Painel de controle > $usuario > Configure). Dentro da tela de configuração do usuário procuramos por “Passe de API” e adicionamos um passe de API apertando o botão “Adicionar novo passe”. Um token será criado e será usado no lugar do parâmetro “tokendousuario”. Se não houverem problemas, a tarefa irá iniciar após executarmos o comando curl acima.

GitHub tem uma funcionalidade chamada de “Webhook”. Essa funcionalidade nada mais é do que uma solicitação de envio (POST) para algum endereço acessível (público) que aceita conexão HTTPS. Iremos usar o webhookrelay que é um serviço gratuito que faz redirecionamento de portas através de um endereço público para um endereço dentro de uma rede interna.

O tutorial de configuração de redirecionamento de portas pode ser visualizado no site oficial do serviço. Como descrito no tutorial oficial do webhookrelay, primeiro instalamos o CLI (Command line interface) na nossa máquina com o comando abaixo:

curl https://my.webhookrelay.com/webhookrelay/downloads/install-cli.sh | bash

Após fazer a instalação, criamos um novo “Bucket” na interface do serviço webhookrelay e definimos o endereço destino que é o endereço local da tarefa (job) que criamos (localhost:8080/job/ATMSonarqube/build?token=atmsonarqube). Após terminamos essa configuração, iremos receber um endereço público parecido com o abaixo:

https://rr6stfsa324incas1236p8rqy0.hooks.webhookrelay.com

O endereço acima será o endereço que iremos definir no webhook do github. No documento oficial do GitHub, os webhooks estão descritos detalhadamente. Se você tem um repositório, pode configurar o webhook no endereço abaixo:

https://github.com/$nomedousuario/$nomedorepositorio/settings/hooks

Após adicionar o endereço público que recebemos do webhookrelay no webhook do nosso repositório remoto no github, enviamos uma modificação. Se não houverem problemas, a tarefa (job) se iniciará sozinha.

Agora, toda vez que o repositório sofrer uma modificação ele irá notificar o jenkins que está operando na nossa rede interna.

Conclusão

Esse projeto foi um exemplo de integração contínua (CI). A integração contínua é uma porção do CI/CD que pertence ao DevOps. Essas tecnologias ajudam a automatizar sistemas e mitigar esforços humanos em relação a tarefas repetitivas. Conhecimentos em CI/CD tem sido um requerimento das empresas que contratam programadores. A automatização de tarefas é uma realidade que os desenvolvedores não devem fugir.

Postagens mais vistas

Os 5 principais componentes do computador

Os 5 principais componentes do computador são a unidade de controle, unidade aritmética e lógica, memória, dispositivo de entrada e dispositivo de saída.

Portas TCP e UDP

A porta é um número de 16 bits que é adicionado no final do endereço IP, insinuando qual aplicativo está vinculado e atuando nessa porta.

LAN

Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.

Retornar aos projetos