Manageability: la importancia de un sistema maleable en un mundo Cloud Native

Gonzalo Fernández Rodríguez    14 septiembre, 2021

Hace poco publicamos el post Castañas en la nube o que significa que mi software sea cloud native en el que tratábamos de explicar qué significaba el término Cloud Native y qué atributos tendrían que tener nuestras aplicaciones/sistemas para que realmente pudieran ser considerados como Cloud Native.

Comentamos la necesidad de escalar que tienen las aplicaciones de ahora derivada de la fuerte demanda de recursos de las actuales aplicaciones, necesidad a la que daba respuesta la tecnología Cloud Computing ofreciendo bajo demanda los recursos necesarios (casi infinitos), de forma instantánea y pagando solo por su uso.

Sin embargo, cualquier aplicación/sistema distribuido lleva asociado una complejidad, en parte heredada de la dependencia que tienen entre sí los diferentes subsistemas que lo componen (almacenamiento, red, cómputo, bases de datos, servicios/microservicios de negocio, etc.).

Inevitablemente el hardware fallará alguna vez, la red sufrirá cortes, los servicios podrán bloquearse y no responder, etc. Con este panorama no basta con mover las aplicaciones desde los entornos “on-prem” a los entornos “cloud” para convertirlas en aplicaciones Cloud Native, tienen que ser capaces de sobrevivir en entornos de este tipo, recuperarse y seguir funcionando sin que los usuarios perciban esos problemas. Por tanto, han de ser diseñadas para soportar fallos y, en definitiva, ser más fiables que la infraestructura sobre la que se ejecutan, es decir, tienen que ser resilientes.

Además de estos posibles fallos (hardware, red, servicios, etc.), existen otros elementos como son los posibles cambios en el negocio, una fluctuación de demanda de nuestros servicios o la ejecución de los mismos en distintos entornos que hacen que tengamos que actuar sobre nuestras aplicaciones para que incorporen nuevas funcionalidades o para que sigan funcionando correctamente y sin interrupciones como desean los usuarios.

El cómo de fácil o difícil sea para nuestras aplicaciones poder cambiar su comportamiento, bien sea para activar/desactivar alguna funcionalidad, bien para desplegar/replegar más nodos del servicio sin downtime[1], o para hacer un failover[2] desde uno o más recursos que han dejado de funcionar hacia otros que aún están disponibles es de lo que vamos a hablar hoy en este post.

¿A qué nos referimos cuando decimos que nuestra aplicación es maleable?

En nuestro anterior artículo decíamos que uno de los atributos de una aplicación Cloud Native, según la CNCF, era “manageable”. Nosotros hablamos de administrable, pero probablemente el término maleable sea más acertado.

Cuando decimos que un material es maleable nos referimos a que podemos cambiar su forma sin romperlo, y sin ningún proceso de fundición, fusión, o cualquier otro proceso industrial o químico que se nos pueda ocurrir.

Buscando una analogía en el mundo software, podríamos decir que una aplicación es maleable cuando podemos modificar su comportamiento desde fuera, sin necesidad de tocar su código y sin necesidad de detener la aplicación, es decir “sin romperla”, sin que ningún usuario perciba que algo ha dejado de funcionar aunque sea momentáneamente.

Es importante destacar la diferencia entre que nuestra aplicación sea maleable o “manageable” con que nuestra aplicación sea mantenible, en cuyo caso haríamos referencia a la facilidad con la que podríamos cambiar el comportamiento o evolucionar nuestra aplicación desde dentro, es decir, haciendo cambios en el código, algo que es igualmente muy importante, pero que no es objeto de este artículo.

Para poder entender mejor lo que queremos decir, imaginemos que tenemos una aplicación o sistema en ejecución que se encuentra proporcionando un determinado servicio a N clientes, el cual necesitamos modificar por algún motivo, como por ejemplo:

  • Hay una demanda creciente/decreciente de clientes, y necesitamos incrementar/decrementar el número de ciertos recursos del sistema. Ojo, no estamos hablando de cómo resolver la escalabilidad (supongamos que nuestro servicio ha sido diseñado sin estado y está preparado para escalar horizontalmente[3 sin problemas).
  • Hemos desarrollado una nueva versión de nuestra aplicación, la hemos probado en nuestros entornos de pruebas y queremos ejecutarla en un entorno productivo donde el código es exactamente el mismo, pero los recursos como la red, las BBDD, el almacenamiento, etc. son otros.
  • Hemos detectado un problema en la configuración de un componente que hace que el servicio se comporte de forma errónea y necesitamos modificar esa configuración.

La clave para conseguir esto no está en mover nuestras aplicaciones a un entorno cloud como hemos comentado anteriormente, la clave está en seguir una serie de prácticas en el diseño de la arquitectura de nuestras aplicaciones para conseguir que no sólo podamos modificar su comportamiento sino que se pueda hacer de una forma sencilla y ágil haciendo que los usuarios perciban que el sistema funciona correctamente en todo momento.

¿Pero cómo conseguimos que nuestra aplicación sea maleable?

Observable

Bien, si esto va de hacer cambios en un sistema que está dando servicio en un entorno productivo, primero de todo hemos de enterarnos cuando es necesario hacer esos cambios. En el post Observability qué es, qué ofrece, nuestro compañero Dani Pous nos hizo una introducción a la importancia de que nuestras aplicaciones sean observables, ya que gracias a ello sabremos lo que está ocurriendo en todo momento y podremos tomar decisiones basadas en esa información que recogen nuestras métricas, logs y trazas.

Si queremos que nuestra aplicación sea maleable, es fundamental que sepamos cuándo tomar esas decisiones, para ello es necesario que dediquemos tiempo a diseñar alarmas que activen los mecanismos automáticos correspondientes que cambien el comportamiento de nuestro sistema (por ejemplo detectar un cluster de BD que no responde para poder hacer un failover automático otro), y también los dashboards que nos den información necesaria para hacer un cambio manual en la configuración y que nuestra aplicación se actualice sin necesidad de reiniciarse (por ejemplo, incrementar un timeout en un fichero de configuración para evitar rechazar peticiones de cliente).

Configurable

En segundo lugar, es necesario que nuestra aplicación tenga algún mecanismo para que de forma externa podamos cambiar su comportamiento. Tenemos que tratar de identificar qué partes de nuestra aplicación han de ser parametrizables (cadenas de conexión a BD, URLs para la invocación de servicios web, threads, memoria o CPUs activas para el performance, etc.). Esta configuración o parametrización es algo que puede cambiar entre distintos entornos (development, integration, production, etc.).

La mayoría de los lectores habrán oído hablar de The Twelve-Factor App, para los que no lo conozcan, es una metodología que crearon en su momento varios desarrolladores de Heroku y que establece doce principios que ayudan a crear aplicaciones en la cloud aportando beneficios como portabilidad, paridad[4] entre los entornos de desarrollo y los de producción o mayor facilidad para escalar entre otros.

Variables de Entorno

Uno de estos doce principios hace referencia a la configuración de las aplicaciones y nos indica que el código de las aplicaciones se mantiene entre los distintos entornos en los que se ejecuta. Sin embargo, la configuración varía, y por eso es importante que la configuración se mantenga separada del código. Añadimos también la importancia de que esa configuración se encuentre versionada en un sistema de control de versiones para facilitar la restauración de una configuración específica en caso de que fuera necesario.

Las variables de entorno tienen la ventaja de que son fáciles de implementar y de cambiar entre despliegues sin cambiar el código, además son soportadas por cualquier lenguaje y cualquier sistema operativo. Pero no todo son ventajas en el uso de variables de entorno.

Las variables de entorno definen un estado global y están compartidas con otras muchas variables, con lo cual hemos de tener cuidado a la hora de definirlas para que no se pisen, además tampoco pueden gestionar configuración más compleja que una cadena de texto. En todo caso para representar configuración a nivel de entornos (Development, Staging, Production, etc.) son una solución muy adecuada.

Argumentos en Línea de Comandos

Otra opción utilizada para la configuración de aplicaciones simples, que además no requiere de ningún fichero, son los argumentos en línea de comandos. Este tipo de configuración, que se proporciona en la línea de comandos cuando arrancamos una aplicación, está indicado para cuando interactuamos con scripts. Sin embargo cuando las opciones de configuración se complican, los argumentos en línea de comandos no son una opción manejable, se complican en exceso y además tienen un formato inflexible

Ficheros de Configuración

Los ficheros de configuración por otra parte ofrecen también muchas ventajas, especialmente cuando tenemos aplicaciones verdaderamente complejas, entre otras cosas porque permiten representar estructuras más complejas que pueden agrupar lógica de nuestra aplicación que está relacionada. No obstante, cuando usamos ficheros de configuración puede ser complicado mantener la integridad de la configuración en todo momento de los nodos de un cluster, ya que tendremos que distribuir esta configuración a cada uno de los nodos. Este problema puede mejorarse mediante la incorporación de una solución tipo etcd o consul que nos ofrecen un sistema de almacenamiento (clave, valor) de forma distribuida.

Despliegues y reconfiguraciones sin parada de servicio

Por último, y no menos importante, necesitamos contar con un sistema de despliegue automatizado que permita entre otras cosas:

  • Actualizar todos los nodos necesarios de un sistema a la nueva configuración. Los tiempos en los que una persona del equipo de operaciones actualizaba la configuración de los diferentes nodos que daban servicio a un sistema o componente del mismo han pasado a la historia. Hoy en día hay servicios que soportan millones de usuarios y tiene miles de nodos activos. ¿Alguien se imagina cómo actualizar miles de nodos si no es de forma automática?
  • Gestionar el escalado/desescalado de los componentes de un sistema/aplicación de forma progresiva sin necesidad de detener el servicio. Esto incluye tareas como el despliegue de la infraestructura, despliegue del software, configuración de balanceadores, etc.

Afortunadamente, el uso extendido de contenedores y orquestadores como Kubernetes en las aplicaciones Cloud Native, hace que el problema de distribución de la configuración disminuya en gran medida, ya que estas plataformas ofrecen mecanismos especializados para esto, como por ejemplo el “ConfigMap” de Kubernetes, que permite también gestionar tanto variables de entorno, como parámetros en línea de comandos y ficheros de configuración.

Asimismo, Kubernetes facilita el despliegue de nuevas versiones mediante la aplicación de lo que se conoce como “Rolling Updates”, este técnica permite ir actualizando las distintas instancias de nuestras aplicaciones de forma progresiva, alojando las nuevas versiones en nodos con recursos disponibles, al tiempo que va eliminando las instancias de la versión anterior, consiguiendo de esta forma un despliegue con el ansiado “Zero Downtime”.

En todos los casos hemos de trabajar siempre con el concepto de inmutabilidad, en el cual las imágenes de los contenedores desplegados en nuestra aplicación así como los objetos de configuración son inmutables. De este modo, una vez desplegadas las aplicaciones, cualquier cambio requerirá la sustitución del contenedor por una nueva imagen o del fichero de configuración u objeto (si por ejemplo tenemos un ConfigMap de Kubernetes) por el de la nueva versión.

Conclusión 

Las aplicaciones Cloud Native usan arquitecturas basadas en microservicios, esto facilita que las aplicaciones se puedan desarrollar y evolucionar más fácilmente (equipos independientes, desacoplamiento funcional, independencia de tecnologías, etc.).

El uso de contenedores para el despliegue de los microservicios (e.g: Docker) y los orquestadores de contenedores (e.g: K8s) cada vez más extendidos facilitan el escalado, desescalado de las aplicaciones y la gestión de miles de nodos dentro de una aplicación/servicio.

Sin embargo, todas estas facilidades no vienen exentas de problemas, la gran cantidad de nodos que pueden estar dando servicio en una aplicación Cloud Native hace que se multiplique el número de posibles fallos y por tanto es necesario que diseñemos nuestros sistemas con la mente puesta en que van a fallar.

Adicionalmente, hemos de ser capaces de distribuir las nuevas versiones (tanto de código como de configuración) a través de un enorme número de instancias y sin que los usuarios perciban una pérdida del servicio. La cantidad enorme de máquinas, servicios, etc. manejados dentro de nuestras aplicaciones hace inviable que estos cambios puedan ser manuales y exige además que trabajemos con el concepto de inmutabilidad para garantizar que cada cambio está asociado a una versión que puede ser restaurada en cualquier momento.

Referencias

https://livebook.manning.com/book/cloud-native-spring-in-action/chapter-1/v-1/87

https://docs.microsoft.com/en-us/dotnet/architecture/cloud-native/definition

https://12factor.net/config


[1] Tiempo de indisponibilidad de una máquina o un servicio

[2] Procedimiento mediante el cual un sistema transfiere el control a otro (duplicado) cuando se detecta un fallo en el primero.

[3] Estrategia de crecimiento para que un servicio tenga mayor capacidad basada en el aumento del número de instancias que responden a las peticiones de los clientes.

[4] Se refiere a la importancia que tiene el que los entornos de desarrollo y productivos sean lo más similares posibles con el objetivo de reducir el tiempo entre que el código está listo y es llevado a producción.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *