Pipelines Genéricos en Jenkins: CI/CD for the masses !

Guillermo Roldán
LaLiga Tech
Published in
12 min readApr 13, 2021

--

Unas de las tecnologías que más están creciendo en los últimos años y en las que cada vez más compañías están invirtiendo y encontrando valor, son las relacionadas con el mundo de la Automatización y de la Integración y Despliegue Continuo de Aplicaciones, debido a que permiten (entre otras cosas) acelerar la entrega de Producto y mejorar la Calidad del mismo, fundamental en los actuales entornos de alta competitividad, más aún cuando cuando se trata de un producto o servicio Premium. En esta Story queremos contaros cómo lo estamos aplicando en LaLiga, compartir las ventajas e inconvenientes de un modelo, que nos ha ofrecido muy buenos resultados, sencillo, con un bajo coste de mantenimiento, y un alto grado de reusabilidad: Pipelines Genéricos.

Introducción a los Pipelines Genéricos

Una de las aproximaciones a la Integración y Despliegue Continuo de Aplicaciones, consiste en desarrollar un proceso de Integración y Despliegue específico para cada aplicación, un modelo que nos permitirá un alto grado de personalización, pero que por otro lado, implica una situación clara de acoplamiento, duplicidad de código y de sobresfuerzo. Cuanto más grande sea la compañía, mayor será el esfuerzo de mantenimiento y también se dificultará la estandarización y aplicación de buenas prácticas (si detectamos un bug, potencialmente tendríamos que aplicarlo en todos los procesos de despliegue de cada una de las aplicaciones, y probarlo, claro). También será más complicado que un perfil DevOps de un proyecto, pueda dar soporte a otro proyecto, al poder tratarse de procesos de despliegue muy diferentes, que requieran cierta curva de aprendizaje.

La aproximación contraria, consiste en tener procesos de Integración y Despliegue Continuo genéricos por Tecnología. Por poner de ejemplo alguno de los casos de uso que tenemos en LaLiga:

  • Un único proceso de despliegue para Aplicaciones sobre Kubernetes.
  • Un único proceso de despliegue y publicación de Librerías Python.
  • Un único proceso de despliegue para trabajos de Spark sobre Azure Databricks.
  • Un único proceso de despliegue para ficheros estáticos y aplicaciones SPA con CDN.
  • Un único proceso de despliegue de base de datos SQL Server (DacPac).

De este modo, cuando se comienza un nuevo proyecto, es posible desplegar desde el primer día, al reutilizar estos procesos, lo que nos da un alto rendimiento, y la seguridad de estar usando un proceso maduro, usado intensamente en la casa (los bugs se detectan rápidamente, y se aplican los fix y las mejoras una única vez, para todos los proyectos).

GitFlow, Jenkins, Web Hooks, y el uso de ramas de despliegue

Una de las formas de implementar este modelo es utilizando Jenkins y los Web Hooks. De este modo, es posible configurar en un repo de Git un Web Hook que salte al aceptar una Merge Request en dicho repo, invocando el Pipeline de Jenkins que ejecutará el proceso genérico de despliegue. Así, es posible tener múltiples proyectos en Git, todos ellos compartiendo un mismo proceso de despliegue.

Siguiendo con este modelo, podemos utilizar las ramas permanentes de GitFlow (ej: master, develop) como ramas de despliegue, de tal modo que al aceptar una Merge Request sobre develop desencadenaremos el despliegue sobre el entorno de Desarrollo, pero si la Merge Request fuera sobre master, se desencadenaría el despliegue sobre el entorno de Producción. Incluso se pueden definir ramas permanentes adicionales (ej: test, qa), para desplegar en entornos intermedios de test/integración y QA ó Pre-Producción.

También protegemos las ramas en Git para controlar quién puede desplegar en qué entorno en cada Proyecto. Por ejemplo, podemos hacer que en develop, test y qa puedan desplegar los equipos de desarrollo con total autonomía, pero que en master se requiera de aprobación para desencadenar el despliegue en Producción.

Es posible añadir switches en el comentario de la Merge Request para forzar ciertos comportamientos. Por ejemplo, podemos incluir /skipci en el comentario de la Merge Request para saltar las pruebas automáticas, lo cual resulta de utilidad cuando el equipo de desarrollo está intentando resolver un problema y necesita realizar numerosos intentos de despliegue, en lugar de tener al equipo esperando que acabe la ejecución de pruebas, cuando no tiene sentido.

No obstante, en aquellos casos en los que no nos encaje utilizar los Web Hooks, también podemos crear un Pipeline de Jenkins basado en un Formulario, que realice el despliegue en función de los parámetros de entrada. Así, podríamos pedir como parámetros el Proyecto y la rama de Git, o cualquier otro valor que necesitemos.

Las posibilidades son muchas. El límite está en la imaginación.

¿Café para todos? No necesariamente…

Una de las preocupaciones que genera este modelo, es que pueda servir para un Proyecto, pero que pueda ser totalmente incompatible con otro. Para esto usamos un fichero de configuración (build.yml) en la raíz del repo Git, en el cual es posible configurar cómo deseamos que se comporte nuestro Pipeline de Jenkins. Esto nos permite dar mucha flexibilidad, pero manteniendo un único Pipeline de Jenkins. Al ejecutar el trabajo de Jenkins, se descargará el repo de Git e interpretará este build.yml, ajustando su comportamiento en base al valor de las etiquetas en su interior.

Por ejemplo, para la ejecución de pruebas funcionales de tipo Cucumber, se informa en el build.yml el repo que contiene las pruebas automáticas, que son gestionadas en un repo aparte, o también se especifica los manifiestos de despliegue en el caso de Kubernetes (cuáles de quiere hacer apply y en qué orden), o si deseamos evitar la caché de docker al hacer el docker build, por poner algunos ejemplos. Todas estas, son configuraciones que el equipo de Desarrollo puede modificar con autonomía para personalizar en cada Proyecto.

Azure y el despliegue seguro con Managed Instance

En nuestro caso utilizamos Azure como principal proveedor Cloud, por lo que ejecutamos Jenkins en una Máquina Virtual en IaaS, y asignamos una Managed Instance a dicha máquina, para el acceso seguro a los diferentes recursos que tenemos en Azure.

Realmente necesitamos varias Granjas de Jenkins con múltiples Nodos, ya que además de los Nodos Linux tradicionales, usamos Nodos Windows para tareas específicas (ej: DacPac, Net Framework), y también Nodos MAC (ej: construcción y despliegue de aplicaciones móviles para IOS). En este último caso, se trata de varios MAC Mini corriendo On-Prem y conectados a través de Express Route.

En cualquier caso, el concepto de Managed Instance, permite asignar una identidad de Azure AD a una Máquina Virtual de Azure, de tal modo, que el código que se ejecute dentro de esa máquina tenga acceso a los recursos a los que demos permiso a dicha identidad. Esto mola, ya que ese mismo código en otra máquina, no funcionaría, con la ventaja de que no tenemos que estar gestionando credenciales (usuarios/contraseñas) que en caso de verse comprometidas, podrían producir una situación incómoda. En AWS sería similar, aunque hablaríamos de Roles IAM, básicamente el mismo concepto.

En este modelo, con Jenkins tenemos una herramienta de despliegue y automatización de carácter híbrido y cloud-independent, que nos permite poder trabajar en Azure y en On-Prem, incluso si tuviéramos la necesidad podríamos tener Nodos esclavos de Jenkins en AWS o Google Cloud.

Gestión de Secretos y Configuraciones: Azure Key Vault al rescate !

Otra necesidad común es poder almacenar y consumir las configuraciones (ej: nombre de una base de datos) y secretos (ej: contraseña) necesarias para la aplicación, por dos motivos fundamentales:

  • Primero y más importante: jamás deben estar hardcodeadas en el código, por seguridad.
  • Segundo, y no menos importante: es necesario sustituir sus valores por los apropiados de cada entorno, ya sea en tiempo de despliegue o en tiempo de ejecución, desacoplando la aplicación de su configuración.

Azure Key Vault resulta en este escenario de gran ayuda, ya que podemos tener un Key Vault para cada Aplicación y cada Entorno (ej: Desarrollo, Test y Producción), de tal modo que en tiempo de despliegue el Pipeline de Jenkins sea capaz de recuperar dichos Secretos desde el Key Vault correspondiente y reemplazarlos, para que la aplicación pueda funcionar correctamente. En este caso, lo que usamos es un sistema similar a las Platillas Jinja, con un placeholder distinto al de estas (para que además sean compatibles), y un script que busca y reemplaza contra Key Vault.

Pero no siempre usamos esta aproximación. En otros escenarios nuestros procesos acceden directamente en tiempo de ejecución a los Key Vault correspondientes, como sería el caso de los trabajos Spark de Azure Databricks, que lo trae incorporado de serie (Secret Scopes). Esta aproximación tiene algunas ventajas, como por ejemplo, la facilidad de poder cambiar los valores de los Secretos, sin tener que desplegar de nuevo, dado que se consultan en tiempo de ejecución, pero por el contrario implica algunas dificultades adicionales en función del caso de uso.

Trazabilidad, Logs, y ocultación de Secretos

Otra ventaja del uso de Pipelines Genéricos en Jenkins, es la gestión de los Logs de ejecución de los trabajos de Jenkins, lo cual, facilita la trazabilidad en un sentido doble:

  • Por un lado es posible ver el Log detallado de la ejecución de cada trabajo de Jenkins con el output de cada comando (al final un trabajo de Jenkins no deja de ser un simple Script).
  • Por otro lado es posible ver el histórico de todas las ejecuciones del Pipeline de Jenkins, lo que nos permitirá saber qué proyecto se ha desplegado en qué entorno y en qué momento del tiempo, incluso quién invocó el despliegue, y si necesitamos más información, podremos también acceder al Log detallado de dicha ejecución del Pipeline.

Esto está muy bien, pero tiene la dificultad de que muchas veces utilizamos comandos que usan contraseñas, API Keys, etc., como parte de sus parámetros de entrada o bien lo muestran en su salida estándar, pudiendo revelar información sensible en los Logs de ejecución de los Pipelines de Jenkins. En estos casos, realizamos la ocultación de los propios comandos o de la salida estándar, en los comandos ejecutados por Jenkins que pueden revelar secretos o información sensible, para que el acceso a los Logs sea una actividad segura en la compañía. Se registra en los Logs cada tarea que se realiza, pero ocultando los detalles comprometidos, para conseguir un equilibrio entre la trazabilidad y la seguridad.

Azure DevOps y Jenkins trabajando en equipo

Si bien es cierto que hacemos un uso intenso de los Pipelines Genéricos de Jenkins, también tenemos proyectos que necesitan procesos de despliegue específicos, incluso utilizando otras tecnologías de despliegue como Azure DevOps. Esto no es problema, incluso encontramos sinergias, al poder compartir tecnologías entre las automatizaciones realizadas en Jenkins y las realizadas en Azure DevOps, como sería el caso del uso de Azure Key Vault, o de SonarQube.

¿Pipelines Declarativos o Programáticos?

En Jenkins podemos tener Pipelines escritos en modo Declarativo o en modo Programático. Básicamente la diferencia es que en modo Programático estaríamos escribiendo prácticamente un Script Groovy, mientras que en modo Declarativo se utiliza una sintaxis más sencilla, donde se definen la fases del Pipeline, que se ejecuta en cada una de ellas, etc.

Resulta más práctico escribir Pipelines Declarativos por la facilidad de mantenimiento y legibilidad del código, más aún teniendo en cuenta que dentro de ellos podremos introducir un bloque de tipo Script si deseamos realizar una tarea avanzada que no conseguimos realizar ejecutando una llamada directa a una función. A continuación se muestra un ejemplo de un Pipeline Declarativo, a modo ilustrativo.

También es posible crearnos nuestras propias Librerías de código en Jenkins (Shared Libraries) para poder reutilizar código entre diferentes Pipelines, que junto al concepto de Pipelines Genéricos ofrece un alto nivel de reusabilidad y facilita el mantenimiento de la solución.

Agentes Dockerizados: simplificando el mantenimiento de los Nodos Jenkins

Otro detalle interesante es la utilización de Agentes Dockerizados en Jenkins. Cuando ejecutamos un trabajo de Jenkins, podemos indicarle sobre qué Nodo (o grupo de de Nodos) lo queremos ejecutar.

El problema es que cada vez hay más trabajos en Jenkins, que para su funcionamiento requieren de la instalación de determinadas versiones de herramientas y productos. Garantizar su disponibilidad en todos los Nodos, según crece y se amplía dicho repertorio, es una tarea propensa a fallos y que requiere de mucha dedicación, más el problema de incompatibilidades de versiones y librerías.

La alternativa es Docker. Tenemos varias imágenes Docker en un Registry privado que almacenaremos versionadas, en nuestro caso sobre un Azure Container Registry (ACR), de tal modo que para cada fase de cada trabajo de Jenkins podemos especificar qué Nodo y qué Imagen Docker necesitamos utilizar para su correcta ejecución (o bien, definirlo a nivel de Pipeline y reutilizarlo en todas la fases), simplificando enormemente toda esta configuración y mantenimiento. Así, cada Nodo esclavo de nuestras Granjas Jenkins, tan sólo necesita Docker, Azure CLI, y poco más, para poder funcionar con normalidad.

Mejora de la Calidad: Análisis Estático de Código y Automatización de Pruebas

Hemos introducido procesos de Mejora de la Calidad (QA) en los Pipelines Genéricos de Jenkins, lo que nos ha permitido implementándolo una vez, poder aplicarlo a todos los Proyectos que comparten dichos Pipelines. Esto es fundamental, no es suficiente agilizar el despliegue en Producción, si no somos capaces de hacerlo con una garantía de calidad.

Todos los proyectos quedan integrados con SonarQube de serie, sin tener que hacer ningún paso adicional, pero con la posibilidad de personalizar detalles como el Quality Gate de cada Proyecto, o si queremos que el incumplimiento del Quality Gate impida o no el Despliegue. Con esto conseguimos tener análisis estático de código fuente, y también la automatización de pruebas unitarias y cobertura de código.

Del mismo modo es posible ejecutar pruebas funcionales o de aceptación, utilizando Frameworks como Karate ó Nightwatch y Cucumber, para la automatización de pruebas de API y de UI.

Recapitulación y despedida

Hace algo más de dos años que empezamos a construir todas estas automatizaciones con Jenkins y a implementar los diferentes Pipelines Genéricos para el despliegue de diferentes tecnologías. El resultado ha sido muy positivo, tanto por las ventajas propias del despliegue continuo como por las específicas del modelo de Pipelines Genéricos, con un coste de mantenimiento muy bajo y una alta reutilización. Como resumen:

  • Uso de Git como fuente de la verdad. El código que está en master, es el que está en Producción. No hay posibilidad de tener código en Producción que no está en el Git corporativo.
  • Control de la Infraestructura y del proceso de Despliegue. Tenemos control de la infraestructura en la que corren nuestras aplicaciones, así como del proceso de despliegue de las mismas. No hay posibilidad de que exista un recurso crítico como único conocedor de la infra o del despliegue de una aplicación.
  • Estandarización de la estrategia de ramas y del despliegue. Si estas trabajando en un Proyecto, resulta sencillo poder participar en otro Proyecto diferente, al compartir las mismas reglas todos los equipos. Existe una pequeña curva de aprendizaje de entrada, que se compensa con el aumento del rendimiento de los equipos.
  • Autonomía y rapidez de los equipos de Desarrollo. Los desarrolladores pueden desplegar desde el primer momento en todos los entornos intermedios, tantas veces como necesiten, de forma autónoma. Para el despliegue en Producción, se requiere aprobación, y aceptación por el equipo de Operaciones que recepciona el servicio.
  • Separación de la Configuración y Secretos en Azure Key Vault, evitando que se almacenen contraseñas y datos sensibles en el historial de Git, que en un futuro puedan resultar comprometidas.
  • Minimizar el mantenimiento de todos los procesos de despliegue, al estar centralizados en Pipelines Genéricos, junto al uso de Librerías (Shared Libraries) y Agentes Dockerizados. La aplicación de fixes o mejoras, se simplifica en gran medida.
  • Integración e independencia del Cloud. Tenemos una solución integrada en Azure, pero que se puede adaptar rápidamente a cualquier otro proveedor de Cloud como AWS o Google Cloud, especialmente en algunos Pipelines Genéricos como el de Kubernetes.
  • Despliegue seguro de Aplicaciones. Los equipos de desarrollo no necesitan conocer Credenciales ni Secretos, para poder desplegar en Producción, debido al uso a Azure Key Vault. El acceso a los recursos de Azure, se apoya en el uso de Managed Instances, para evitar problemas de fuga de credencias. Los Logs de Jenkins ocultan la información sensible, aportando a los desarrolladores todo el nivel de trazabilidad que necesitan, pero sin comprometer la seguridad.

Hay muchos más detalles, que no hemos podido incluir para no extendernos demasiado, más otras mejoras que tenemos en curso o previstas en backlog. En cualquier caso, esperamos que este artículo pueda haber sido de vuestro interés, que os ayude a valorar algunas de nuestras lecciones aprendidas, y a tener una perspectiva de cómo trabaja una empresa como LaLiga con este tipo de tecnologías.

--

--

Guillermo Roldán
LaLiga Tech

Head of Architecture @LaLiga • Making things happen • Cloud enthusiast • Data fanatic;