Integración continua con Gitlab

gitlab-logo2.png

En Softtek hemos apostado fuerte por Gitlab en los últimos meses, hasta el punto de convertirse en uno de los pilares de nuestra iniciativa de innovationLabs & sideProjects, formando parte de la línea de Transformación Digital que ya presentamos en abril.

Gitlab es un proyecto de código abierto que tiene detrás una gran comunidad, con más de 1700 contribuidores alrededor del mundo. Es un producto que integra repositorio, control de versiones, gestión de incidencias, revisión de código, integración continua y mucho más en una única interfaz de usuario, lo que lo convierte en una solución muy interesante para gestionar nuestros pequeños side projects y proporcionar un espacio común de colaboración.

Una de las partes que más me interesaron de este proyecto, es la posibilidad de configurar un circuito de integración continua propio para cada proyecto. Esto nos permite hacer integraciones automáticas que engloben la compilación, la ejecución de test y el despliegue de nuestras aplicaciones, asegurando la temprana detección y solución de errores. Sin duda, algo imprescindible para cualquier proyecto innovador, con múltiples colaboradores y siempre en busca de la excelencia en cuanto a calidad.

En este post queremos aportar nuestro granito de arena a la comunidad hispana de Gitlab, explicando varios ejemplos de integración continua para distintas tipologías de proyectos:

  • Aplicación monolítica basada en Spring Boot, Maven y desplegada en AWS
  • Aplicación multimódulo basada en Spring Boot, Gradle y desplegada en AWS

Entendido estos dos pequeños ejemplos, seguro que no tendréis problemas en realizar los cambios necesarios para cualquier otra tipología de aplicación.

 

.gitlab-ci.yml

Lo primero que debemos saber es que, si colocamos un fichero .gitlab-ci.yml en la raíz de nuestro proyecto y lo subimos a cualquier repositorio de Gitlab, cada vez que haya un commit en nuestro repositorio se disparará un pipeline de CI y se procesará ese fichero. Esta es la base de nuestro circuito de integración continua: todo lo que queramos hacer debemos configurarlo en este fichero.

Por defecto, la ejecución del pipeline se separa en tres etapas (stages): build, test y deploy.

 

Aplicación Spring Boot & Maven

Partimos de una aplicación backend, basada en Spring Boot y construída con Maven. Para construir una aplicación similar de forma rápida y sencilla, podéis seguir nuestro webinar "Construyendo un API REST":

¡Todos nuestros webinars!

 

Este sería nuestro fichero .gitlab-ci.yml de inicio:

image: maven:3.3.9-jdk-8

before_script:
- mkdir -p $HOME/.m2/

stages:
- test
- build

myapplication_test:
stage: test
script:
- mvn clean test -B

myapplication_build:
stage: build
script:
- mvn package -B -Dmaven.test.skip
artifacts:
paths:
- target/myapplication-1.0.0.jar

 

Vamos a explicarlo parte por parte:

Lo primero que nos encontramos es:

image: maven:3.3.9-jdk-8


Esto nos permite especificar una imagen de Docker personalizada sobre la que ejecutar nuestros procesos de integración continua. En este punto es importante entender que no necesitamos tener conocimientos previos con Docker y que nuestra aplicación no tiene porqué estar dockerizada. Gitlab se encarga de tomar el control y cargar esta imagen.

En este ejemplo, nos basaremos en la imagen maven:3.3.9-jdk-8 que nos permitirá ejecutar comandos maven para probar y construir nuestra aplicación.

Lo siguiente que haremos es definir las etapas de nuestro circuito. Dejamos la fase de despliegue continuo (deploy) fuera de este primer ejemplo y ésto nos deja con sólo dos etapas (test y build):
stages:
- test
- build

Por último, tan sólo nos queda definir tantos jobs necesitemos para cada una de nuestras etapas:

myapplication_test:
stage: test
script:
- mvn clean test -B

Definimos un job al que nombramos myapplication_test, indicamos que se debe ejecutar en la fase test e indicamos también el comando maven que se debe ejecutar (mvn test). Podemos ejecutar cualquier comando maven gracias a la imagen docker que utilizamos como base y que especificamos anteriormente.

(*) Añadimos la opción "-B" para ejecutarlo en modo batch, especialmente recomendable para entornos de integración continua. Así, este comando se ejecuta en modo no interactivo y no requiere ninguna entrada adicional por parte del usuario.

La etapa de build es muy similar:

myapplication_build:
stage: build
script:
- mvn package -B -Dmaven.test.skip
artifacts:
paths:
- target/myapplication-1.0.0.jar

En este caso añadimos las sentencias artifacts / paths, utilizadas para especificar una lista de recursos o ficheros que se deben adjuntar al trabajo, siempre que el job termine con éxito. Debemos tener en cuenta que sólo podemos utilizar las rutas que se encuentran dentro del área de trabajo del proyecto. En este caso, al finalizar la etapa de build, añadiremos el fichero entregable *.jar a la lista de recursos para que posteriormente pueda ser usado en la fase de deploy, que añadiremos más adelante.

 

Aplicación multimódulo Spring Boot & Gradle

Adaptar el ejemplo anterior a otro tipo de aplicación no es complicado. Con un poco de sentido común y un par de búsquedas en el site oficial de Gitlab puede encontrar la solución ajustada a su caso sin demasiados problemas.

Supongamos ahora una aplicación construida con Gradle (en lugar de Maven) y que, además, esté compuesta de dos modulos Spring Boot (productos y usuarios). Debemos colocar el fichero .gitlab-ci.yml en la raíz del proyecto padre, y desde ahí ejecutar los comandos sobre cada uno de los módulos.

image: gradle:alpine

stages:  
  - test
  - build
  
stage_test:
   stage: test
   script: gradle test

stage_build:
   stage: build
   script: gradle build
   allow_failure: false
   artifacts:
    paths:
      - products/build/libs/products-1.0.0.jar
      - users/build/libs/users-1.0.0.jar

 Como podemos comprobar, apenas hay cambios sobre el primer ejemplo:

  • Partimos de la imagen de Docker gradle:alpine
  • Substituimos los comandos de construcción de maven por sus equivalencias en gradle.
  • Añadimos los jar de cada uno de los módulos a la lista de recursos compartidos.

 

Despliegue continuo

En los dos ejemplos anteriores hemos visto las fases de test y build de dos aplicaciones distintas. La última fase sería la de despliegue (deploy), que dependerá directamente de la plataforma que queramos usar.

Para este post vamos a desplegar nuestra aplicación en AWS Elastic beanstalkun servicio de Amazon Web Services fácil de utilizar para implementar y escalar servicios así como aplicaciones web desarrolladas en múltiples lenguajes. Basta con crear tu instancia, subir el fichero *jar y pulsar un botón para empezar a trabajar con tu aplicación. 

Todo esto se puede automatizar en nuestro fichero yml. Partiendo del primer ejemplo, ésta sería la nueva configuración:

image: maven:3.3.9-jdk-8

variables:
  AWS_ACCESS_KEY_ID: "MyAWSAccessKeyID"
  AWS_SECRET_ACCESS_KEY: "MyAWSSecretKey"
  AWS_DEFAULT_REGION: "eu-west-1"
  EB_APP_NAME: "myapplication" 
  EB_APP_ENV: "myEnvironment"  
  EB_APP_JAR_NAME: "myapplication-0.1.0-SNAPSHOT.jar"
  EB_VERSION: "myapplication"
  S3_BUCKET: "elasticbeanstalk-eu-west-1-ID"  
  
cache:
  key: "$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME"
  paths:
    - .m2/

before_script:
  - mkdir -p $HOME/.m2/
  
stages:
  - test
  - build
  - deploy

myapplication_test:
  stage: test
  script:
    - mvn clean test -B

myapplication_build:
  stage: build
  script:
    - mvn package -B -Dmaven.test.skip
  artifacts:
    paths:
      - target/$EB_APP_JAR_NAME
      

myapplication_deploy:
  only:
    - master
  stage: deploy
  image: python:latest
  script:
  - pip install awscli
  - aws s3 cp target/ s3://"$S3_BUCKET"/  --recursive --exclude "*" --include "*.jar"
  - aws elasticbeanstalk create-application-version --application-name "$EB_APP_NAME" --version-label "$EB_VERSION $CI_JOB_ID" --description "$EB_APP_NAME deployment" --source-bundle S3Bucket="$S3_BUCKET",S3Key="$EB_APP_JAR_NAME"
  - aws elasticbeanstalk update-environment --application-name "$EB_APP_NAME" --environment-name "$EB_APP_ENV" --version-label "$EB_VERSION $CI_JOB_ID"

En primer lugar, declaramos una serie de variables relacionadas con la configuración de nuestra instancia de AWS: el id y clave de acceso a nuestra cuenta, la región, los nombres de nuestro entorno, de nuestra aplicación, de nuestro bucket y de nuestro entregable *.jar:

variables:
  AWS_ACCESS_KEY_ID: "MyAWSAccessKeyID"
  AWS_SECRET_ACCESS_KEY: "MyAWSSecretKey"
  AWS_DEFAULT_REGION: "eu-west-1"
  EB_APP_NAME: "myapplication" 
  EB_APP_ENV: "myEnvironment"  
  EB_APP_JAR_NAME: "myapplication-0.1.0-SNAPSHOT.jar"
  EB_VERSION: "myapplication"
  S3_BUCKET: "elasticbeanstalk-eu-west-1-ID"  

A continuación definimos el job encargado del despliegue de la aplicación en AWS:

myapplication_deploy:
  only:
    - master
  stage: deploy
  image: python:latest
  script:
  - pip install awscli
  - aws s3 cp target/ s3://"$S3_BUCKET"/  --recursive --exclude "*" --include "*.jar"
  - aws elasticbeanstalk create-application-version --application-name "$EB_APP_NAME" --version-label "$EB_VERSION $CI_JOB_ID" --description "$EB_APP_NAME deployment" --source-bundle S3Bucket="$S3_BUCKET",S3Key="$EB_APP_JAR_NAME"
  - aws elasticbeanstalk update-environment --application-name "$EB_APP_NAME" --environment-name "$EB_APP_ENV" --version-label "$EB_VERSION $CI_JOB_ID"

 

Dentro del job, restringimos la ejecución de este stage solo a cambios realizados en la rama master ("only: master"), ya que no queremos desplegar cada commit de cada una de las múltiples ramas que pueden coexistir en el proyecto.

En este caso, el script es un poco más largo y se divide en 4 líneas. Lo primero que hay que resaltar en este Gitlab nos permite definir diferentes imágenes Docker para cada uno de nuestros jobs. En este caso, vamos a usar la última versión disponible de phyton, que nos permitirá instalar la línea de comandos de AWS dentro de nuestro circuito de integración ("pip install awscli"). 

La interfaz de línea de comando de AWS, también conocida como awscli, nos permitirá interactuar directamente con nuestra cuenta de Amazon para acceder, intercambiar ficheros, instalar, reiniciar, etc.

A continuación, subiremos los entregables *.jar generados durante la fase de build a nuestro bucket de AWS. Para ello, utilizamos el comando "aws s3 cp" y pasaremos como parámetros la ruta de origen, la ruta de destino y las opciones necesarias. Hay que recalcar que, en este caso, la ruta de origen "target/" es accesible, ya que en la fase de build fue definida como artefacto de nuestro circuito.

Con el entregable subido a nuestra cuenta de AWS, ya sólo nos queda crear y desplegar nuestra nueva versión en AWS Elastic beanstalk, para lo que usaremos los comandos "aws elasticbeanstalk".

Para crear nuestra nueva versión, utilizamos "aws elasticbeanstalk create-application-version" y le pasamos como parámetros el nombre de la aplicación, la version y la descripción. Para el nombre de la versión, que debe ser único, hemos utilizado una variable reservada de Gitlab, $CI_JOB_ID, que es un identificador único del job de integración continua que se está lanzando.

Para terminar nuestro job, tan solo nos queda finalizar el despliegue. Para ello ejecutamos "aws elasticbeanstalk update-environment", pasándole como parámetros los nombres del entorno, la aplicación y la versión.

 

Gitlab CI en acción...

Configurado nuestro fichero yml, ya solo queda subirlo a nuestro repositorio y verlo en acción. Accediendo a la sección Pipelines de nuestro proyecto, podemos ver el histórico de ejecuciones.

Si hacemos commit&push en cualquier rama distinta de master de nuestro repositorio, se lanzarán únicamente los stages de build y test:

twostages.png

Podemos entrar en el detalle de ejecución:

2stages_detalle.png

Incluso podemos ver las trazas de ejecución de cada uno de los jobs:

2stages_detalle_job.png

Todo ha ido bien y hemos obtenido dos verdes en nuestro circuito de integración, por lo que estamos listos para hacer el merge request. Una vez que los cambios de nuestra rama se vuelquen sobre el master, se volverá a ejecutar nuestro pipeline, esta vez con tres etapas:

3stages_resumen.png

3stages_detalle.png

Si todo finaliza ok, podemos ir a nuestro AWS para comprobar que la nueva versión ha sido desplegada y que su estado actual es OK:

deployed.pngSi en alguna ocasión alguno de los jobs fallan, podremos visualizarlo igualmente en nuestra ventana de pipelines y además la persona que realizó el commit recibirá un email informando del error:

error.png

 

Esto es todo por ahora. Espero que os sirva a modo de introducción a aquellos que iniciáis un proyecto en Gitlab. En las próximas semanas, ampliaremos este post para integrar tambien un job de análisis estático de código con Sonar en nuestro pipeline.

Cualquier duda, utilicen los comentarios! ;)