Integración final
Introducción
Ahora tenemos todos los recursos técnicos y funcionales que necesitamos para alcanzar nuestro objetivo final: hacer que todos nuestros servicios sean totalmente redundantes y tolerantes a fallos.
Todavía tenemos que eliminar los últimos puntos únicos de fallo (SPOF). Los recursos ya existentes y los que ofrece Kubernetes nos permitirán salvar los últimos obstáculos y completar nuestra aplicación principal.
Volúmenes persistentes
1. Clúster NFS-HA
Ya hemos cubierto los volúmenes persistentes en el capítulo Despliegue con Kubernetes. Además, en el capítulo Almacenamiento de alta disponibilidad, creamos un clúster NFS de alta disponibilidad, con un playbook de Ansible que le permite crear recursos compartidos como desee. Kubernetes soporta volúmenes NFS. Nuestro ejemplo se basa en VirtualBox VMs y no tenemos ninguna clase de almacenamiento (storage class) para estos volúmenes. Pero podemos adaptar nuestro playbook para crear volúmenes persistentes usando una plantilla y añadirla a Kubernetes.
La plantilla Jinja nfs-pv.yaml.j2 es la siguiente:
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ final_pvname }}
labels:
size: {{ pvsize }}i
format: xfs
{% if labels is defined %}{% for k,v in labels.items() %}
{{ k }}: {{ v }}
{% endfor %}{% endif %}
spec:
capacity:
storage: {{ pvsize }}i
accessModes:
- ReadWriteOnce
- ReadWriteMany
nfs:
path: /{{ final_pvname }}
server: {{ nfs_vip }}
persistentVolumeReclaimPolicy: Recycle
{% if storageclass is defined %} storageClassName:
{{ storageclass }}{% endif %}
NFS permite utilizar el modo de acceso RWX (Read-Write-Many): el volumen puede ser compartido entre varios pods, lo que es ideal para nuestra aplicación eni-todo y sus descargas de archivos. Pero también puede utilizar el modo RWO (Read-Write-Once): un volumen se asocia a un único pod y no se puede compartir.
La línea...
Registry Docker
1. Arquitectura
En el capítulo Infraestructura y servicios básicos, instalamos nuestro registro Docker en el servidor infra01. Este servicio no es de alta disponibilidad. Su pérdida no es necesariamente grave (se reinstala rápidamente); tiene que esperar a que se reinicie o, en el peor de los casos, en caso de pérdida de almacenamiento, reconstruir las imágenes (si no tiene una copia de seguridad). Sin embargo, esto no deja de ser un SPOF. Vamos a utilizar todos los medios a nuestro alcance para resolver este problema.
Vamos a desplegar uno o varios pods de registro, tomados de la imagen proporcionada por Docker, la misma que en el capítulo de Infraestructura y servicios básicos. El registro que vamos a crear utilizará uno de los volúmenes persistentes que acabamos de crear para almacenar nuestras imágenes.
El registro se expondrá mediante un servicio y, sobre todo, un enrutador ingress. Nuestros repartidores de carga HAProxy proporcionarán acceso global antes del enrutador ingress.
Podríamos desplegar este registro en HTTP hasta los routeres ingress y gestionar la terminación SSL/TLS sólo en los repartidores HAProxy. Pero esta es una oportunidad para aplicar algunas de las técnicas del controlador ingress Nginx: vamos a comunicarnos utilizando cifrado de extremo a extremo.

Figura 1: Integración de un registro Docker en Kubernetes
2. Generar un secreto
Primero cree el archivo:
$ kubectl create ns registry
Los pods deben poder acceder a las claves y certificados de registry.diehard.net.
Cree un secreto de tipo TLS (consulte el capítulo Despliegue con Kubernetes), que puede utilizar tanto para el registry como para la ruta ingress, especificando su nombre y dónde encontrar los valores (adapte la ruta en consecuencia):
$ kubectl -n registry create secret tls ssl-key-cert \
--key=./certs/registry.diehard.net.key \
--cert=./certs/registry.diehard.net.crt
secret/ssl-key-cert created
El resultado es el siguiente (truncado). Los valores están codificados (pero no cifrados) en formato base64:
$ kubectl -n registry get secret ssl-key-cert -o yaml
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZEa
...
QgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key:...
Base de datos
1. Problemática
Hasta ahora, hemos estado utilizando la llamada base de datos autónoma: una única instancia iniciada en un servidor (modelo on-premise) o iniciada en forma de contenedor. El problema es obvio: no hay tolerancia a fallos. En caso de parada voluntaria o involuntaria del servicio, nuestra aplicación eni-todo deja de ser accesible. Así que este es un SPOF que vamos a eliminar.
Existen varias soluciones para garantizar la disponibilidad, con o sin Kubernetes. Entre las basadas en MySQL o MariaDB en modo clúster, existen soluciones multimaestro, en las que todos los servidores están activos para leer y escribir y se sincronizan entre sí y soluciones maestro-replica, en las que un maestro acepta escrituras y las réplicas aceptan lecturas, con una distribución de registros (shards).
Entre ellas están las soluciones Galera (multimaster) y Vitess (master-replicas).
En el capítulo Despliegue con Kubernetes, presentamos un proyecto de solución basado en StatefulSets, con una instancia MariaDB maestra y una o más réplicas. Como sólo hay un maestro y las réplicas son de sólo lectura, si perdemos el maestro, entonces se requieren manipulaciones para cambiar a un nuevo maestro, tanto en el lado del clúster como en el lado de las aplicaciones desplegadas. Si se pueden reducir los cambios manuales, es lo ideal.
También podríamos considerar el uso de un clúster de tipo Pacemaker, como para nuestro clúster NFS-HA, pero cambiando un servicio MySQL, su almacenamiento y su IP. Esto también significa que esta solución necesita un almacenamiento persistente de alta disponibilidad. Podríamos entonces utilizar una exportación presentada por el primer clúster o incluso añadir nuevos recursos al clúster Pacemaker existente.
Otra solución perfectamente factible, que los autores ya han implementado en proyectos menos críticos, consiste en desplegar un único pod MySQL o MariaDB a través de Kubernetes, asignarle un volumen persistente y dejar que "viva su vida". En caso de fallo o cambio de nodo o en caso de actualización, habrá cierto tiempo de inactividad, pero los mecanismos de desalojo de Kubernetes cambiarán el pod a un nuevo nodo y el servicio se reiniciará por sí solo....
eni-todo
1. Adaptaciones
Ya tenemos todo lo necesario para desplegar nuestra aplicación: volúmenes, base de datos, routeres, repartidores de carga, etc. Ya hemos desplegado la aplicación eni-todo en el capítulo Despliegue con Kubernetes, con el entorno de pruebas bajo Minikube. Sin embargo, se aplican algunos pequeños cambios. Aquí es donde los principios de DevOps realmente entran en juego.
En primer lugar, los registros de tipo sesión, que dependían en gran medida de la persistencia de la conexión, ahora se pueden gestionar de forma nativa mediante una simbiosis de Apache Tomcat y Kubernetes. Apache Tomcat dispone desde hace tiempo de mecanismos de replicación de sesiones, en particular a través del módulo Tribe. Recientemente, la clase org.apache. catalina.tribes.membership.cloud.KubernetesMembership Provider se utiliza para consultar el API de Kubernetes, lo que permite a Tomcat descubrir a sus pares y configurar la compartición de sesiones. De esta manera, los registros se conservan, a menos, claro está, que se apague todo. El interés no sólo está ligado a la información de sesión gestionada por eni-toto: la aplicación podría evolucionar para incluir, por ejemplo, un gestor de conexiones y derechos, cuya información se compartiría entre instancias, de modo que el usuario no tendría que volver a conectarse (por no hablar de perder datos) en caso de caída o reinicio de una de ellas. Por supuesto, hay que tener en cuenta el coste de red de estos intercambios y el tamaño de las sesiones replicadas.
Aprovecharemos esto para añadir la noción de persistencia de extremo a extremo: a menos que se pierda la instancia actual, nos aseguraremos de que la conexión se realice siempre al mismo pod.
A continuación, vamos a utilizar el clúster Galera que creamos anteriormente, que es en sí mismo redundante, sin más adaptación que utilizar el nombre DNS interno del servicio Kubernetes.
En este ejemplo, colocamos el clúster y la aplicación en dos namespaces diferentes. Esta no es necesariamente la práctica habitual. A menudo se encuentran clústeres, especialmente en grandes empresas, donde los namespaces están dedicados a cada proyecto y la comunicación entre namespaces está...
Conclusión
Hemos recorrido un largo camino desde el primer capítulo y la instalación autónoma de nuestra aplicación. Desde la infraestructura hasta la aplicación y el acceso, ahora todo es redundante y tolerante a fallos. Hemos alcanzado nuestro objetivo: nuestra aplicación es altamente disponible.
Más allá de los mecanismos técnicos establecidos, la consecución de este objetivo es el resultado de una estrecha colaboración entre el desarrollador de la aplicación eni-todo y el operador responsable de la plataforma, es decir, entre Dev y Ops. El desarrollador debe tener en cuenta los comentarios del equipo de operaciones sobre la arquitectura de su producto: integrar un mecanismo de sesión, considerar que varias instancias de la aplicación deberán ejecutarse, permitir que la aplicación se ejecute en un contenedor, etc. El equipo de operaciones debe tener en cuenta las necesidades del desarrollador: dimensionar correctamente los recursos, gestionar los mecanismos de afinidad, proponer el almacenamiento necesario, etc.
Y podemos ver lo difusos que pueden llegar a ser los límites en ciertas áreas. Kubernetes, por ejemplo, requiere un profundo conocimiento del sistema y de la red para su instalación y mantenimiento, pero también amplios conocimientos funcionales para manipular sus objetos. Al fin y al cabo, Dev y Ops tienen buenos conocimientos...