Ejemplo Básico de CI/CD:
Cuando se aborda el tema de CI/CD (Continuous Integration/Continuous Delivery, Integración Continua/Entrega Continua) muchas veces los conceptos se presentan de manera ‘filosófica’, olvidando un poco cómo ‘aterrizarlos’ a la práctica. En este blog queremos presentar un acercamiento precisamente a cómo llevar a la práctica un flujo básico de CI/CD.
Es muy importante comprender el flujo básico CI/CD para un proyecto de Desarrollo de Software, ya que al comienzo podría parecer confuso qué paso lleva a cuál, qué debe hacerse en cada etapa, o qué elemento es responsable de cuál tarea.
Una vez se ha entendido este flujo, el segundo punto más complejo de entender podría ser la configuración en sí del Sistema de Integración Continua (qué herramienta utilizar, qué plugins instalar, cómo configurar los permisos, cómo generar los contenedores, etc.)
Aunque existen múltiples formas de abordar una implementación de CI/CD, vamos a tomar un ejemplo básico representado por el siguiente diagrama:
(Basic Flow for a Back-End CI/CD Project)
Este flujo comienza cuando un desarrollador envía (‘push’) su archivo/código fuente a un repositorio, el cual puede ser GitHub. Cuando GitHub detecta un push, envía una notificación a un Sistema de Integración Continua (por ejemplo, Jenkins). Una vez Jenkins recibe la información de GitHub, construye el proyecto (‘build’) mediante una configuración previamente establecida (jobs de Jenkins):
- Compilación
- Pruebas unitarias
- Pruebas de integración
Entre otras, sobre el proyecto. Esta construcción depende del tipo de proyecto: pueden haber proyectos de Front-End o de Back-End. Para este ejemplo vamos a trabajar con Java.
Si la construcción se llevó a cabo correctamente sin errores, Jenkins genera un contenedor y ese contenedor se puede subir a Amazon ECR. De lo contrario si ocurre algún error, el proceso no llega hasta el ECR.
Es elección del Desarrollador cada cuánto realiza push al repositorio (desde su IDE).
Para el caso de GitHub, es necesario configurar ‘webhooks’, el cual es un ‘punto de información’ para notificar a un servicio externo cada vez que se realice un push:
En la imagen anterior, la URL corresponde a un webhook asociado con Jenkins. Jenkins, a su vez, permite configurar trabajos asociados con ese webhook:
Un job (en Jenkins) es un proyecto que se puede configurar para especificar cómo se va a compilar, construir, y distribuir un sistema:
El ‘disparador’ asociado a este proyecto de Jenkins está vinculado con GitHub:
Permite especificar el repositorio y la rama (‘branch’) asociada con el proyecto:
En este caso, la construcción pasa por un archivo (Jenkinsfile):
En el archivo Jenkinsfile podríamos configurar las diferentes etapas por las que pasa la construcción de la aplicación:
En la imagen anterior el archivo Jenkinsfile especifica diferentes etapas de construcción. Cada etapa debe ser ejecutada satisfactoriamente antes de que inicie la siguiente.
La primera etapa es el ‘checkout’, en la cual Jenkins obtiene todas las fuentes modificadas del repositorio una vez que GitHub le informa de un nuevo ‘Push’.
En la segunda etapa (‘compile’) se construye el proyecto. Para este ejemplo se está construyendo un proyecto en Java por medio de Maven, para lo cual se ejecuta el comando:
mvn -U clean compile
Con este comando se generan, a partir de código fuente, los .class de java (que serán interpretados posteriormente por la Máquina Virtual de Java).
Para la tercera etapa que son las pruebas de unidad (‘Unit Test’) se ejecuta el comando:
mvn test
Una vez se han completado las pruebas, se ‘empaqueta’ el proyecto mediante el comando:
mvn package
El cual genera un .jar, que se podría considerar ya como el ‘entregable’ que contiene todos los .class necesarios para el proyecto Java:
Por último se realizan pruebas de integración:
mvn verify
Un ejemplo de prueba de integración puede ser realizar una solicitud a una API REST y verificar que la respuesta sea siempre la esperada (llamar a la API proporcionando un argumento y comparar que la respuesta). Para este punto se utiliza un contenedor Docker temporal, el cual dura sólo durante la ejecución de las pruebas de integración.
Cuando las pruebas de integración terminan satisfactoriamente, se realiza el despliegue definitivo (‘deploy’). El ‘deploy’ consiste en tomar la imagen recién generada y llevarla al repositorio de imágenes (en este caso Amazon ECR). En el ejemplo anterior, este despliegue se está haciendo a un ambiente de UAT, el paso previo a producción.
Cada construcción que se realiza queda registrada en Jenkins:
El ‘recorrido’ (pipeline) que realiza el proyecto en Jenkins puede verse también gráficamente de la siguiente forma:
En la imagen anterior se puede observar cómo el flujo (en Jenkins) es:
En caso de presentarse un error, Jenkins indica precisamente la etapa en la que se produjo. Por ejemplo en la construcción #39 el error se produjo en las Pruebas de Integración. En las otras construcciones no se produjo ningún error:
Es posible obtener más información sobre el error que se generó:
Al fallar una etapa las siguientes (si existen) no se ejecutan:
En cada etapa de la construcción se indica su duración total (en milisegundos o segundos). Jenkins también calcula la duración promedio para cada etapa en todo el proyecto.
Una vez Jenkins envía la nueva imagen a Amazon ECR, Jenkins reinicia el servicio para que ECS detecte la nueva imagen (comienzan a bajarse las instancias actuales de contenedores, y a subirse nuevas instancias).
Las imágenes que se van enviando a Amazon ECR quedan almacenadas en repositorios:
Cada repositorio puede contener una o más imágenes:
Cada vez que se solicita un reinicio desde Jenkins lo que se reinicia es un servicio asociado a un cluster de AWS ECS:
En la imagen anterior existen varios servicios dentro del cluster. Cada uno de esos servicios (para el ejemplo) tiene una instancia en ejecución ya que se ha configurado ese número de instancias como el deseado.
Una vez se ha reiniciado el servicio, se puede acceder a las diferentes opciones que tiene el software desarrollado. Es decir, si el proyecto es una API REST ya podrían empezar a hacerse solicitudes a esta API.
Es importante resaltar que el único paso ‘humano’ se hizo al comienzo, cuando el Desarrollador envió su código (‘push’) al repositorio de archivos, y este evento desató el resto del proceso automatizado. Sú única preocupación fue la de hacer bien su código, ejecutar sus pruebas de unidad (que no fallaran, estas pruebas son importantes para evitar realizar en vano todo el proceso automático) y que su código compile correctamente. De ahí en adelante el Desarrollador se puede ‘olvidar’ de qué es lo que pasa.
Jenkins permite configurar notificación por email para ciertos eventos. Por ejemplo si falló la construcción (‘build’) puede informar al Equipo de Desarrolladores, a alguna persona responsable de esta etapa. Esto permite una retroalimentación constante al Equipo Humano para que realicen las revisiones pertinentes.
Una vez se ejecutan todas las etapas, el mismo Desarrollador podría ver la última versión de su desarrollo en ejecución. Por ejemplo, si se trata de un nuevo ícono en el sitio web este podría verificarse al final de todo el proceso (existen herramientas especializadas para verificar desarrollos ‘Front-End’).
En el caso de un desarrollo para ‘Front-End’, Jenkins podría enviar (cuando todas las pruebas fueron exitosas) la actualización del(os) archivo(s) a un Repositorio de Archivos (por ejemplo, Amazon S3):
(Basic CI/CD Flow for a ‘Front-End’ Project)