Cómo aplicar las prácticas de CI/CD a los diferentes pipelines de una solución

Al momento de desarrollar una solución en AWS es común implementar una serie de pasos que automatizan la construcción y despliegue de los diferentes componentes que conforman dicha solución, llamados pipelines de integración continua (CI) y despliegue continuo (CD). Por ejemplo, en una aplicación web moderna típica pueden encontrarse pipelines para la infraestructura, para el API de backend y para la aplicación estática. A su vez, cuando se adopta un esquema multicuenta en donde se tiene una cuenta AWS por cada ambiente (como desarrollo, calidad y producción), este número de pipelines se multiplica. Esta cantidad de pipelines se vuelve difícil de gestionar a medida que se van introduciendo cambios en ellos, ya sea para corregir problemas o para incluir nuevas capacidades dentro de los pipelines.

Con el fin de facilitar la gestión de tantos pipelines es necesario aplicar sobre estos las mismas estrategias de CI/CD usadas para las aplicaciones, por ello hablamos de pipeline de pipelines: pipeline². En este post vamos a revisar varios aspectos que debemos tener en cuenta para implementar estas estrategias.


Estructura Multicuenta

En un entorno empresarial en donde se tiene implementada una organización con múltiples ambientes y necesidades de uso, se recomienda crear una cuenta para cada ambiente como un mecanismo para restringir accesos y para aislar efectos colaterales en el uso de los servicios de AWS, como por ejemplo alcanzar los límites de un servicio debido a una prueba no controlada o limitar el alcance de una brecha de seguridad. Así mismo, es buena práctica contar con una cuenta en donde se centralice todo el uso de los servicios relacionados con los pipelines (ver cuenta DevOps Tools de la Figura 1).

Tal como se puede observar en la siguiente imagen, cada pipeline tiene su propio repositorio en donde se almacenan los archivos que se requieren para construir y desplegar los artefactos correspondientes a cada pipeline. En el caso de un pipeline de infraestructura tendremos las plantillas de CloudFormation más el script de CodeBuild (buildspec); y en el caso de un pipeline de una aplicación contenerizada tendremos el código fuente de la aplicación, mas el dockerfile y el buildspec. Ante la realización de un commit en un repositorio el pipeline se ejecutará y desplegará los cambios en sus respectivos ambientes.


Figura 1 - Pipelines de una aplicación contenerizada en Fargate


Estrategias de Liberación

En este punto es importante resaltar que dependiendo del nivel de madurez de la organización es posible tener un pipeline que pase por todos los ambientes automáticamente hasta llegar a producción, logrando lo que es conocido como Despliegue Continuo (Continuous Deployment). En un esquema como este se adoptan prácticas de gestión de código en Git como Trunk Based Development, en donde prácticamente solo se tiene una rama de código (master o trunk) a la cual llegan todos los cambios, y si llegan a haber otras ramas estas son de corta duración (ver Figura 2).


Figura 2 - Pipeline de despliegue continuo con Trunk based development


Sin embargo, una gran mayoría de las organizaciones no está aún al nivel de automatización requerido por el despliegue continuo, por lo cual deben iniciar con un esquema que se asemeje a su proceso actual y de allí aplicar ciclos de mejora continua hasta llegar al nivel de automatización esperado. En escenarios como este, nos podemos encontrar con ciclos de despliegue mayores a un par de semanas, pruebas manuales, aprobaciones manuales de diferentes entes de control, ambientes con versiones diferentes del software desplegado y demás cosas por el estilo.

Para los casos descritos anteriormente es práctico aplicar un esquema de control de versiones como Git Flow, en donde existen ramas que representan la versión del software que se encuentra desplegada en cada ambiente. En este esquema es normal tener un pipeline asociado a cada rama, el cual se encarga de construir y desplegar los artefactos en su respectivo ambiente, tal como se muestra en la siguiente figura.


Figura 3 - Pipelines asociados a una rama por ambiente


En lo que resta de este post vamos a trabajar con escenarios como los presentados en esta imagen debido a que son los que más nos hemos encontrado en la práctica y porque en ellos se hace particularmente importante tener un pipeline para el despliegue de los demás pipelines.

Recursos compartidos

Adicionalmente a lo mencionado anteriormente, cada pipeline requiere una serie de recursos o servicios para poder funcionar, los cuales en la mayoría de los casos pueden compartirse entre los diferentes pipelines. Algunos ejemplos de este tipo de recursos son:

Entre todos los pipelines:
  • Llave KMS usada para la encriptación en reposo de los artefactos y para los despliegues cross-account.
  • Bucket S3 para los artefactos que se pasan entre los pasos de un pipeline.
  • Tópicos SNS para notificaciones al equipo de desarrollo y para aprobadores manuales o automáticos.
  • Herramientas de validación de código como SonarQube.
Entre los pipelines de una solución (ver Figura 3):
  • Repositorio Git.
  • Repositorio Contenedores.

Esto nos lleva a separar en un pipeline el aprovisionamiento de los recursos compartidos y en otro los recursos correspondientes a una solución. De esta manera, los ARN de los recursos compartidos son pasados por parámetro a los pipelines de las soluciones, para que sean usados por estos.

En la Figura 4 podemos observar una representación del aprovisionamiento de los recursos compartidos, en donde se inicia con la ejecución de una plantilla de CloudFormation que crea estos recursos. Posteriormente, las plantillas de CloudFormation son incorporadas dentro del repositorio Git shared-devops para que cada vez que éstas sean modificadas se ejecute el pipeline shared-devops-pipeline y se desplieguen las actualizaciones correspondientes. Aquí es importante tener en cuenta que si se decide actualizar el stack de shared-devops-template desde el pipeline shared-devops-pipeline se producen fallos en la ejecución del pipeline cuando se modifica la definición del pipeline dentro la plantilla (un escenario del huevo y la gallina). En caso tal, se pueden tomar dos opciones: ejecutar de nuevo manualmente el pipeline o retirar la ejecución del shared-devops-template del pipeline.


Figura 4 - Despliegue de recursos compartidos


Pipeline Maestro de la Solución

Llegamos finalmente a la definición del pipeline que se encarga de crear los pipelines de una solución. Como dijimos anteriormente, una solución normalmente está conformada por varias capas: infraestructura, backend, frontend, etc. Cada una de estas capas deben tener sus correspondientes pipelines por ambiente.

Tal como se muestra en la figura 5, se implementa un pipeline que se encarga de crear los pipelines por cada capa (infra y backend por simplicidad del gráfico) y por cada ambiente, apoyándose en los recursos compartidos. Es posible que cada capa requiera una definición de pipeline diferente, por lo que requiere una plantilla separada por cada capa. En caso contrario se podría reutilizar la misma plantilla varias veces, solo que cambiarían los parámetros con los que se le llama. Los parámetros con los que se llaman las plantillas pueden estar especificados en archivos params.json dentro del repositorio, en la configuración del pipeline o en artefactos generados por un script de CodeBuild que lea parámetros de Parameter Store, Secrets Manager o una BD.


Figura 5 - Pipeline para pipelines de la Solución


De la misma manera como se realizan validaciones en los pipelines de infraestructura  de CloudFormation, se deben incluir pasos que comprueben la adopción de las buenas prácticas en las plantillas que crean los pipelines. Por ende, se recomienda usar herramientas como yamllint, cfn-lint, task-cat y demás para ayudar en esta labor.

Finalmente, cada vez que se desea hacer una mejora dentro de un pipeline, solo es necesario modificar una plantilla y la actualización se despliega automáticamente. Así mismo, si se desea incluir una nueva capa dentro de la solución se adicionan los pasos que crean los nuevos pipelines.


Figura 6 - Ejemplo de pipeline de solución con dos capas y tres ambientes

Resumen

En este post hemos descrito cómo se pueden aplicar las estrategias de CI/CD sobre los propios pipelines de CI/CD con el fin de obtener los beneficios de consistencia, rapidez, automatización y calidad que se logran cuando se adoptan estas estrategias en las soluciones de una organización.