Integração continua com jenkins e docker

O objetivo deste post é demonstrar uma forma de integração continua de aplicações Java utilizando o Jenkins e Docker, ao final do post o Jenkins será capaz de verificar alterações de um repositório Git, compilar e fazer deploy da aplicação em um container docker.

Pré-requisitos

É preciso ter o docker rodando no seu ambiente caso deseje executar os passos deste post. No momento em que foi escrito a versão do docker utilizado foi a 18.04.0-ce.

Jenkins

Para subir um jenkins local utilizei a última versão do da imagem disponível no jenkins utilizando o comando abaixo:

docker run -p 8080:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home -v \
/var/run/docker.sock:/var/run/docker.sock --name jenkins-ci jenkins/jenkins:lts

Mais informações sobre a imagem do jenkins pode ser encontrada no github da imagem oficial, lá pode ser encontrado vários exemplos para subir o container.
Ao subir o container pela primeira vez o jenkins exibe no log do container uma senha para o usuário admin, precisaremos dessa senha para configurar o jenkins em nosso primeiro acesso.
Após concluir a inicialização do container este pode ser acessado pela url http://localhost:8080, ao abrir o link no navegador será preciso informar a senha de administrador que pegamos no log do jenkins.
Na etapa seguinte, podemos selecionar a opção Install suggested plugins, após a instalação podemos definir um novo usuário para o jenkins. Posteriormente faremos a instalação de outros plugins necessários para o desenvolvimento do post.

Após o processo de setup do jenkins seremos levados a tela inicial que neste momento não deve ter nenhum job criado. Faremos a criação de nosso job para a integração continua.

Para este post, usaremos o pipeline do jenkins para orquestrar nossas atividades - baixar, construir e subir container.
Ao criar o novo job, somos levados a tela de configuração do projeto no jenkins. A parte que nos interessa neste instante é a Pipeline, onde digitaremos o script de pipeline com os estágios que desejaremos executar.

Facilita fazer um resumo do que se deseja fazer, até para que fique mais fácil estruturar o pipeline. Pretendemos executar os passos abaixo:

  1. Baixar o projeto que queremos buildar do git
  2. Construir nosso projeto
  3. Gerar uma imagem docker do aplicativo
  4. Inicializar um container para servir o aplicativo

Iniciaremos a criação de nosso script com o trecho abaixo:

node {

}

1. Baixar o projeto que queremos buildar do git

Neste passo iremos baixar uma aplicação Java utilizando Springboot do github, para isso criaremos um novo estágio dentro do nosso pipeline:

stage('git pull') {
    git url: 'https://github.com/caiogallo/ci-sample.git', branch: 'master'
}

Este estágio é bem simples, baixamos a branch master do nosso projeto de exemplo. Após a configuração deste estágio já e possível executar nosso pipeline e verificar no log do jenkins as linhas referentes ao clone do nosso repositório.

[Pipeline] { (git pull)
[Pipeline] git
Cloning the remote Git repository
Cloning repository https://github.com/caiogallo/ci-sample.git
 > git init /var/jenkins_home/workspace/ci-post # timeout=10
Fetching upstream changes from https://github.com/caiogallo/ci-sample.git
 > git --version # timeout=10
 > git fetch --tags --progress https://github.com/caiogallo/ci-sample.git +refs/heads/*:refs/remotes/origin/*
 > git config remote.origin.url https://github.com/caiogallo/ci-sample.git # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url https://github.com/caiogallo/ci-sample.git # timeout=10
Fetching upstream changes from https://github.com/caiogallo/ci-sample.git
 > git fetch --tags --progress https://github.com/caiogallo/ci-sample.git +refs/heads/*:refs/remotes/origin/*
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision 4fa9aad7a3d05172714be88341d5206675162928 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 4fa9aad7a3d05172714be88341d5206675162928
 > git branch -a -v --no-abbrev # timeout=10
 > git checkout -b master 4fa9aad7a3d05172714be88341d5206675162928
Commit message: "initial commit"
First time build. Skipping changelog.
[Pipeline] }

2. Construir nosso projeto

Após baixado precisamos compilar nosso projeto e para isso utilizaremos o maven, para isso iremos criar um novo estágio em nosso pipeline:

stage('build'){
    withMaven(maven: 'maven'){

    }
}

Neste novo estágio informamos que será utilizado a ferramenta maven que no nosso jenkins será configurado com o nome maven.
Para fazer isso devemos configurar o nosso maven no jenkins, isso pode ser feito na opção Manage Jenkins/Global Tool Configuration. Na sessão Maven clique no botão Add Maven. No campo nome deveremos informar o mesmo que utilizamos no nosso pipeline (no caso, maven mesmo), deixar marcada a opção Install automatically e a versão selecionada por ser a mais recente disponível. Feito isso, o maven será instalado na primeira vez que será executado. Também será preciso instalar o plugin Pipeline Maven Integration, que reconhece a instrução WithMaven… dentro do pipeline.

Feito isso, voltamos ao nosso pipeline e adicionamos o comando para o build dentro do pipeline:

sh 'mvn clean package'

Ao final, o estágio de build deverá estar parecido com este

stage('build'){
    withMaven(maven: 'maven'){
        sh 'mvn clean package'
    }
}

Após salvar e executar o pipeline, é possível ver no log do jenkins as linhas referentes a execução do maven.

3. Gerar uma imagem docker do aplicativo

Para criar o container da nossa aplicação, primeiro precisamos gerar nova imagem docker adicionando o jar de nossa aplicação. Para criar a imagem docker precisamos de um Dockerfile. Eu coloquei esse Dockerfile na raiz da aplicação de exemplo:

FROM java:8
ADD target/ci-sample-0.0.1-SNAPSHOT.jar /app.jar
CMD ["java", "-jar", "/app.jar"]

Com o dockerfile criado e comitado no git, chegou a hora de voltarmos ao jenkins. Nele criaremos um novo estágio para construir essa imagem docker que posteriormente iremos utilizar para subir a aplicação.
Devemos agora criar um novo estágio no jenkins:

stage('Build docker images'){
    docker.withTool('docker'){
        def app = docker.build 'ci-app-image'
    }
}

Antes de executar esse estágio é preciso adicionar o docker no jenkins com o nome docker, assim como fizemos com o maven. Ele também pode ser colocado para download automático e selecionado a versão mais nova.

4. Inicializar um container para servir o aplicativo

Chegou a hora de criar um estágio novo para subir nossa aplicação. Vamos adicionar mais um stage no pipeline para fazer isso.

stage('Start containners'){
    docker.withTool('docker'){
        docker.image("ci-app-image")
                .run('-p 8090:8080 --name ci-sample')
    }
}

Ao subir o container mapeamento a porta 8080 que é padrão do Springboot para a porta 8090 do nosso host, isto foi feito para evitar conflito com a porta do jenkins.
Após a conclusão dessa etapa se executarmos o comando docker ps no terminal um novo container deverá ser exibido com o nome ci-sample

-asciicast

Ainda temos um detalhe a acertar no nosso pipeline. Após executar com sucesso a primeira fez as execuções seguintes passarão a não funcionar, isto porque ele irá tentar criar um container com o mesmo nome de um já existente. Esse problema pode ser resolvido parando e removendo o container

stage('Stop and remove containers'){
    docker.withTool('docker'){
        sh '''
            NAME=ci-sample
            
            CONTAINER=`docker ps -a -q -f name=$NAME`
            echo container: $CONTAINER
            if [ -z $CONTAINER ]
            then
                echo "container $NAME not running"
            else
                docker stop $CONTAINER
                docker rm $CONTAINER
            fi
            
        '''
    }
}

No trecho de código acima utilizei shell script para verificar se o container existe e, em caso afirmativo, parar e removê-lo.
Com isso terminamos a parte do nosso pipeline restando somente a configuração do pulling do git. Aqui vou configurar para que o jenkins verifique alteração de código a cada 1 minuto no git.
Para fazer isso basta habilitar a opção Poll SCM no parte Build Triggers do jenkins. Será solicitado para informar a periodicidade de execução, utilizarei a instrução do cron * * * * * para execução a cada minuto.

E com isso concluímos nosso job, a cada push na branch master do projeto um novo build do jenkins deverá ser iniciado subindo a versão recente da aplicação. O código completo do pipeline do jenkins pode ser encontrado aqui.