Como criar um pipeline de Integração Contínua no Jenkins + Docker

Com a adoção do Ágil pelas equipes de desenvolvimento, os desenvolvedores passaram a fazer alterações no código várias vezes ao dia. Tendo em vista este cenário, vamos imaginar o que seria necessário para que uma equipe mantenha a qualidade do software.

A cada incremento no código:

  1. O desenvolvedor teria que avisar o time sobre a alteração;
  2. Uma tarefa de teste teria que ser criada para um QA/Tester;
  3. O QA teria que fazer o download do repositório do projeto em sua máquina e montar o ambiente;
  4. Executar os comandos de teste de teste unitário;
  5. Executar os comandos de teste de testes de integração;
  6. Analisar relatórios gerados e anexá-los a tarefa;
  7. Encerrar a tarefa.

E se um bug fosse encontrado? Após a correção todo o processo teria de ser executado novamente.

Provavelmente encontraremos problemas como: testes longos e lentos, falhas de comunicação entre a equipe, muito tempo gasto em depuração, e o principal, demora na entrega de valor para o cliente.

Para solucionar este problema, ferramentas de Integração Contínua como o Jenkins foram criadas. Com essas ferramentas é possível automatizar este processo a cada alteração feita no código, permitindo que as equipes detectem erros rapidamente e os localizem mais facilmente.

O objetivo deste passo a passo é mostrar uma maneira de configurar um pipeline usando o Jenkins com foco apenas em Integração Contínua. É uma forma eficiente de manter o time informado através de relatórios e notificações.

Existem diferentes maneiras de criar pipelines no Jenkins. Os pipelines declarativos fornecem uma sintaxe simplificada e com instruções específicas.

Criando Jobs

Criar jobs é a principal função do Jenkins. Simplificando, você pode pensar em um job como uma tarefa específica ou passo que será executado. Isso pode envolver simplesmente compilar seu código e executar seus testes de unidade. Ou você pode querer que um trabalho de construção execute outras tarefas relacionadas, como executar seus testes de integração, medir a cobertura de código ou métricas de qualidade de código.

Para criar um job no Jenkins, devemos selecionar a opção New Item:

Depois devemos criar um nome e selecionar o tipo de pipeline:

Nosso script será inserido na seção Pipeline, conforme a imagem abaixo:

Importante: Use e abuse da opção Pipeline Sintax para obter ajuda ao criar o script, usando função Snippet Generator.

Plugins

Os plugins são o principal meio de aprimorar a funcionalidade de um ambiente Jenkins para atender às necessidades específicas da organização ou do usuário. Para entender como instalar e gerenciar plugins no Jenkins, consulte a documentação através do link: https://jenkins.io/doc/book/managing/plugins/ .

Serão necessários os seguintes plugins:

Explicar o funcionamento de cada plugin não está no escopo deste post, mas você pode consultar suas respectivas documentações nos links descritos acima.

Slack Notification

Faremos uma simples configuração para que o plugin do Slack funcione corretamente. Para isto, acesse o menu Manage Jenkins > Configure System.

Insira as informações do seu Workspace e Channel no Slack:

Criando a estrutura do Pipeline

Os pipelines declarativos devem ser colocados dentro de um bloco de pipeline, por exemplo:

pipeline {
    /* colocar seu Declarative Pipeline aqui */
}

Dentro deste bloco, devemos incluir o código referente às seções do seu pipeline. Elas irão definir sua estrutura e como será executado. Neste caso, usaremos as seguintes:

  • Agent
  • Stages & Steps
  • Post

Agent

A seção agent especifica que o Pipeline inteiro, será executado em um container Docker no ambiente Jenkins.

O parâmetro image faz download de uma imagem Docker do maven: 3-alpine (caso não exista na sua máquina), e executa essa imagem como um contêiner separado.

Este contêiner Maven se torna o a máquina que Jenkins usará para executar seu projeto de Pipeline. No entanto, esse contêiner tem vida curta, ou seja, é criado durante o início e depois é removido no final do seu pipeline.

O parâmetro args cria um mapeamento entre os diretórios /root/.m2 (repositório Maven) no contêiner Maven Docker e o do sistema de arquivos do host.

pipeline {
	agent {
		docker {
			image 'maven'
			args '-v /root/.m2:/root/.m2'
		}
	}
}

Stages

A seção stages permite gerar diferentes estágios em seu pipeline que serão visualizados como segmentos diferentes quando a tarefa for executada.


pipeline {
	agent {
		docker {
			image 'maven'
			args '-v /root/.m2:/root/.m2'
		}
	}
	stages {
		/* colocar os estágios do seu pipeline aqui */
	}
}

Stage e Steps

Pelo menos uma seção stage deve ser definida na seção stages. Dentro desse estágio estarão as instruções que o pipeline executará. Os estágios devem ser nomeados, pois o Jenkins exibirá cada um deles em Stage View.

Stage View

A seção steps está contida dentro do bloco stage, e define uma série de uma ou mais etapas a serem executadas em uma determinada diretiva de estágio.

pipeline {
	agent {
		docker {
			image 'maven'
			args '-v /root/.m2:/root/.m2'
		}
	}
	stages {
		stage('Checkout') {
			steps {
				/* colocar os jobs do estágio de Checkout aqui */
			}
		}
		stage('Build + Unit tests') {
			steps {
				/* colocar os jobs do estágio de Build e testes unitários aqui */
			}
		}
		stage('Archiving Reports') {
			steps {
				/* colocar os jobs do estágio de arquivamento de relatórios aqui */
			}
		}
		stage('BDD tests job'){
			steps {
				/* colocar os jobs do estágio de execução de BDD aqui */
			}
		}
	}
}

Post

A seções Post define uma ou mais etapas adicionais que são executadas após a conclusão de uma execução de pipeline ou estágio (dependendo do local da seção do post no pipeline). O post pode suportar qualquer um dos seguintes blocos de pós-condição: alwayschangedfixedregressionabortedfailuresuccessunstableunsuccessful, e cleanup. Esses blocos de condições permitem a execução de etapas dentro de cada condição, dependendo do status de conclusão do pipeline ou estágio.

Utilizaremos o bloco always, pois queremos que as etapas sejam executadas, independentemente do status de conclusão da execução do pipeline ou do estágio.

A estrutura final de nosso pipeline será a seguinte:

pipeline {
	agent {
		docker {
			image 'maven'
			args '-v /root/.m2:/root/.m2'
		}
	}
	stages {
		stage('Checkout') {
			steps {
				/* colocar os jobs do estágio de Checkout aqui */
			}
		}
		stage('Build + Unit tests') {
			steps {
				/* colocar os jobs do estágio de Build e testes unitários aqui */
			}
		}
		stage('Archiving Reports') {
			steps {
				/* colocar os jobs do estágio de arquivamento de relatórios aqui */
			}
		}
		stage('BDD tests job'){
			steps {
				/* colocar os jobs do estágio de execução de BDD aqui */
			}
		}
	}
	post {
		always {
			/* colocar as ações do bloco post aqui */
		}
	}
}

Implementando os Steps em cada Stage

Para implementar os setps você pode utilizar a opção Snippet Generator do próprio Jenkins, o que facilita na geração dos códigos.

Checkout

Nosso primeiro step será o Checkout, que irá fazer um “git clone” no repositório do nosso projeto no github:


stage('Checkout') {
   steps {
      slackSend channel: 'jenkins-ci', color: '#33AFFF', message: "*STARTED*: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'\n *More info at:* ${env.BUILD_URL}", teamDomain: 'devteam', tokenCredentialId: 'slack'
      git branch: 'dev', credentialsId: 'github', url: 'https://github.com/murillowelsi/repos/ms-notification'
    }
}

O comando slackSend será utilizado para enviar uma notificação no slack da equipe assim que o job for iniciado. Como parâmetros passaremos:

  • channel (canal do slack)
  • color (cor do marcador da mensagem no slack)
  • message (mensagem enviada no slack).
Notificação de Start no pipeline

Para fazer download do repositório, iremos usar o comando git, informando os parâmetros:

  • branch (branch escolhida)
  • credentialsId (chave gerada pelo Snippet Generatordo Jenkins)
  • url (endereço do repositório no git).

Após criar o estágio de checkout, podemos executar nosso pipeline, clicando em Build Now.

Após a execução, a coluna Checkout será exibida no Stage View e, a cada execução com sucesso, será mostrada na cor verde:

Testes Unitários

Neste estágio do pipeline, iremos executar os testes unitários.

Como o projeto está escrito na linguagem Java, iremos usar o Maven para compilar e executar os testes com o comando sh (comando executado no shell) mvn clean test.

stage('Unit tests') {
  steps {
    sh 'mvn clean test'
  }   
}

Depois de adicionarmos o estágio de testes unitários, podemos executar nosso pipeline, clicando novamente em Build Now.

Note que agora uma nova coluna de testes unitários foi adicionada ao Stage View.

Relatórios de Code Coverage

Para armazenar e publicar os relatórios testes unitários no Jenkins utilizaremos os comandos publishHTML, indicando os parâmetros:

  • reportDir (diretório onde o relatório do jacoco é armazenado)
  • reportFiles (arquivo de relatório)
  • reportName (nome do relatório)
  • testResults (arquivo de resultados de teste)

stage('Archiving Reports') {
    steps {
        dir(path: '.') {
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'target/site/jacoco/', reportFiles: 'index.html', reportName: 'Code Coverage', reportTitles: 'Code Coverage'])
            step([$class: 'JUnitResultArchiver', testResults: 'target/surefire-reports/TEST-*.xml'])
        }
    }   
}

Após executarmos nosso pipeline novamente, a coluna de arquivamento de relatórios será exibida no Stage View.

Agora será exibido um gráfico que nos possibilita uma visualização rápida sobre falhas/sucesso de testes unitários.

Para acessar os relatórios detalhados de cobertura de testes, selecione a opção Code Coverage no menu lateral esquerdo:

Code Coverage Report

Execução de testes de integração

Podemos executar neste mesmo pipeline os testes integrados. Neste caso, os testes automatizados desenvolvidos pelos QAs da equipe estão armazenados em um repositório diferente do repositório de código dos Desenvolvedores. Faremos então o download do nosso repositório de testes integrados usando o comando git novamente, mas dessa vez indicando o repositório da equipe de QA.

Para executar os testes do cucumber utilizaremos o comando sh (comando executado no shell) mvn clean install.

O comando cucumber será utilizado para gerar os relatórios dos cenários de teste do BDD, através do plugin Cucumber Reports.

stage('BDD tests'){
    steps {
        git credentialsId: 'github', url: 'https://github.com/murillowelsi/repos/bdd-tests'
        sh 'mvn clean install'
        cucumber failedFeaturesNumber: -1, failedScenariosNumber: -1, failedStepsNumber: -1, fileIncludePattern: 'target/*.json', pendingStepsNumber: -1, skippedStepsNumber: -1, sortingMethod: 'ALPHABETICAL', undefinedStepsNumber: -1                       
    }
}    

Após executarmos novamente nosso pipeline, no Stage View teremos uma nova coluna referente aos testes integrados:

Podemos visualizar os relatórios do cucumber clicando na opção Cucumber Reports:

Post script

Para finalizar, executaremos o post script, para enviar uma mensagem de sucesso ou falha para o time no canal do slack a cada execução do pipeline.

Adicionaremos no parâmetro color o método COLOR_MAP, para que as cores sejam alteradas de acordo com o resultado.

post {
    always {
        slackSend channel: 'jenkins-ci', teamDomain: 'devteam', tokenCredentialId: 'slack',
            color: COLOR_MAP[currentBuild.currentResult],
            message: "*${currentBuild.currentResult}:* Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'\n *More info at:* ${env.BUILD_URL}"
    }
}   
Notificação de sucesso no Slack

Executaremos novamente nosso pipeline e o último estágio de Post Actions será exibido:

Configurando triggers

Agora vamos para a parte mais legal do nosso pipeline. Aqui é onde dizemos ADEUS a todo aquele processo manual que citei no início deste post, e passamos a ser ágeis de verdade!

O pipeline será executado AUTOMATICAMENTE a cada commit feito pelos desenvolvedores no código. Para que isso funcione devemos fazer a seguinte configuração em Configure > Build Triggers:

Marcando a opção Poll SCM o Jenkins buscará novos commits no git, usando a técnica de polling. Em schedule será necessário indicar a frequência que serão feitas as tentativas. Devemos usar expressões de cron (https://crontab.guru/).

Versão final do script

Para finalizar o passo a passo, temos nosso script completo:

def COLOR_MAP = ['SUCCESS': 'good', 'FAILURE': 'danger', 'UNSTABLE': 'danger', 'ABORTED': 'danger']

pipeline {
    agent {
        docker {
            image 'maven'
            args '-v /root/.m2:/root/.m2'
        }
    }
    stages {
        stage('Checkout') {
            steps {
                slackSend channel: 'jenkins-ci', color: '#33AFFF', message: "*STARTED*: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'\n *More info at:* ${env.BUILD_URL}", teamDomain: 'devteam', tokenCredentialId: 'slack'
                git branch: 'dev', credentialsId: 'github', url: 'https://github.com/murillowelsi/repos/ms-test'
            }
        }        
        stage('Build + Unit tests') {
            steps {
                    sh 'mvn clean test'
            }   
        }
        stage('Archiving Reports') {
            steps {
                dir(path: '.') {
                    publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'target/site/jacoco/', reportFiles: 'index.html', reportName: 'Code Coverage', reportTitles: 'Code Coverage'])
                    step([$class: 'JUnitResultArchiver', testResults: 'target/surefire-reports/TEST-*.xml'])
                }
            }   
        }
        stage('BDD tests'){
            steps {
                git credentialsId: 'github', url: 'https://github.com/murillowelsi/repos/bdd-tests'
                sh 'mvn clean install'
                cucumber failedFeaturesNumber: -1, failedScenariosNumber: -1, failedStepsNumber: -1, fileIncludePattern: 'target/*.json', pendingStepsNumber: -1, skippedStepsNumber: -1, sortingMethod: 'ALPHABETICAL', undefinedStepsNumber: -1                       
            }
         }     
    }
    post {
        always {

            slackSend channel: 'jenkins-ci', teamDomain: 'devteam', tokenCredentialId: 'slack',
                color: COLOR_MAP[currentBuild.currentResult],
                message: "*${currentBuild.currentResult}:* Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'\n *More info at:* ${env.BUILD_URL}"
            
        }
    }        
}

Finalmente temos nosso pipeline pronto!

Com os testes sendo executados em uma esteira, eliminamos a necessidade de executar nossa automação manualmente a cada incremento no código.

Bora por isso pra rodar? Até a próxima, pessoal!