1 La necesidad de escenarios intermedios de migración a cloud native

Toda definición canónica de un roadmap de transformación digital incluye estrategias específicas para escenarios de transición, denominados habitualmente como “lift and shift” o “improve and move”. Todos ellos están encaminados a resolver diferentes escenarios intermedios en la migración de una aplicación hacia una arquitectura objetivo, minimizando los riesgos asociados a la misma.

Existen multitud de sectores que en este camino de transformación se enfrentan a desafíos importantes como la presencia de soluciones propietarias, aplicaciones complejas o, incluso, imposiciones regulatorias, que exigen una estrategia de modernización menos agresiva que sitúe esas soluciones intermedias como ciudadanos de primer nivel y permita afianzar una transición segura hacia el objetivo final.

Por ejemplo, la migración hacia plataformas puramente basadas en contenedores no siempre es factible, viable o incluso recomendable. En un proceso tan complejo como la adopción de Kubernetes el pragmatismo adquiere una importancia decisiva.

Convivencia de virtualización y contenedores

Uno de los principales retos en la adopción de Kubernetes es abordar la transición desde virtualización o bare-metal a contenedores.

Una estrategia en la que los objetivos sean tecnológicos y finalistas forzará la migración a contenedores, como una condición sine qua non para que una aplicación pueda definirse como modernizada a cloud native. Es decir, en este modelo la virtualización no tiene cabida en cloud native y, por tanto, el camino crítico de todas las aplicaciones en este roadmap de transformación digital es una migración forzosa a contenedores.

El resultado más que probable de una estrategia de este tipo son máquinas virtuales disfrazadas de contenedores.

Dado que el foco está más en el destino y el formato que en interiorizar la transformación, es probable que el proceso termine sin adoptar las prácticas, patrones, arquitecturas y beneficios propios de una plataforma cloud native basada en Kubernetes, más allá de una cuestión de formato (contenedores).

Una estrategia más sensata es dividir el dominio de la aplicación. Algunos componentes se mantienen como servicios virtualizados, mientras que otros se migran a contenedores.

Este enfoque permite que cada aplicación pueda tener diferentes velocidades en la transición a cloud native y, por tanto, que haya una oportunidad de partida para una adopción de cloud native segura.

En este escenario podríamos definir aplicaciones cloud native híbridas como aquellas cuyo dominio está compuesto de servicios virtualizados y contenedores.

El reto tecnológico de esta estrategia es la convivencia entre servicios virtualizados y otros basados en contenedores. Y para lograrlo pueden adoptarse diferentes implementaciones.

Dos plataformas en paralelo: virtualización y Kubernetes

Aunque puede parecer atractivo desplegar una plataforma de virtualización junto con Kubernetes, bien sea independiente (como oVirt, VMWare…) o integrada en el stack de un proveedor cloud (Amazon, Azure, Google…), esta arquitectura mixta supone una gestión duplicada de todos los aspectos de una plataforma.

Toda funcionalidad transversal (como HA, networking, backups, observabilidad, seguridad, disaster recovery) debe resolverse en dos plataformas que no convergen. El desarrollo, integración, despliegue y operación quedan totalmente afectados por la decisión, impactando de manera importante la velocidad de entrega y la complejidad del entorno.

En el caso de una aplicación cloud native híbrida (componentes virtualizados y en contenedores), el despliegue queda dividido en dos plataformas estancas.

El despliegue queda dividido en dos plataformas estancas.

Kubernetes como plataforma única de virtualización y contenedores: KubeVirt

KubeVirt es un operador de Kubernetes que permite desplegar y administrar máquinas virtuales en Kubernetes, extendiendo sus capacidades para gestionar cargas virtualizadas, permitiendo así combinar máquinas virtuales y contenedores en un clúster de Kubernetes.

Logo de KubeVirt con slogan.

Gracias a esta tecnología, es posible simplificar la gestión y eliminar la necesidad de plataformas separadas para los servicios virtualizados y los contenedores. El dominio de una aplicación cloud native híbrida se circunscribe a la Kubernetes, permitiendo una gestión unificada de la aplicación.

Gracias a KubeVirt se elimina la necesidad de plataformas separadas.

Kubevirt es un proyecto open source, auspiciado por la Cloud Native Computing Foundation e impulsado también por Red Hat, como base de su offering comercial OpenShift Virtualization.

Las ventajas de KubeVirt son numerosas, pero destacan las siguientes:

Un ejemplo de aplicación cloud native híbrida

Para poner en contexto todas sus ventajas, vamos a definir una aplicación cloud native híbrida y a resolver el despliegue completo de la misma, donde convivirán contenedores con máquinas virtuales.

Una aplicación cloud native híbrida es aquella cuyo dominio está compuesto de servicios virtualizados y contenedores.

Imaginemos un escenario sencillo:

En este escenario, la aplicación python ya está migrada a contenedores, pero la base de datos aún está virtualizada.

Una aplicación python que expone un endpoint.

El despliegue de esta aplicación requiere una solución para la comunicación entre la base de datos y el driver de MongoDB que ejecuta la aplicación python para recuperar el dato.

Implementación con Kubevirt

Nota: Esta es una aplicación de ejemplo desarrollada para la demostración de este caso de uso. No se han aplicado prácticas de seguridad o gestión de la configuración, entre otros asuntos.

Aplicación Python

La aplicación Python es una aplicación web simple creada con Flask:

from flask import Flask, jsonify
from pymongo import MongoClient

app = Flask(__name__)

@app.route('/')
def welcome():

   client = MongoClient('mongodb', 27017)

   db = client['demo']
   collection = db['names']
   name = collection.find_one()['name']
   client.close()
   rc = '\n'
   return f"Welcome to a cloud native hybrid application, {name}! {rc}"

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8080)

Expone un endpoint que devuelve un mensaje en el que un nombre se extrae de una base de datos MongoDB.

Deployment de la aplicación

apiVersion: apps/v1
kind: Deployment
metadata:
 name: kubevirt-networking-poc-helm
 namespace: kubevirt-networking-poc
spec:
 replicas: 1
 selector:
   matchLabels:
     app: kubevirt-networking-poc
 template:
   metadata:
     labels:
       app: kubevirt-networking-poc
   spec:
     containers:
       - name: kubevirt-networking-poc
         image: "kubevirt-networking-poc:latest"
         ports:
           - containerPort: 8080


---
apiVersion: v1
kind: Service
metadata:
 name: kubevirt-networking-poc-service
 namespace: kubevirt-networking-poc
spec:
 type: LoadBalancer
 ports:
   - port: 8080
     targetPort: 8080
 selector:
   app: kubevirt-networking-poc

El recurso Deployment define cómo se implementa y se ejecuta la aplicación Python en el clúster de Kubernetes.

El Deployment especifica la cantidad de réplicas, en este caso 1, las etiquetas y la configuración de los contenedores. El contenedor se crea a partir de la imagen de la aplicación y se expone en el puerto 8080.

El recurso Service se utiliza para exponer la aplicación Python en la red y permitir que otros componentes del clúster, así como los usuarios finales, se comuniquen con ella.

El recurso Service define el tipo de servicio (LoadBalancer), el puerto en el que se expone la aplicación (8080) y las etiquetas que se utilizan para seleccionar los Pods que implementan la aplicación.

Recurso Service

Base de datos virtualizada

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
 name: mongodb-vm
 namespace: kubevirt-networking-poc

spec:
 running: true
 template:
   metadata:
     labels:
       kubevirt.io/domain: mongodb-vm
   spec:
     domain:
       devices:
         disks:
           - name: containerdisk
             disk:
               bus: virtio
           - name: cloudinitdisk
             disk:
               bus: virtio
           - name: mongodb-data
             disk:
               bus: virtio
         interfaces:
           - name: default
             masquerade: {}
             model: virtio
       resources:
         requests:
           memory: 2Gi
           cpu: 1
     networks:
       - name: default
         pod: {}
     volumes:
       - name: containerdisk
         containerDisk:
           image: "quay.io/containerdisks/centos-stream
:9"
       - name: cloudinitdisk
         cloudInitNoCloud:
           userData: |
             #cloud-config
             users:
               - name: superuser
                 plain_text_passwd: 'superuser'
                 lock_passwd: false
                 groups: users, admin
                 sudo: ALL=(ALL) NOPASSWD:ALL
             hostname: mongodb-vm
             package_upgrade: true
             write_files:
               - path: /etc/yum.repos.d/mongodb-org-6.0.repo
                 content: |
                   [mongodb-org-6.0]
                   name=MongoDB Repository
                   baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/6.0/x86_64/
                   gpgcheck=1
                   enabled=1
                   gpgkey=https://www.mongodb.org/static/pgp/server-6.0.asc
               - path: /root/init-mongo.js
                 content: |
                   db = new Mongo().getDB("demo");
                   db.names.insertOne({name: "John Doe"});
               - path: /root/adjust_config_mongo.sh
                 content: |
                   sed -i 's/^\( *bindIp *: *\).*/\10.0.0.0/' /etc/mongod.conf
             runcmd:
               - dnf install -y epel-release
               - yum install -y mongodb-org
               - systemctl daemon-reload
               - systemctl enable --now mongod.service
               - sleep 10
               - chmod +x /root/adjust_config_mongo.sh
               - sh /root/adjust_config_mongo.sh
               - systemctl restart mongod.service
               - sleep 10
               - mongosh < /root/init-mongo.js
       - name: mongodb-data
         persistentVolumeClaim:
           claimName: mongodb-pvc
Uno de los Custom Resources que aporta KubeVirt es VirtualMachine.

Uno de los Custom Resources que aporta KubeVirt es VirtualMachine, que permite definir instancias de servicios virtuales de forma declarativa.

La especificación de VirtualMachine incluye información sobre la configuración y los recursos de la máquina virtual:

Cloud-Init: Bootstrap de la máquina virtual

Cloud-init es una herramienta open source que se utiliza para inicializar y configurar instancias de máquinas virtuales y servidores en la nube al momento de su creación. Cloud-init se ejecuta en la fase de arranque de la instancia, permitiendo la personalización de la configuración del sistema, la instalación de paquetes, la creación de usuarios, la configuración de la red, la asignación de claves SSH y muchas otras tareas de administración.

La mayoría de las imágenes de sistemas operativos en la nube (por ejemplo, Ubuntu, CentOS, Fedora, Debian, etc.) ya tienen cloud-init preinstalado. Los proveedores de servicios en la nube como AWS, Google Cloud o Azure admiten el uso de cloud-init para personalizar las instancias en el momento del lanzamiento.

Cloud-init utiliza un archivo de configuración en formato YAML para definir las acciones y configuraciones específicas que se aplicarán a la instancia. Este archivo de configuración puede ser proporcionado por el usuario al momento de crear la instancia en el proveedor de servicios en la nube, o puede ser almacenado en un repositorio centralizado y accesible para su uso en múltiples instancias.
Cloud-init se puede también combinar con herramientas de gestión de la configuración como Chef o Puppet, para ampliar las opciones de bootstrapping de servicios virtuales.

En el caso concreto del ejemplo que nos ocupa, la instalación y configuración de MongoDB en la máquina virtual Fedora se lleva a cabo mediante un archivo de configuración de Cloud-init, que se especifica en el archivo vm.yaml dentro del volumen llamado cloudinitdisk.

El archivo de configuración de Cloud-init se compone de varias secciones, que incluyen la instalación de paquetes, la configuración de archivos y la ejecución de comandos para una puesta en marcha básica de MongoDB.

También se cargan los datos en la colección de mongo de la aplicación.

Volúmenes

La máquina virtual utiliza principalmente dos volúmenes:

Comunicación entre la máquina virtual y el contenedor

apiVersion: v1
kind: Service
metadata:
 name: mongodb
 namespace: kubevirt-networking-poc
spec:
 ports:
 - name: mongo
   port: 27017
   protocol: TCP
   targetPort: 27017
 selector:
   kubevirt.io/domain: mongodb-vm
Con este recurso “Service” podemos exponer la base de datos MongoDB

Con este recurso “Service” podemos exponer la base de datos MongoDB que se ejecuta en la máquina virtual en el entorno de OpenShift, permitiendo que otros componentes del namespace, como la aplicación Python, se comuniquen con ella.

La especificación del recurso Service incluye la siguiente información:

Resultado final

Cuando un usuario invoque el endpoint que expone el servicio asociado a la aplicación Python, recibirá esta respuesta:

Resultado final

Donde el nombre John Doe se ha recogido de una base de datos MongoDB virtualizada sobre una plataforma Kubernetes. Yikes!

Show me the code!

Todo el código está disponible en este repositorio.

2 Ventajas de la convivencia de virtualización y contenedores en Kubernetes

Despliegue de aplicaciones cloud native híbridas

KubeVirt permite unificar el despliegue completo de la aplicación sobre Kubernetes. Si utilizáramos una solución como Helm, podríamos desplegar y versionar nuestra aplicación cloud native híbrida en conjunto, facilitando no solo la instalación, sino también el rollout de nuevas versiones o el rollback.

Dado que se puede definir de forma declarativa el servicio virtualizado, tal y como se definen otras necesidades en Kubernetes, podemos emplear ya todas las técnicas disponibles para gestionar el despliegue, como gestión de la configuración, templating, parametrización, placement rules, etc.

También es interesante destacar cómo es posible trabajar con Persistent Volumes a este nivel, permitiendo la integración de las soluciones de storage de Kubernetes con las máquinas virtuales.

Networking de aplicaciones cloud native híbridas

Dado que virtualización y contenedores se despliegan en Kubernetes, se pueden utilizar todas las técnicas de networking a nuestro alcance para garantizar la comunicación entre contenedores y servicios virtualizados.

En este caso hemos empleado un Service para permitir la comunicación entre un contenedor, donde tenemos el cliente de Mongo en la aplicación de Python, y una máquina virtual, donde tenemos la base de datos.

Además, podemos segmentar el tráfico de red respecto a otras aplicaciones gracias al uso de namespaces.

3 Conclusiones

Evolución de aplicaciones cloud native híbridas

¿Es posible profundizar aún más en el uso de virtualización en Kubernetes? Sí, en este post nos centramos en el uso de las NetworkPolicies y, próximamente veremos, certificados, observabilidad o políticas.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.