¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Daniel Guala 07/09/2023 Cargando comentarios…
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.
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 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:
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).
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.
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.
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.
En esta demo usaremos 2 clusters de Kubernetes:
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.
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”.
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”:
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:
Deberemos ejecutar ese comando sobre una consola con acceso de cluster-admin en el 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:
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.
Se pueden crear aplicaciones desde varios tipos de repositorios: Git repositories, Helm repositories y Object storage:
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.
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:
Una vez se crea la aplicación la veremos en el panel de 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.
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.
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:
Con esto hemos conseguido que nuestra aplicación esté servida en alta disponibilidad en 2 clusters en 2 clouds distintos.
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 :
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"
}
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:
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:
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.
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!
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.
Cuéntanos qué te parece.