El proceso de Integración Continua

Integración Continua

La Integración continua se ha vuelto una pieza imprescindible en el desarrollo de software y la metodología ágil. Nosotros lo vemos como un camino o proceso en constante desarrollo. En este artículo se pretende mostrar el proceso CI actual de ZSS, para una Tecnologia dada. Este no es un articulo introductorio ni un tutorial.

Los beneficios que nos provee la integración continua desde un punto de vista práctico son los siguientes:

  • - Resilencia a los cambios: En diversos proyectos, los requerimientos iniciales mutan con el tiempo. Al momento de implementar estos cambios, y muchas veces hacer roll-back de algunos; la integración continua nos ha permitido realizar el cambio en menor tiempo y esfuerzo, así como, con menos riesgo.
  •  
  • - Mitigar incendios: Muchas veces aparecen bugs cuando el software está en producción. Corregirlos se convierte en un imperativo y los equipos comienzan a correr en circulos con antorchas. Al contar con pruebas automatizadas logramos, acotar el impacto del bugfix. Podemos detectar además de manera mas simple los errores al contar con un código ordenado y bien documentado.
  •  
  • - Múltiples entregas periódicas: La metodología ágil nos empuja a rezalizar más entregas de forma periódica. Al contar con la integración continua permite correr el ciclo de pruebas de manera más rápida y automatizada.

 

El proceso de Integración Continua que utilizamos no difiere de los modelos comunes. En el caso presentado en este artículo el despliegue corresponde a subir el artefacto construido a un repositorio de artefactos binarios.

 

 

Aplicación de muestra

Para explicar nuestro proceso vamos a usar una aplicación de muestra expuesta a continuación. Esta se compone de una interfaz WEB para la capa Front, y de un conjunto de API en la capa Back que son consumidos desde el Front que corre en el navegador del usuario. La aplicación que vamos a examinar esta compuesta por dos componentes:

  • - Front: Corresponde a una aplicación con interfaz de usuario web, la cual está escrita en Angular 7 utilizando Typescript. La comunicación con la capa Back se realiza a través de una API RESTFul. Esta aplicación cuenta con test unitarios y el uso de TSLINT. Se utiliza NodeJS y Angular CLI para el desarrollo y build.
  •  
  • - Back: Básicamente expone una API RESTFul a la capa Front. La aplicación está escrita en Java 8 utilizando Spring y Spring Boot. Cuenta con test unitarios basados en Junit y se utiliza checkstyle como linter. La herramienta de build utilizada es Maven.

 

 

Dado estos dos proyectos, se espera poder realizar el siguiente proceso para cada uno de ellos:

  • - Checkout del Código: Obtener la última versión del código disponible. En este caso, el inicio del proceso se da cuando el repositorio recibe un push y emite un webhook a la plataforma de integración.
  •  
  • - Compilar/Realizar Build: Una vez obtenido el código, se valida que compile correctamente y se pueda realizar un build.
  •  
  • - Realizar test unitarios: Se ejecutan los Test Unitarios.
  •  
  • - Aplicar Linter: Se corre el análisis via Linter (checkstyle y tslint), los informes son incorporados al análisis estático de código que es subido a SonarQube para que evalúe el Quality Gates.
  •  
  • - Análisis Estático de Código: Se ejecuta análisis estático de código via SonarQube y el informe se disponibiliza en el servidor.
  •  
  • - Validar Quality Gate: Una vez realizado el análisis estático de código, se espera el resultado del Quality Gate para determinar si el proyecto cumple con el estandard y contnuar con el pipeline.
  •  
  • - Generar Build productivo: Se realiza la construcción del artefacto para un ambiente productivo.
  •  
  • - Desplegar en repositorio de binarios: La versión productiva del artefacto es desplegada en el repositorio de binarios, en este caso Nexus Sonatype.

 

 

El objetivo es poder crear un pipeline utilzando Jenkins, Sonarqube y Sonatype Nexus. Todos los test ejecutados, y los análisis asociados a Sonarqube y los Linter deben estar disponibles para el desarrollador, en las plataformas y en su ambiente local, en todo momento.

 

 

Lo primero que se puede notar es que utilizamos Bitbucket para almacenar nuestro código y que las plataformas instaladas se encuentran en AWS. Como se mecionó el repositorio recibe un push y emite un webhook a la plataforma de integración, en este caso Jenkins. Utilizamos Google conectado con Jenkins y Sonarqube, de forma que usemos nuestros usuarios de Google para acceder a las plataformas. Sin el uso de este SSO, se reduce la usabilidad de la solución.

Lo segundo que salta a la vista es que los IDE están conectados a Sonarqube. Utilizando SonarLint podemos obtener el análisis estático de código, en tiempo real conectado a nuestro servidor central Sonarqube en los IDE de los desarrolladores examinando su código local. Con esto se reduce la iteración del desarrollador para corregir los code smells detectados.

Ahora vamos a revisar que se configuró en cada uno de los servidores de nuestro ambiente. Posteriormente revisaremos el pipeline para Java y luego para Angular así como la configuración de los IDE.

 

Jenkins

Es el motor de Integración Continua. Encargado de ejecutar los pasos descritos. Un punto esencial, sobre todo en el caso de Angular, es que pueda ejecutar contenedores Docker. Los plugin adicionalmente instalados y su uso son los siguientes:

 

 

Dado que el pipeline (JenkinsFile) se encuentra alojado dentro de nuestro controlador de versiones junto con nuestro código, se deben realizar varias configuraciones: configuración del servidor Sonarque, credenciales varias, de modo que queden ofuscadas en los pipeline y los token de SonarQube. También no olvidar configurar la dirección de nuestro server, paso vital para utilizar el Login via Google.

 

 

SonarQube

Es una plataforma para evaluar código fuente. Realiza análisis estático de código y además va a incorporar los análisis de otras herramientas, en este caso CheckStyle y TSLINT. Está encargada de evaluar los Quality Gates. SonarQube viene con la mayoria de los plugin necesarios instalados, salvo por:

 

 

Se puede observar como los informes de SonarQube, contienen sensores propios y externos, como TSLINT en este caso. Un paso importante para lograr la integración entre SonarQube y Jenkins es habilitar el webhook para informar sobre el estado del Quality Gates hacia Jenkins.

 

 

Sonatype Nexus

Es una plataforma que utlizada como repositorio de artefactos. En la fase final de nuestros pipeline, el último paso es desplegarlos en un repositorio de atefactos, con esto, aseguramos la trazabilidad de los binarios entregados al cliente y/o despleguados.

 

 

A la plataforma Nexus, no se le instala ningún plugin adicional. Principalmente se configuran los repositorios específicos, los usuarios y token respectivos.

Back - Aplicación Java

La aplicación Back se encuentra escrita en Java haciendo uso intensivo de Spring y Spring Boot. La herramienta de Build a utilizar es Maven. El pipeline que hemos diseñado es el siguiente:

 

 

Se puede observar que los pasos de pipeline no difieren mucho de los habituales. Un punto interesante es que incorporamos el análisis con checkstyle, que a su vez es subido al reporte en SonarQube. Checkstyle, es un herramienta OpenSource distribuida bajo licencia GNU LGPL. Nosotros utilizamos la plantilla de estilos de Google. Para correr el análisis utilizamos Maven.

 

 

El despliegue sobre Nexus se realiza utilizando el plugin Nexus Artifact Uploader. Se puede apreciar en el código del pipeline la sección enviroment donde se definen coordenadas para realizar el upload. Como ya se mecionó una de las características principales de los pipeline en Jenkins, es poder ofuscar todo tipo de credenciales. El plugin Pipieline Utility Steps es utilizado para parsear el pom.xml del proyecto Maven y obetener la metadata necesaria para el upload.

                
  pipeline {
      agent any
      environment {
       	scannerHome = tool 'Sonar 3.3'
       	jdkHome = tool 'Java Local 8'
       	mavenHome = tool 'Maven Local'
       	// This can be nexus3 or nexus2
          NEXUS_VERSION = "nexus3"
          // This can be http or https
          NEXUS_PROTOCOL = "xxxx"
          // Where your Nexus is running
          NEXUS_URL = "xxxxxxxxxxx"
          // Repository where we will upload the artifact
          NEXUS_REPOSITORY = "zss_repo"
          // Jenkins credential id to authenticate to Nexus OSS
          NEXUS_CREDENTIAL_ID = "ZSS Nexus"
      }
      tools {
          maven 'Maven Local'
          jdk 'Java Local 8'
      }
      stages {
          stage('Build') {
              steps {
                  sh 'mvn -B -DskipTests clean install'
              }
          }
          stage('Test') {
              steps {
                  sh 'mvn -B test'
              }
          }
          stage('Checkstyle') {
              steps {
                  sh 'mvn checkstyle:checkstyle'
              }
          }
          stage('sonar') {
              steps {
                  withSonarQubeEnv('Sonar ZSS') {
                      sh "${scannerHome}/bin/sonar-scanner"
                  }
              }
          }
          stage('Quality Gate') {
              steps {
                  waitForQualityGate abortPipeline: true
              }
          }
          stage("publish to nexus") {
              steps {
              	sh "mvn package -DskipTests=true"
                  script {Permite utilizar las credenciales de Google para autentificarse
                      // Read POM xml file using 'readMavenPom' step , this step 'readMavenPom' is included in: https://plugins.jenkins.io/pipeline-utility-steps
                      pom = readMavenPom file: "pom.xml";
                      // Find built artifact under target folder
                      filesByGlob = findFiles(glob: "target/*.${pom.packaging}");
                      // Print some info from the artifact found
                      echo "${filesByGlob[0].name} ${filesByGlob[0].path} ${filesByGlob[0].directory} ${filesByGlob[0].length} ${filesByGlob[0].lastModified}"
                      // Extract the path from the File found
                      artifactPath = filesByGlob[0].path;
                      // Assign to a boolean response verifying If the artifact name exists
                      artifactExists = fileExists artifactPath;
                      if(artifactExists) {
                          echo "*** File: ${artifactPath}, group: ${pom.groupId}, packaging: ${pom.packaging}, version ${pom.version}";
                          nexusArtifactUploader(
                              nexusVersion: NEXUS_VERSION,
                              protocol: NEXUS_PROTOCOL,
                              nexusUrl: NEXUS_URL,
                              groupId: pom.groupId,
                              version: pom.version,
                              repository: NEXUS_REPOSITORY,
                              credentialsId: NEXUS_CREDENTIAL_ID,
                              artifacts: [
                                  // Artifact generated such as .jar, .ear and .war files.
                                  [artifactId: pom.artifactId,
                                  classifier: '',
                                  file: artifactPath,
                                  type: pom.packaging],
                                  // Lets upload the pom.xml file for additional information for Transitive dependencies
                                  [artifactId: pom.artifactId,
                                  classifier: '',
                                  file: "pom.xml",
                                  type: "pom"]
                              ]
                          );
                      } else {
                          error "*** File: ${artifactPath}, could not be found";
                      }
                  }
              }
          }

      }
  }
                
            

 

El ambiente de desarrollo utilizado para construir la aplicación Back sobre Java, es el clásico Eclipse IDE. Uno de nuestros objetivos principales es que el desarrollador tenga retroalimentación de la calidad del código en todo momento. Para lograr esto incorporamos una serie plugins y configuraciones en Eclipse:

  • - SonarLint: Con SonarLint podemos incorporar los exámenes de código estático de SonarQube. Para mantener coherencia con las reglas utilizadas por la organización lo conectamos a nuestro servidor de SonarQube.
  •  

     

  • - Eclipse-CS: Es un plugin para Eclipse que permite realizar exámenes de código estático utilizando Checkstyle. Como ya se mencionó al revisar el pipeline, es el mismo exámen que posteriormente se sube a SonarQube. Para mantener el mismo estandard se utiliza la plantailla de estilos de Google. Es necesario el uso de este plugin debido a que el análisis por parte de Jenkin se realiza en otro instante de tiempo.
  •  

     

  • - Configurar estilo Google en Eclipse: Al comenzar a utilizar Checkstyle nos dimos cuenta que estabamos violando varias reglas de indetanción y formateo (miles de code smells). Para resolver esto, y no hacerlo a mano, lo adecuado es configurar el formato de Google en el editor de Eclipse; de esta manera, podemos resolver varios code smells con un simple Ctrl+Shift+F.
  •  

     

Front - Aplicación Typescript/Angular 7

La aplicación Front se encuentra escrita en Typescript en base al Framework Angular 7. Hacemos uso extensivo de Angular CLI. El pipeline que hemos diseñado es el siguiente:

 

 

Algunos pasos del pipeline están decorados con el simbolo Docker. La razón de esto es que para realizar las pruebas unitarias via Karma, se encesita un navegador. Existe opciones headless, como Headless Chrome, PhantomJS, pero configurarlas en los distintos ambientes (Jenkins en AWS, máquinas de los desarrolladores) es un trabajo extra en sí. Además, a lo largo del tiempo, nos percatamos, que dependiendo de la versión de NodeJS y otros detalles los resultados de las pruebas y linter, eran distintos entre los diferentes ambientes. Como en la mayoría de los problemas en informática, ya fue resuelto por alguien mas hábil. Cual es la solución? utilizar contenedores en base a Docker. Existe la imagen trion/ng-cli-karma y varios artículos sobre como usarla. Esta imagen nos provee un contenedor con NodeJS, Angular CLI y Karma listos para usar.

 

                
                  pipeline {
                      agent any
                      environment {
                       scannerHome = tool 'Sonar 3.3'
                       jdkHome = tool 'Java Local 8'
                       mavenHome = tool 'Maven Local'
                       // This can be nexus3 or nexus2
                       NEXUS_VERSION = "nexus3"
                       // This can be http or https
                       NEXUS_PROTOCOL = "xxxx"
                       // Where your Nexus is running
                       NEXUS_URL = "xxxxxxxxxxxx"
                       // Repository where we will upload the artifact
                       NEXUS_REPOSITORY = "zss_repo"
                       // Jenkins credential id to authenticate to Nexus OSS
                       NEXUS_CREDENTIAL_ID = "ZSS Nexus"
                      }
                      tools {nodejs 'NodeJS 10.15.3'}
                      stages {
                          stage('install') {
                              agent {
                                  docker {
                                      image 'trion/ng-cli-karma'
                                      registryCredentialsId 'dockercell'
                                      reuseNode true
                                  }
                              }
                              steps {
                                  sh 'npm install'
                              }
                          }
                          stage('test') {
                              agent {
                                  docker {
                                      image 'trion/ng-cli-karma'
                                      registryCredentialsId 'dockercell'
                                      reuseNode true
                                  }
                              }
                              steps {
                                  sh 'node_modules/@angular/cli/bin/ng test --watch=false'
                              }
                          }
                          stage('lint') {
                            agent {
                                docker {
                                    image 'trion/ng-cli-karma'
                                    registryCredentialsId 'dockercell'
                                    reuseNode true
                                }
                            }
                            steps {
                                  sh 'node_modules/@angular/cli/bin/ng lint --format=json --force=true > report.json'
                            }
                          }
                          stage('sonar') {
                              steps {
                                  sh 'truncate -s-3 report.json'
                                  withSonarQubeEnv('Sonar ZSS') {
                                      sh "node_modules/sonar-scanner/bin/sonar-scanner"
                                  }
                              }
                          }
                          stage("Quality Gate") {
                              steps {
                                  waitForQualityGate abortPipeline: true
                              }
                          }
                          stage('build') {
                              agent {
                                  docker {
                                      image 'trion/ng-cli-karma'
                                      registryCredentialsId 'dockercell'
                                      reuseNode true
                                  }
                              }
                              steps {
                                  sh 'node_modules/@angular/cli/bin/ng build --prod'
                              }
                          }
                          stage('nexus') {
                              steps {
                                  script {
                                    def project = readJSON file: "${env.WORKSPACE}/package.json"
                                    def fileExists = new File( "${env.WORKSPACE}/${project.name}.zip" )
                                    if( fileExists.exists() ) {
                                        sh "rm ${project.name}.zip"
                                    } else {
                                        println "File doesn't exist"
                                    }
                                    zip zipFile: "${project.name}.zip", archive: false, dir: "dist/${project.name}"
                                    filesByGlob = findFiles(glob: "${project.name}.zip");
                                    artifactPath = filesByGlob[0].path;
                                    nexusArtifactUploader(
                                        nexusVersion: NEXUS_VERSION,
                                        protocol: NEXUS_PROTOCOL,
                                        nexusUrl: NEXUS_URL,
                                        groupId: project.name,
                                        version: project.version,
                                        repository: NEXUS_REPOSITORY,
                                        credentialsId: NEXUS_CREDENTIAL_ID,
                                        artifacts: [
                                            // Artifact generated such as .jar, .ear and .war files.
                                            [artifactId: project.name,
                                            classifier: '',
                                            file: artifactPath,
                                            type: 'zip']
                                        ]
                                    );
                                  }
                              }
                          }
                      }
                  }
                
           

 

El utilizar contenedores Docker nos permite realizar ciertas tareas pero nos limita en otras. Por tanto combinamos ciertos pasos de pipeline entre Docker y el host de Jenkins. Los únicos pasos que podemos invocar dentro de los contenedores Docker, son instrucciones 'sh'. Además si se utiliza la imagen en las fases, nos encontramos con el problema que crea dos workspace distintos, uno para lo que corre en el host y otro para los contenedores Docker. El plugin de Jenkins nos soluciona esto a través de la opción 'reuseNode true'. Con esto logramos un único workspace entre los distintos mundos. Las fases asociadas a SonarQube se deben realizar a nivel de host Jenkins. Al momento de realizar el upload a Nexus, utilizamos el plugin Pipieline Utility Steps el cual nos permite parsear un archivo JSON (package.json) a través de 'readJSON'. Este plugin nos soluciona obtener la metadata al momento de hacer push a Nexus. Se debe tener atención en el código dentro de los script, por ejemplo, en este caso utilizamos funciones de manipulación de archivos; estas, deben ser autorizadas explícitamente en la configuración de Jenkins.

 

 

Desde hace algún tiempo Microsoft ha comenzado a hacer guiños al mundo Open Source. De esta manera nos encontramos con Visual Studio Code, el cual se ha convertido en nuestro IDE de cabecera para proyectos Angular y React. Cuenta con un variado ecosistema de plugin y marketplace, y salvo algunos detalles de latencia y cantidad de archivos abiertos funciona de manera excelente en Linux. Para complementar nuestro pipeline y que los desarrolladores cuenten con las reglas en linea, instalamos los siguientes plugin en Visual Studio Code:

  • - Angular v7 Snippets: John Papa, una vez más nos entrega este plugin que autcompleta y sugiere código Angular. Con esto se logra aumentar la productividad y minimizar errores.
  •  

     

    kisspng-jenkins-docker-continuous-delivery-continuous-inte-5b1eb567f01075.492816241528739175983
  • - TSLint: TSLint es una herramienta para realizar exámenes de código estático para Typescript. Con este plugin obtenemos el análisis en tiempo real. Además al ejecutar el pipeline en Jenkins se genera un informe el cual es subido a SonarQube.
  •  

     

  • - SonarLint: De la misma manera que existe plugin SonarLint para Eclipse existe para Visual Studio Code. Nos permite ejecutar un examen de SonarQube, utilizando el mismo conjunto de reglas que las dispuestas en nuestro serivdor corporativo de SonarQube.
  •  

     

 

Finalmente podemos ver combinados los exámenes de las herramientas locales, con los exámenes de las herramientas en linea.

 

 

Conclusiones

Al inicio del artículo se plantea la integración continua como un proceso en constante desarrollo, las conclusiones que se presentan tienen que ver con la experiencia ganada en este proceso:

  • - Uso de Contenedores: Muchas veces el camino está resuelto a través de contenedores. Hay temas que no se pueden resolver en base a Groovy. Cuando se presente un problema o la inversión de tiempo y esfuerzo crezca, es necesario investigar si un contenedor ya resuelve la problemática. En algunas ocasiones, nos ha ocurrido que al momento de inciar algún pipeline la solución aún no está disponible sin embargo la encontramos después.
  •  

  • - Ofuscar Credenciales: No almacenar credenciales en los pipeline. Utilice las credenciales globales de Jenkins para esto. Exponga la menor cantidad de información relativa a la organización.
  •  

  • - Usabilidad: Inicialmente implementamos estas plataformas sin utilizar un SSO. Si bien cumplian su función, tener que recordar otro nombre de usuario y clave, se traducía en que no usaban las herramientas.
  •  

 

Pasos Siguientes

Los nuevos pasos que queremos implementar son los siguientes:

  • - Entrega continua: Implementar entrega continua, para los proyectos que se despliegan en contenedores y/o nube.
  • - Pruebas e2e y stress: Generar pruebas "end to end" para la capa Front y de stress para la Back.
  • - Jenkins-X: Implementar una solucion de Jenkins-x completamente basada en Docker y Kubernetes, al menos para la ejecución del Pipeline.
  •