CI/CD para arquitecturas de microservicios

Azure

Una de las principales razones de las arquitecturas de microservicios es la existencia de ciclos de lanzamiento más rápidos. Sin un buen proceso de CI/CD, no logrará la agilidad que los microservicios prometen. En este artículo se describen los desafíos y se recomiendan algunas estrategias para resolver el problema.

¿Qué es CI/CD?

Cuando se habla de CI/CD, en realidad se está hablando de varios procesos relacionados: integración continua, entrega continua e implementación continua.

  • Integración continua. Los cambios de código se combinan con frecuencia en la rama principal. Los procesos automatizados de compilación y pruebas garantizan que el código de la rama principal siempre tenga una calidad de producción.

  • Entrega continua. Los cambios en el código que pasan el proceso de CI se publican automáticamente en un entorno similar al de producción. La implementación en el entorno de producción real puede requerir aprobación manual, pero, en caso contrario, se realiza automáticamente. El objetivo es que el código siempre esté listo para implementarse en producción.

  • Implementación continua. Los cambios de código que pasan los dos pasos anteriores se implementan automáticamente en producción.

Estos son algunos objetivos de un proceso de CI/CD sólido para una arquitectura de microservicios:

  • Cada equipo puede compilar e implementar los servicios que posee de forma independiente, sin afectar ni interrumpir a otros equipos.

  • Antes de implementar en producción una nueva versión de un servicio, se implementa en entornos de desarrollo, pruebas y control de calidad para su validación. Se aplican pruebas de calidad en cada fase.

  • Se puede implementar una nueva versión de un servicio en paralelo con la versión anterior.

  • Hay en vigor directivas de control de acceso suficientes.

  • En el caso de las cargas de trabajo en contenedor, puede confiar en las imágenes de contenedor que se implementan en producción.

¿Por qué es importante una sólida canalización de CI/CD?

En una aplicación monolítica tradicional, hay una canalización de compilación única cuya salida es el ejecutable de la aplicación. Todo el trabajo de desarrollo se envía a esta canalización. Si se encuentra un error de alta prioridad, debe integrarse, probarse y publicarse una corrección, lo que puede retrasar el lanzamiento de nuevas características. Puede mitigar estos problemas con módulos correctamente factorizados y el uso de ramas de características para minimizar el impacto de los cambios de código. No obstante, a medida que la aplicación se hace más compleja y se van añadiendo más características, el proceso de publicación de un monolito tiende a hacerse más frágil, con lo que tiene más posibilidades de presentar errores.

Según la filosofía de los microservicios, nunca debería haber una larga serie de versiones en la que todos los equipos tengan que hacer cola. El equipo que compila el servicio "A" podrá publicar una actualización en cualquier momento, sin tener que esperar a que los cambios realizados en el servicio "B" se combinen, prueben e implementen.

Diagrama de un monolito de CI/CD

Para lograr una velocidad de publicación elevada, la canalización de versiones debe estar automatizada y ser muy confiable para minimizar el riesgo. Si hace lanzamientos a producción una o más veces al día, las regresiones o las interrupciones del servicio deben ser poco frecuentes. Al mismo tiempo, si se implementa una actualización con errores, debe disponer de un método fiable para ponerla al día o revertirla rápidamente a una versión anterior de un servicio.

Desafíos

  • Muchas bases de código pequeñas e independientes. Cada equipo es responsable de compilar su propio servicio, con su propia canalización de compilación. En algunas organizaciones, los equipos pueden usar repositorios de código distintos. El uso de repositorios distintos puede provocar una situación en la que los diferentes equipos se especialicen en una parte distinta del proceso de compilación del sistema, pero nadie en la organización sepa realmente cómo implementar la aplicación completa. Por ejemplo, ¿qué ocurriría en un escenario de recuperación ante desastres si fuera necesario implementar rápidamente en un nuevo clúster?

    Mitigación: utilice una canalización unificada y automatizada para compilar e implementar servicios, de modo que este conocimiento no esté "oculto" en cada equipo.

  • Varios lenguajes y plataformas. Si cada equipo usa su propio conjunto de tecnologías, puede ser difícil crear un único proceso de compilación que sirva para toda la organización. El proceso de compilación debe ser lo suficientemente flexible como para que cada equipo pueda adaptarlo al lenguaje o la plataforma que utiliza.

    Mitigación: containerice el proceso de compilación para cada servicio. De este modo, el sistema de compilación solo tiene que poder ejecutar los contenedores.

  • Integración y pruebas de carga. Si cada equipo publica actualizaciones a su propio ritmo, puede resultar complicado diseñar una solución de pruebas integral sólida, especialmente si los servicios tienen dependencias en otros servicios. Además, poner en marcha un clúster de producción completo puede ser costoso, por lo que resulta poco probable que cada equipo ejecute su propio clúster completo a escala de producción solo para realizar pruebas.

  • Administración de versiones. Cada equipo debe tener la capacidad de implementar una actualización en producción. Eso no significa que cada miembro del equipo tenga permiso para hacerlo. Sin embargo, contar con un rol de administrador de versiones centralizado puede reducir la velocidad de las implementaciones.

    Mitigación: Cuanto más automatizado y fiable sea el proceso de CI/CD, menor será la necesidad de contar con una autoridad central. No obstante, puede tener una directiva para publicar actualizaciones de características importantes y otra distinta para publicar correcciones de errores menores. La descentralización no significa que no se requiera gobernanza.

  • Actualizaciones del servicio. Cuando se actualiza un servicio a una nueva versión, no debería interrumpir otros servicios que dependen de él.

    Mitigación: utilice técnicas de implementación, como azul-verde o el lanzamiento controlado, para cambios secundarios. Para los cambios importantes en la API, implemente la nueva versión en paralelo con la versión anterior. De este modo, los servicios que utilizan la API anterior se pueden actualizar y probar para la nueva API. Consulte Actualización de los servicios a continuación.

Un repositorio frente a varios repositorios

Antes de crear un flujo de trabajo de CI/CD, es preciso saber cómo se va a estructurar y administrar el código base.

  • ¿Los equipos trabajan en repositorios independientes o en un único repositorio?
  • ¿Cuál es su estrategia de ramificación?
  • ¿Quién puede insertar código en producción? ¿Hay un rol de administrador de versión?

El enfoque del repositorio único cada vez tiene más adeptos, pero ambos métodos tienen sus ventajas y desventajas.

  Un repositorio Varios repositorios
Ventajas Uso compartido de código
Mayor facilidad para estandarizar el código y las herramientas
Mayor facilidad para refactorizar el código
Detectabilidad (vista única del código)
Propiedad clara por equipo
Posiblemente menos conflictos en la fusión mediante combinación
Ayuda a aplicar el desacoplamiento de los microservicios
Desafíos Los cambios en el código compartido pueden afectar a varios microservicios
Mayor posibilidad de conflictos en la fusión mediante combinación
Las herramientas se deben escalar a un código base grande
Control de acceso
Proceso de implementación más complejo
Mayor dificultad para compartir código
Mayor dificultad para aplicar los estándares de codificación
Administración de dependencias
Código base difuso, mala detectabilidad
Falta de infraestructura compartida

Actualización de los servicios

Existen diversas estrategias de actualización de un servicio que ya está en producción. Aquí se describen tres opciones comunes: actualizaciones graduales, implementación azul-verde y lanzamiento controlado.

Actualizaciones graduales

En una actualización gradual, se implementan nuevas instancias de un servicio y las instancias nuevas empiezan a recibir solicitudes de forma inmediata. A medida que llegan nuevas instancias, se eliminan las anteriores.

Ejemplo. En Kubernetes, las actualizaciones graduales son el comportamiento predeterminado cuando se actualiza la especificación de pod para una implementación. El controlador de implementación crea un ReplicaSet nuevo para los pods actualizados. A continuación, se escala verticalmente el nuevo ReplicaSet mientras se reduce verticalmente el anterior a fin de mantener el número de réplicas deseado. Los pods antiguos no se eliminarán hasta que los nuevos estén listos. Kubernetes mantiene un historial de la actualización, por lo que puede revertir una actualización si es necesario.

Ejemplo. Azure Service Fabric usa la estrategia de actualización gradual de forma predeterminada. Esta estrategia es más adecuada para implementar una versión de un servicio con nuevas características sin cambiar las API existentes. Service Fabric inicia una implementación de actualización mediante la actualización del tipo de aplicación a un subconjunto de los nodos o un dominio de actualización. A continuación, se pone al día en el siguiente dominio de actualización hasta que se actualizan todos los dominios. Si no se puede actualizar un dominio de actualización, el tipo de aplicación se revierte a la versión anterior en todos los dominios. Tenga en cuenta que un tipo de aplicación con varios servicios (y si todos los servicios se actualizan como parte de una implementación de actualización) es propenso a errores. Si un servicio no se puede actualizar, toda la aplicación se revierte a la versión anterior y los demás servicios no se actualizan.

Las actualizaciones graduales plantean el desafío de que, durante el proceso de actualización, se ejecuta una combinación de versiones antiguas y nuevas que reciben tráfico. Durante este período, una solicitud podría dirigirse a cualquiera de las dos versiones.

Para los cambios importantes en la API, se recomienda admitir ambas versiones en paralelo, hasta que se actualicen todos los clientes de la versión anterior. Consulte Control de versiones de la API.

Implementación azul-verde

En una implementación azul-verde, se implemente la nueva versión junto con la versión anterior. Después de validar la nueva versión, se cambia todo el tráfico a la vez desde la versión anterior a la nueva versión. Tras el cambio, deberá supervisar la aplicación para detectar posibles problemas. Si algo va mal, puede volver a la versión anterior. Si no hubiera ningún problema, puede eliminar la versión anterior.

Con una aplicación de n niveles o monolítica más tradicional, la implementación azul-verde normalmente implicaba el aprovisionamiento de dos entornos idénticos. La nueva versión se implementaría en un entorno de ensayo y se redirigiría el tráfico del cliente al entorno de ensayo, por ejemplo, intercambiando las direcciones IP virtuales. En una arquitectura de microservicios, las actualizaciones se producen en el nivel de microservicio, por lo que normalmente implementaría la actualización en el mismo entorno y usaría un mecanismo de detección de servicios para intercambiar.

Ejemplo. En Kubernetes, no es necesario aprovisionar un clúster diferente para realizar implementaciones azul-verde. En su lugar, puede utilizar selectores. Cree un nuevo recurso de implementación con una nueva especificación de pod y otro conjunto de etiquetas. Cree esta implementación sin eliminar la implementación anterior ni modificar el servicio que hace referencia a ella. Una vez que se ejecutan los nuevos pods, puede actualizar el selector del servicio para que se corresponda con la nueva implementación.

Una desventaja de la implementación azul-verde es que, durante la actualización, se ejecuta el doble de pods para el servicio (actual y siguiente). Si los pods requieren una gran cantidad de recursos de CPU o memoria, debe escalar horizontalmente el clúster de forma temporal para controlar el consumo de recursos.

Lanzamiento controlado

En un lanzamiento controlado, se implementa una versión actualizada en un pequeño número de clientes. A continuación, supervisará el comportamiento del servicio nuevo antes de distribuirlo a todos los clientes. Esto le permitirá realizar una implementación lenta de un modo controlado, observar datos reales y detectar problemas antes de que se ven afectados todos los clientes.

Un lanzamiento controlado es más complejo de administrar que una actualización gradual o azul-verde, ya que debe dirigir de forma dinámica las solicitudes a versiones distintas del servicio.

Ejemplo. En Kubernetes, puede configurar un servicio para abarcar dos conjuntos de réplicas (uno para cada versión) y ajustar manualmente el número de réplicas. Sin embargo, este enfoque es bastante genérico, debido a la forma en que Kubernetes equilibra la carga entre los pods. Por ejemplo, si tiene un total de diez réplicas, solo puede cambiar el tráfico en incrementos del 10 %. Si utiliza una malla de servicio, puede utilizar las reglas de enrutamiento de malla de servicio para implementar una estrategia de lanzamiento controlado más sofisticada.

Pasos siguientes