En el círculo de QSO (QA, Seguridad, Sistemas e infraestructuras Cloud) de Paradigma ayudamos a otras empresas en temas tan interesantes como desplegar una aplicación en varios clusters de Kubernetes en múltiples Clouds y cómo hacer el failover de la app entre esos clusters cuando uno de ellos no está disponible.

En este post utilizaremos el Advanced Cluster Management (ACM) de Red Hat para la gestión de la aplicación y los clústeres de Kubernetes y un Balanceador Global con HAproxy para gestionar el tráfico hacia la aplicación.

La necesidad de usar más de un Cluster de Kubernetes

Con casi diez años de existencia, Kubernetes es, a día de hoy, la opción de facto para orquestar nuestras aplicaciones contenerizadas. Además de otros beneficios, nos ayuda a definir de manera ágil la estrategia de alta disponibilidad (HA) y Disaster Recovery (DR) de las aplicaciones que viven dentro de un cluster de Kubernetes.

Pero ¿qué pasa cuando el cluster de Kubernetes donde está alojada nuestra aplicación se cae por completo? Surge la necesidad de definir estrategias de HA y DR con más de un cluster de Kubernetes.

Por otro lado, también gestionamos varios clusters de k8s diferenciados basándonos en otros criterios: ambientales (desarrollo y producción), legales (empresa A y empresa B), geográficos (EMEA, APAC, AMERICAS), Clouds (OpenShift, EKS, AKS, GKE…), Blue-Green/A-B Upgrades, rendimiento, aplicaciones, etc.

Utilizar más de un cluster de Kubernetes es habitual y puede venir dado por varios motivos y cuando comenzamos a hablar de decenas o cientos de clusters la gestión manual es inviable. Nace la necesidad de centralizar el gobierno y observabilidad de los clusters y las aplicaciones que viven ellos, por ejemplo, implantando políticas de gestión, seguridad, aplicación, upgrade... para saber qué clusters las cumplen y cuáles no y tomar las medidas necesarias.

Red Hat Advanced Cluster Management y Open Cluster Management

Red Hat ha preparado para el entorno empresarial la utilidad Red Hat Advanced Cluster Management (RHACM o ACM) basándose en el proyecto open source Open Cluster Management (OCM), siguiendo su filosofía de usar, potenciar y contribuir al software libre.

Esta utilidad está enfocada en el gobierno y ciclo de vida tanto de varios clusters de Kubernetes como de las aplicaciones que viven en estos clusters en una consola web/cluster centralizada.

A diferencia de otros proyectos de gestión multi-cluster como karmada que moderniza la antigua federación en Kubernetes (KubeFed) y usa una arquitectura similar a la de un cluster de Kubernetes (un karmada Api Server, karmada Controllers y karmada Schedule), en el caso de Open Cluster Management se emplea una arquitectura “Hub-Spoke”, también llamada “Hub-Agent”, en la que hay:

Pasos del Hub Cluster

En el caso de ACM, el Hub Cluster se instala como Operador de Kubernetes en OpenShift, pero se necesita una suscripción distinta a la de OpenShift llamada OpenShift Plus, que incluye otros productos para la gestión de seguridad, redundancia de datos y otros aspectos a nivel multi-cluster.

En cuanto a los clusters gestionados no se necesita ninguna suscripción especial, ni en ACM ni en OCM. Se pueden gestionar cualquier tipo de Cluster de Kubernetes (OpenShift, GKE, AKS, EKS, etc).

Global Server Load Balancing

ACM y OCM nos ayudan en la gestión y ciclo de vida tanto de clusters de Kubernetes como de aplicaciones que vivan en ellos.

Pero ¿cómo gestionar el tráfico y las peticiones a estas aplicaciones con instancias en múltiples clusters? ¿Y en múltiples clouds? ¿Quién evita redirigir una petición a un cluster que no esté disponible?

En este post lo conseguiremos haciendo Global Server Load Balancing (GSLB) usando HAproxy en 2 regiones, MAD y BCN; y dos clusters en 2 clouds, AWS y Azure (aunque el cluster de OpenShift podríamos tenerlo en On-Premise en lugar de AWS, por ejemplo).

El balanceo global de carga de servidores es el acto de equilibrar la carga entre servidores (o servicios) distribuidos globalmente a través de un balanceador físico (Hardware LB) o Virtual (Software LB) y que dirige a los consumidores de aplicaciones a cualquiera de los cluster según las reglas que se definan en su configuración.

Peticiones del balanceo global de carga de servidores

En nuestro caso haremos balanceo de capa 7/Aplicación por lo que los consumidores hacen referencia a un único FQDN Global, en este ejemplo *.global.com, para acceder a las aplicaciones.

En el caso de OpenShift, los clusters sirven las aplicaciones en un dominio por defecto, en este post *.apps.ocp4.example.com, que se especifica cuando se instala el cluster y se crean rutas con este dominio por defecto. En un entorno multi-cluster, donde el cluster de OpenShift sea parte de un balanceador global se crearán además rutas con el dominio global *.global.com.

En AKS no hay dominios por defecto y además debemos instalar un ingress controller para gestionar el enrutado de tráfico de aplicación. En este caso, ya que nuestro GLB e ingress controller por defecto de OpenShift usan HAproxy, seguimos en esta línea y hemos instalado el Kubernetes ingress controller de HAProxy añadiéndole un certificado TLS autofirmado.

Peticiones a nuestro GLB

Hay otras formas de hacer GSLB, como por ejemplo a través de DNS, devolviendo una IP basada en el origen de la request y devolviendo la del data center más cercano o de las zonas DNS disponibles, con la desventaja de que este balanceo es por datacenter y suele ser de capa 4 (TCP), no por aplicación (capa 7) como en nuestro ejemplo.

Una ventaja es que el tráfico de aplicación no pasa por un GLB.

Demo

En esta demo usaremos 2 clusters de Kubernetes:

En nuestra demo usaremos dos clusters, MAD y BCN

En un entorno productivo se recomienda tener instancias de HAProxy, o cualquier otro tipo de balanceador físico o virtual en alta disponibilidad fuera de cualquiera de los 2 clusters, o usar las soluciones de balanceo que provee nuestro cloud favorito.

Importar un Cluster en ACM

Para comenzar la demo, suponemos que ya tenemos al menos un cluster de OpenShift y que sobre este se encuentra instalado el Advanced Cluster Management for Kubernetes.

Este primer cluster es el de MAD, al cual además hemos añadido la label “location=madrid”.

Cluster de Openshift que ya tenemos creado

Suponemos también que acabamos de instalar el segundo cluster de AKS que será el Cluster de BCN y, a continuación, vamos a importarlo.

Para ello, en la consola de ACM iremos a “Import Cluster” y pondremos los datos que necesitamos para importar el cluster añadiendo la label “location=bcn”:

Importamos el cluster BCN

Una vez guardemos, se generará un botón con un comando para ejecutar sobre el cluster de BCN y así instalar los componentes necesarios para que se complete la importación a ACM:

Se genera un botón con un comando para ejecutar sobre el cluster

Deberemos ejecutar ese comando sobre una consola con acceso de cluster-admin en el cluster de AKS BCN:

Cluster de AKS BCN

El comando creará todos los CRDs y recursos que se necesitan para que se importe el cluster.

Sin embargo, como el registry de Red Hat no permite autenticación anónima, para que el cluster de AKS pueda bajarse las imágenes que necesita en esta demo hemos tenido que hacer login en el registry con nuestra cuenta de Red Hat, crear un secreto a partir de nuestro .docker/config.json y modificar los deployments para que usen este secreto. Hay que hacerlo en el siguiente orden, ya que cada pod que se ejecute creará el siguiente deploy.

# login al registry
$ docker login https://registry.redhat.io
Username: user123
Password:
WARNING! Your password will be stored unencrypted in /home/daniel/.docker/config.json.
# crear secret ns open-cluster-management-agent con auth al registry 
$ kubectl -n  open-cluster-management-agent create secret generic registry-redhat-io-dockerconfigjson     --from-file=.dockerconfigjson=/home/daniel/.docker/config.json     --type=kubernetes.io/dockerconfigjson
secret/registry-redhat-io-dockerconfigjson created

# patchear deployments ns open-cluster-management-agent
$ for DEPLOY in klusterlet klusterlet-registration-agent klusterlet-work-agent; do
 kubectl -n  open-cluster-management-agent patch deploy $DEPLOY --type merge -p '{ "spec" : { "template" : { "spec" : { "imagePullSecrets" : [{"name": "registry-redhat-io-dockerconfigjson"}] }}}}' ; sleep 10 ; done ; echo

deployment.apps/klusterlet-work-agent patched
deployment.apps/klusterlet-registration-agent patched
deployment.apps/klusterlet-work-agent patched
# crear secret ns open-cluster-management-agent-addon con auth al registry 
$ kubectl -n  open-cluster-management-agent-addon create secret generic registry-redhat-io-dockerconfigjson     --from-file=.dockerconfigjson=/home/daniel/.docker/config.json     --type=kubernetes.io/dockerconfigjson
secret/registry-redhat-io-dockerconfigjson created

# patchear deployments ns open-cluster-management-agent-addon

$ for DEPLOY in klusterlet-addon-operator klusterlet-addon-appmgr klusterlet-addon-certpolicyctrl  klusterlet-addon-iampolicyctrl klusterlet-addon-policyctrl-config-policy  klusterlet-addon-policyctrl-framework klusterlet-addon-search klusterlet-addon-workmgr; do
 kubectl -n  open-cluster-management-agent-addon patch deploy $DEPLOY --type merge -p '{ "spec" : { "template" : { "spec" : { "imagePullSecrets" : [{"name": "registry-redhat-io-dockerconfigjson"}] }}}}' ; sleep 10 ; done ; echo

deployment.apps/klusterlet-addon-appmgr patched
deployment.apps/klusterlet-addon-certpolicyctrl patched
deployment.apps/klusterlet-addon-iampolicyctrl patched
deployment.apps/klusterlet-addon-policyctrl-config-policy patched
deployment.apps/klusterlet-addon-policyctrl-framework patched
deployment.apps/klusterlet-addon-search patched
deployment.apps/klusterlet-addon-workmgr patched

Después de confirmar que todos los pods de los deployments en los 2 namespaces (open-cluster-management-agent y open-cluster-management-agent-addon) están corriendo, deberíamos poder ver todos los datos del cluster que acabamos de importar:

Datos del cluster que acabamos de importar.

Crear Aplicación en ACM

A continuación tratamos una aplicación desplegada en 2 clusters con los siguientes datos:

LOCATION CLOUD CLUSTER INGRESS PUBLIC IP
MAD AWS OpenShift Ingress 52.31.102.2
BCN AZURE AKS HAproxy Kubernetes Ingress 20.200.200.200

En ACM crearemos una nueva aplicación de tipo subscription, que además del objeto subscription, creará otros objetos asociados como application, channel y placementRules. ACM se integra también con Aplicaciones de ArgoCD.

Aplicaciones desde varios tipos de repositorios

Se pueden crear aplicaciones desde varios tipos de repositorios: Git repositories, Helm repositories y Object storage:

Creamos una aplicación tipo Git.

Vamos a crear una aplicación de tipo “Git” en la que los manifiestos de la aplicación se deben encontrar en el repositorio de git. La aplicación de pruebas que usaremos se encuentra en la branch host-ingress y devuelve el nombre del pod y el valor de algunas variables de entorno para así diferenciar un entorno de ejecución (cluster) de otro cuando hagamos peticiones.

Aplicación de pruebas que usaremos

Siguiendo la filosofía de Kubernetes, podemos gestionar el despliegue de nuestra aplicación en los clusters que tengan una label en concreto o en todos los clusters gestionados. En nuestro caso lo haremos en todos los clusters. Una vista interesante es habilitar el “YAML: on” y ver el código YAML que generará la creación de la aplicación a través de la interfaz web:

Vemos el código YAML que generará la creación de la aplicación

Una vez se crea la aplicación la veremos en el panel de Applications:

Aplicación en el panel Applications

Si entramos en la app podemos ver un resumen de la app, editarla y ver la topología de la aplicación, clusters en los que está desplegado, etc.

La vista de topología es una vista interesante en la que podemos ver las dependencias, relaciones y estado de todos los objetos en todos los clusters en los que la app esté desplegada y de un vistazo confirmar si están ejecutándose correctamente o si tienen algún problema. En este caso, si seleccionamos el replicaset podremos ver los pods desplegados en ambos clusters y si tienen algún problema o queremos realizar alguna comprobación podemos ver el yaml y los logs del pod en la misma consola multi-cluster sin tener que ir al cluster en concreto.

Vista de pod YAML
Logs en ACM de pod en Azure

En cuanto al host Global para esta aplicación de ejemplo está definido env-app.global.com explícitamente en el manifiesto del ingress. Este manifiesto tiene también una referencia a un certificado autofirmado guardado en un secreto (global-cert) y una anotación para el cluster de OpenShift, ambas nos ayudarán a servir la app por HTTPS.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: env-api-host
  annotations:
    route.openshift.io/termination: edge
spec:
  tls:
  - secretName: global-cert
    hosts:
    - "global.com"
  rules:
  - host: env-app.global.com
    http:
      paths:
      - backend:
          service:
            name: env-api-host
            port:
              number: 8080
        path: /
        pathType: Prefix

Podemos confirmar el despliegue y funcionamiento correcto de la aplicación en cada uno de los 2 clusters haciendo una petición a la IP del Cluster y viendo que el nombre del pod que está corriendo en ellos es el mismo que aparece en la respuesta.

Haremos una petición a la url de la ruta que hemos creado con el dominio global, https://env-app.global.com, a cada cluster, pero resolviendo la IP del cluster. De lo contrario, la petición se iría al dominio de internet global.com que no controlamos:

$ kubectl -n env-app  get po
NAME                            READY   STATUS    RESTARTS   AGE
env-api-host-547bdc94b4-vdbc8   1/1     Running   0          18h

$ curl -sk https://env-app.global.com/ --resolve env-app.global.com:443:20.200.200.200 | jq .
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-547bdc94b4-vdbc8",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000u"
}
$ oc -n env-app get pods
NAME                           READY   STATUS    RESTARTS   AGE
env-api-host-5dcbcfcff-hwwtg   1/1     Running   2          19h

$ curl -sk https://env-app.global.com/ --resolve env-app.global.com:443:52.31.102.2 | jq .
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-5dcbcfcff-hwwtg",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "ip-10-0-228-156.eu-west-1.compute.
internal"
}

Para que nuestra aplicación esté en alta disponibilidad en ambos clusters, no hace falta utilizar OCM o ACM, estas herramientas solo nos simplifican la gestión multi-cluster de la aplicación. En este ejemplo son solamente 2 clusters en 2 clouds, pero con estas herramientas podría escalarse el despliegue a decenas de clusters en varios clouds.

Lo que realmente nos da la alta disponibilidad de nuestra aplicación es usar un balanceador global de carga como veremos en las siguientes secciones.

Crear GLB

Ahora necesitamos crear el Global Load Balancer. Como comentamos al principio, lo desplegaremos en el cluster de MAD/ACM usando un deployment de HAproxy:

$ oc new-project glb-haproxy
Now using project "glb-haproxy" 

$ oc -n glb-haproxy apply -f configmap-haproxy.yaml
configmap/haproxy-config created 

$ oc -n glb-haproxy apply -f deployment-haproxy.yaml
deployment.apps/haproxy created

El manifiesto del deployment usa la imagen de docker hub de “haproxy:latest”

# deployment-haproxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: haproxy
spec:
  selector:
    matchLabels:
      app: haproxy
  replicas: 1
  template:
    metadata:
      labels:
        app: haproxy
    spec:
      containers:
      - name: haproxy
        image: haproxy:latest
        resources:
          requests:
            memory: "500Mi"
            cpu: "300m"
          limits:
            memory: "900Mi"
            cpu: "800m"
        ports:
        - containerPort: 8080
        - containerPort: 443
        volumeMounts:
        - name: haproxy-config
          mountPath: /usr/local/etc/haproxy/haproxy.cfg
          subPath: haproxy.cfg
      volumes:
      - name: haproxy-config
        configMap:
          name: haproxy-config

Y mapea el siguiente configmap que tiene como backends las IPS de nuestros clusters y hace un http check al path “/” enviando el header (hdr) host env-app.global.com:

# configmap-haproxy.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: haproxy-config
data:
  haproxy.cfg: |
    global
      log stdout format raw daemon debug

    defaults
      log global
      mode http
      option httplog
      option dontlognull
      timeout client 10s
      timeout connect 5s
      timeout server 10s
      timeout http-request 10s

    frontend apps
      bind 0.0.0.0:8080
      default_backend apps_servers

    backend apps_servers
      mode http
      option httpchk
      balance roundrobin
      http-check send meth GET  uri / hdr host env-app.global.com
      default-server check ssl verify none

      server mad 52.31.102.2:443
      server bcn 20.200.200.200:443

Pasados unos segundos el pod de haproxy debería estar corriendo y, si no hay ningún warning en los logs, quiere decir que HAproxy ha hecho los checks exitosamente contra nuestros 2 clusters:

Pod de haproxy corriendo

Con esto hemos conseguido que nuestra aplicación esté servida en alta disponibilidad en 2 clusters en 2 clouds distintos.

Acceso a la aplicación a través del GLB

Simularemos atacar externamente al GLB haciendo un port-forward al pod de HAProxy para que nuestro portátil sea capaz de atacar al pod al puerto 8080 a través de nuestra IP local 127.0.0.1 :

Ejecutado el port-forward

Una vez ejecutado el port-forward, como el comando no hace retorno, habría que abrir otra consola para lanzar cualquier test. Aunque se podría poner “&” al final del comando para evitar abrir otra consola.

[16:13:26] $ oc -n glb-haproxy port-forward pod/haproxy-796974c644-nm65x 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Handling connection for 8080

Recordemos que en un escenario real la ip 127.0.0.1 sería la IP expuesta del GLB y habría una entrada DNS (env-app.global.com) apuntando a esa IP, con lo que bastaría con atacar ese DNS curl https://env-app.global.com.

En nuestro caso podemos probar cualquiera de las siguientes peticiones para atacar al GLB, ya que son equivalentes:

$ curl -s http://127.0.0.1:8080 -H host:env-app.global.com

$ curl -s http://env-app.global.com:8080 --resolve env-app.global.com:8080:127.0.0.1

Haremos 5 peticiones a la aplicación y vemos que contestan en round-robin los pods de MAD y BCN, alojados en nodos distintos de clusters distintos de Clouds distintos.

$ for i in {1..5}; do echo "request $i" ; curl -s http://env-app.global.com:8080 --resolve env-app.global.com:8080:127.0.0.1 | jq . ; done

request 1
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-57447bb4cc-nf82k",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "ip-10-0-228-156.eu-west-1.compute.internal"
}
request 2
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 3
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-57447bb4cc-nf82k",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "ip-10-0-228-156.eu-west-1.compute.internal"
}
request 4
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 5
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-57447bb4cc-nf82k",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "ip-10-0-228-156.eu-west-1.compute.internal"
}

Failover

Ahora que la app funciona, vamos a simular la pérdida de un datacenter cambiando la configuración de ACM que hace que la app se despliegue en todos los clusters disponibles a solo en BCN, haciendo uso de las PlacementRules y las labels (location=bcn).

A través de la interfaz podemos ver los cambios a nivel gráfico y de código:

Cambios a nivel de gráfico y código.

Una vez se ha borrado el pod de la app de MAD, comprobamos en el balanceador que se ha caído el server “apps_servers/mad” y, por tanto, se ha retirado del balanceo. Podemos comprobarlo en los logs del pod de HAproxy:

Los logs del pod de HAproxy

Confirmamos que efectivamente se ha quitado del balanceo el backend de MAD/OpenShift con las siguientes peticiones en las que vemos que siempre contesta la instancia de BCN/AKS:

$ for i in {1..5}; do echo "request $i" ; curl -s http://env-app.globa
l.com:8080 --resolve env-app.global.com:8080:127.0.0.1 | jq . ; done
request 1
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 2
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 3
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 4
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}
request 5
{
"APP_NAME": "ENV-APP",
"POD_NAME": "env-api-host-688f4db58-x8rf7",
"POD_NAMESPACE": "env-app",
"POD_NODE_NAME": "aks-agentpool-23307445-vmss00000v"
}

Con esto nuestra aplicación ha soportado la caída de un cluster de Kubernetes y/o de un cloud por completo.

Hay que tener en cuenta que esta aplicación es stateless (no tiene información persistente), por lo cual deberíamos considerar en un escenario productivo la replicación/sincronización de datos entre clusters/clouds.

Conclusión

Haciendo GSLB entre varios clusters de Kubernetes podemos tener nuestras aplicaciones en alta disponibilidad geográfica, por cluster, y si estos clusters están en distintos clouds, por clouds. ¡Esperamos que os sirva ayuda!

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.