Integración continua con Jenkins Pipeline

Desde principios de 2017, estoy inculcando en mi empresa la importancia y la necesidad de tener un entorno de CI consolidado. Empecé instalando un Jenkins en un pequeño servidor perdido y ahora mismo he conseguido un proyecto para implantar integración continúa dentro de todos los equipos de desarrollo que hay.

En este post me gustaría plasmar muchos de los conocimientos adquiridos.

Definición del Workflow

En primer lugar, hemos definido un Workflow por el que tendrán que pasar todos los proyectos para asegurarnos en todo momento  que esos proyectos cuentan con la calidad necesaria y además para realizar despliegues automáticos en pre-producción y producción.

Os muestro un esquema general del workflow propuesto:

 

Workflow

Como resumen rápido, los pasos del workflow serían:

  1. Descargar el código
  2. Compilar
  3. Ejecutar los tests
  4. Realizar un análisis de calidad de código, comprobar la cobertura y análisis de seguridad.
  5. Despliegue en pre-producción
  6. Confirmación por parte humana.
  7. Realizar un tag del código
  8. Despliegue en producción
  9. Notificar cambios en JIRA

Implementación del Workflow

Para implementar este workflow de integración continúa, creo que la herramienta Jenkins en su versión 2 es la herramienta perfecta para ello ya que permite crear «Pipelines» o workflows con ficheros en lenguaje groovy con todas las ventajas que eso tiene.

Así que me puse manos a la obra y empecé a trabajar con Jenkins Pipeline  no sin darme algún que otro quebradero de cabeza.

Elegí Maven como herramienta principal ya que con diferentes plugins me permite realizar la mayoría de las acciones del workflow. También podría haber utilizado los plugins de Jenkins pero no todos están actualizados para dar soporte de pipeline.

Adjunto pipeline desarrollado.

#!groovy

pipeline {
     agent any    //Agente de Docker, de momento no utilizo Docker
     tools { //Alias a herramientas instaladas en Jenkins
        maven 'M3' //M3 es el nombre que le puse al maven instalado para Jenkins
        jdk 'JDK8' //JDK8 es el nombre que le puse al java de Jenkins
    }
    options {
        //Si en 3 días no ha terminado que falle.
        timeout(time: 76, unit: 'HOURS') 
    }
    environment {
        //variable con el nombre del proyecto
        APP_NAME = 'My-App'
    }
    stages { //Inicio fases del workflow
       stage ('Initialize') { //Primer paso, notificar inicio workflow
             steps {
                  slackSend (message: 'Inicio ejecucion ' + APP_NAME, channel: '#jenkins', color: '#0000FF', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXX' )
                  hipchatSend (color: 'GRAY', failOnError: true, notify: true, message: 'Inicio ejecucion ' + APP_NAME + ' <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', v2enabled: true,  room: 'Jenkins' )
            }
       }
       stage ('Build') { //Compilamos el proyecto
            steps {
                 bat "mvn -T 4 -B --batch-mode -V -U -e -Dmaven.test.failure.ignore clean package -Dmaven.test.skip=true"
            }
       }
       stage ('Test') {
            //Fase de tests. En paralelo tests automaticos y de rendimiento
            steps {
                 parallel 'Integration & Unit Tests': {
                     bat "mvn -T 4 -B --batch-mode -V -U -e test"
                 }, 'Performance Test': {
                     bat "mvn jmeter:jmeter"
                 }
           }
       }
       stage ('QA') {
       //Fase de QA. En paralelo Sonar, Cobertura y OWASP
           steps {
                parallel 'Sonarqube Analysis': {
                    //Si quieres ver la cobertura en sonar es necesario ejecutar cobertura y después sonar
                    bat "mvn -B --batch-mode -V -U -e org.jacoco:jacoco-maven-plugin:prepare-agent install -Dmaven.test.failure.ignore=true"
                    bat "mvn -B --batch-mode -V -U -e sonar:sonar"
                    echo 'Sonarqube Analysis'
               }, 'Cobertura code coverage' : {
                    //Realizamos análisis de cobertura de código
                    //Si la cobertura de código es inferior al 80% falla la ejecución y falla el workflow
                    bat "mvn -B --batch-mode -V -U -e verify"
               }, 'OWASP Analysis' : {
                    bat "mvn -B -X --batch-mode -V -U -e dependency-check:check"
               }
          }
          //Tras ejecutar los pasos en paralelos guardo el reporte de tests
          post {
               success {
                    junit 'target/surefire-reports/**/*.xml' 
               }
          }
      }
      stage ('Deploy to Pre-production environment') {
      //Desplegamos en el entorno de Pre-Producción
      //Se despliega en un tomcat con el plugin Cargo
           steps {
                bat "mvn -B -P Desarrollo --batch-mode -V -U -e clean package cargo:redeploy -Dmaven.test.skip=true"
           }
      }
      stage ('Confirmation') {
      //En esta fase esperamos hasta que la persona configurada confirme que desea subir a Producción. 
      //Tiene 72 horas para confirmar la subida a Producción.
      //Se envían notificaciones para que la persona tenga constancia
           steps {
                slackSend channel: '@dromeroa',color: '#00FF00', message: '\u00BFDeseas subir a produccion?. \n Confirma en la siguiente web: ${BLUE_OCEAN_URL}' , teamDomain: 'my-company', token: 'XXXXXXXXXXX'
                hipchatSend (color: 'YELLOW', failOnError: true, notify: true, message: '\u00BFDeseas subir a producci\u00F3n\u003F. \n Confirma en el siguiente <a href="${BLUE_OCEAN_URL}">Enlace</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
                timeout(time: 72, unit: 'HOURS') {
                    input '\u00BFContinuar con despliegue en producci\u00F3n\u003F'
                }
           }
      }
      stage ('Tagging the release candidate') {
           //Realizamos un tag en SVN del código fuente
           steps {
               //Tagging from trunk to tag
               echo "Tagging the release Candidate";
               bat "mvn -B --batch-mode -V -U -e scm:tag -Dmaven.test.skip=true"
          }
      }
      stage ('Deploy to Production environment') {
           //Comenzamos a subir a producción en los dos servidores 
           steps {
                parallel 'Server 1': {
                    //Necesitamos realizar reintentos ya que falla la subida en remoto y se producen colisiones
                    retry(6) {
                        bat "mvn -T 4 -B -P Produccion --batch-mode -V -U -e tomcat7:redeploy -Dmaven.test.skip=true"
                    }
                }, 'Server 2' : {
                    retry(6) {
                        bat "mvn -T 4 -B -P Produccion --batch-mode -V -U -e tomcat:redeploy -Dmaven.test.skip=true"
                    }
                }
           }
      }
      stage ('CleanUp') {
      //Limpiamos el workspace para no llenar los discos
           steps {
                deleteDir()
           }
      }
    } //Fin de las fases del workflow
    //Inicio de acciones post ejecución del workflow
    //Notificamos como ha sido la ejecución del workflow
    post {
      success {
           slackSend channel: '#jenkins',color: '#00FF00', message: APP_NAME + ' ejecutado satisfactoriamente.', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXXX'
           hipchatSend (color: 'GREEN', failOnError: true, notify: true, message: APP_NAME + ' ejecutado satisfactoriamente. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
      failure {
           slackSend channel: '#jenkins',color: '#FF0000', message: APP_NAME + ' se encuentra en estado fallido. ${BLUE_OCEAN_URL}', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXX'
           hipchatSend (color: 'RED', failOnError: true, notify: true, message: APP_NAME + ' se encuentra en estado fallido. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
      unstable {
           slackSend channel: '#jenkins',color: '#FFFF00', message: APP_NAME + ' se encuentra en estado inestable. ${BLUE_OCEAN_URL}', teamDomain: 'my-company', token: 'XXXXXXXXXXXXXXXXXXXX'
           hipchatSend (color: 'RED', failOnError: true, notify: true, message: APP_NAME + ' se encuentra en estado inestable. <a href="${BLUE_OCEAN_URL}">Enlace a la ejecuci\u00F3n</a>', textFormat: true, v2enabled: true, room: 'Jenkins')
      }
    }
   }

Se puede comprobar que el workflow permite automatizar un montón de tareas que el desarrollador tendría que realizar en lugar de dedicarse a desarrollar, y además dichas tareas son susceptibles a fallos, con lo cuál eliminamos esa posibilidad.  También podemos imponer una serie de reglas de calidad en el código que han de ser cumplidas si se quiere realizar un despliegue de una nueva versión.

A continuación, muestro como se ve en Jenkins una ejecución del Workflow en Jenkins. Nuestro Jenkins tiene instalado el plugin Blue Ocean que cambia la interfaz de Jenkins.

Workflow2

Próximos pasos:

  • Integrar con Docker
  • Integrar con Ansible para realizar puestas en producción de Artefactos JAR
  • Integrar con JIRA para notificar al cliente que se ha desplegado una nueva versión.

En próximos artículos mostraré como han ido las próximas integraciones y como realizarlas.

Este post muestra una visión rápida y a «grosso modo» de como se ha realizado el proceso de instaurar una política de integración continua. Quedarían detalles por explicar como la configuración de los plugins de Maven o la instalación de Jenkins.

Finalizo este post invitando a cualquier persona que tenga alguna duda a dejar un comentario para que pueda ayudar en la medida de lo posible. 🙂

Enlaces de interés:

2 comentarios en “Integración continua con Jenkins Pipeline

Deja un comentario