Introducción a Jenkins: Construcción de Pipelines CI/CD

Introducción

DevOps es un conjunto de prácticas que pretenden agilizar el ciclo de vida del desarrollo de software, coordinando los departamentos de desarrollo (Dev) y de operaciones (Ops). Los equipos que tienen integrada una cultura de DevOps son capaces de integrar y entregar continuamente software (CI/CD) a través de la automatización de su compilación, testeo y despliegue. 

Jenkins es un servidor de automatización de código abierto que permite definir pipelines de CI/CD. Siendo éste de código abierto, Jenkins dispone de una gran variedad de plugins que permiten extender sus utilidades e interaccionar más fácilmente con otras herramientas.

En este post, se describe el proceso de instalación y configuración de Jenkins para que éste pueda interactuar con un repositorio en GitHub. Adicionalmente, se crea y se configura un job en Jenkins que permite al usuario de GitHub ver de manera automática si los tests unitarios de una rama del repositorio se han ejecutado correctamente.

Instalación de Jenkins con Docker

Docker es una herramienta que nos permite desplegar software dentro de contenedores. En el caso de Jenkins, existe una imagen oficial (jenkins/jenkins:lts-jdk11) que permite desplegar fácilmente un servidor de Jenkins. Como más adelante se usará Python para crear los tests, se crea una Dockerfile para crear una nueva imagen que parta de la oficial pero que tenga Python instalado.

FROM jenkins/jenkins:lts-jdk11
USER root
RUN apt-get update -y
RUN apt-get install -y python3.9

Para crear una imagen llamada jenkins_build a partir del Dockerfile, se usa el comando:

docker build -t "jenkins_build" .

Finalmente, para ejecutar el contenedor, se usa el siguiente comando partiendo de la imagen creada antes:

docker run --rm \
  -p 8080:8080 \
  -p 50000:50000 \
  -v /path/to/jenkins_home:/var/jenkins_home \
  --name jenkins_demo \
  jenkins_build

A continuación, se explica paso por paso el comando anterior:

  • La opción --rm hace que el contenedor se borre al finalizar su ejecución. Eso permite que este comando se pueda utilizar cada vez que queramos ejecutarlo. Alternativamente, se puede omitir esta opción y relanzar el contenedor con docker start [nombre_contenedor].
  • La opción -p 8080:8080 mapea el puerto local 8080 al puerto del contenedor 8080, que corresponde al puerto de la interfaz web.
  • Asimismo, la opción -p 50000:50000 corresponde al puerto del agente.
  • La opción -v /path/to/jenkins_home:/var/jenkins_home crea o utiliza un volumen, asociando un directorio de tu sistema al directorio /var/jenkins_home dentro del contenedor. Esta opción permite persistir los cambios en un directorio local.
  • La opción --name jenkins_demo le pone un nombre al contenedor.

Una vez acabada la instalación, se mostrará en la consola un mensaje que contendrá la contraseña del usuario admin y otro mensaje anunciando que el servidor de Jenkins ya está listo para su uso:

Tras la puesta en marcha, se puede acceder a la UI de Jenkins a través de http://localhost:8080, donde Jenkins pedirá que se introduzca la contraseña del usuario admin:

A partir de aquí, Jenkins guiará al usuario para terminar la instalación y la configuración del servidor. Entre las opciones de bienvenida, se encuentran la instalación de plugins recomendados, algunos de los cuales se usarán en la demostración a continuación.

Integración de Jenkins y GitHub

En este apartado se crea el job para ilustrar un proceso de CI/CD. Se quiere que el servidor de Jenkins sea capaz de detectar cambios en un repositorio de GitHub y que, cuando esto pase, ejecute los tests unitarios definidos en este repositorio. Finalmente, se desea que el resultado de estos tests pueda ser consultado en el repositorio de GitHub para que todos los miembros del equipo puedan saber el estado de un commit, una rama o un pull request.

Para empezar, se configura el repositorio de GitHub para interactuar con Jenkins. En primer lugar se prepara un webhook. Esto permite a GitHub notificar a Jenkins de que ha sucedido un evento en el repositorio, como por ejemplo, que se haya realizado un push a alguna de las ramas. Para ello, se debe ir a la pestaña de configuración del repositorio y, en el apartado Webhooks, añadir uno. Una vez aquí, se debe introducir la dirección IP del servidor de Jenkins y añadirle /github-webhook/ al final. 

Si se ha seguido paso a paso el proceso de instalación descrito en este post, el servidor de Jenkins sólo estará disponible en la red local. Para mantener el enfoque en la interacción entre Jenkins y GitHub y como se está trabajando sobre un entorno de pruebas, se ha decidido usar la herramienta ngrok para exponer un puerto local (en este caso 8080) a internet.

Seguidamente, se genera un token de acceso personal para que Jenkins pueda informar a GitHub de los resultados de los tests unitarios en un commit. Para hacerlo, se debe acceder a la configuración de la cuenta de GitHub y en la pestaña de configuración de desarrollador. En el apartado de tokens de acceso personal, se puede generar un nuevo token, al que se le dará control sobre los repositorios privados de la cuenta (casilla repo).

Una vez generado el token, se deberá introducir en la configuración de Jenkins. Para ello, se accederá a Manage Settings en el dashboard principal y luego a la configuración del sistema. En el apartado de GitHub, se podrá añadir un nuevo servidor, donde se marcará la casilla Manage hooks:

Se deberán añadir unas nuevas credenciales del tipo Secret text y añadir el token de Github a Secret.

Creación de la pipeline

A continuación, se crea un job desde el dashboard principal y se selecciona la opción Freestyle project. En el primer apartado de la configuración del job, se selecciona la opción de GitHub project, donde se añade la dirección url ssh del repositorio. 

La misma url se añade en el apartado de Source code management, donde adicionalmente se tendrá que añadir unas nuevas credenciales, pero esta vez por ssh. Esto requiere que se haya generado un par de claves pública y privada y se haya subido la pública a GitHub. En caso de no tenerlo configurado, se puede consultar aquí la documentación de GitHub. Para configurar Jenkins, se selecciona la opción de SSH Username with private key y se rellenan los campos de usuario, clave privada y passphrase con sus valores respectivos.

Después, se selecciona la opción GitHub hook trigger for GITScm polling para que Jenkins ejecute el job cuando reciba el aviso del webhook. Seguidamente, se tiene que definir qué hará el job. En primer lugar, el job ejecutará los tests. Para ello, en el apartado Build, se añade un build step que permita ejecutar un comando de shell. En este caso, el repositorio contiene 1 test que usa la librería unittest, entonces se usa el siguiente comando para encontrar todos los tests y ejecutarlos:

python3.9 -m unittest discover

Finalmente, el resultado del test tiene que actualizar el estado del commit en el repositorio. Para hacerlo, se crea una acción de Post-build del tipo Set GitHub commit status (universal) y se selecciona la opción de usar mensajes y estados por defecto en Status result. 

Ejecución del job

Una vez configurado el job y la interacción entre las dos plataformas, solo queda comprobar el funcionamiento con diferentes resultados de los tests. Para resumir, el funcionamiento debería ser el siguiente: primero, se realiza un commit en local que se sube al repositorio y éste se notifica a Jenkins. Luego, Jenkins descarga el repositorio y realiza los tests. Finalmente, el resultado de los tests se notifica a GitHub, donde se marcan los cambios como exitosos o fallidos. 

En primer lugar, se crea un test que siempre falla, como por ejemplo:

import unittest


class Test(unittest.TestCase):
    def test_run(self):
        expected = 42
        actual = 314
        self.assertEqual(expected, actual)

Accediendo al job en Jenkins, se puede ver como la build número 1 ha fallado en el panel de la izquierda:

Si se accede a los commits GitHub, se puede ver como el commit se ha marcado con una x:

Por último, se realiza el mismo proceso con un test que nunca falla:

import unittest


class Test(unittest.TestCase):
    def test_run(self):
        expected = 42
        actual = 42
        self.assertEqual(expected, actual)

Se observa que en Jenkins el job se ha ejecutado correctamente y que en Github el commit se ha marcado como exitoso.

Conclusiones

En este post se ha visto cómo utilizar Jenkins para crear pipelines de CI/CD y agilizar el ciclo de vida del desarrollo de software. En un ejemplo sencillo, se ha visto cómo definir la interacción entre GitHub y Jenkins para que se ejecute un job cuando se hace push al repositorio y actualizar el commit en cuestión. 

Jenkins permite expandir las posibilidades de las pipelines gracias a los plugins que ofrece. Por ejemplo, en casos más complejos, se puede utilizar los plugins de Pipeline, para definir pipelines de manera programática a través de ficheros Jenkinsfile en lenguaje DSL.

Si este artículo te ha parecido interesante, te animamos a visitar la categoría Software para ver más posts como este y a compartirlo en redes con todos tus contactos. No olvides mencionarnos (@DamavisStudio) para conocer tu opinión.
Guillermo Camps
Guillermo Camps
Artículos: 16