El otro día estuve revisando un proyecto, desplegado en un Kubernetes (un AKS, aunque eso no es relevante en este caso). El tema es que parecía que “las sticky sessions no iban”. Por motivos del proyecto, era necesario tener sticky sessions y además estrictas, es decir, que en caso de que se escalara el número de pods los usuarios NO fuesen redirigidos a esos nuevos pods para repartir la carga.
Ya, ese tipo de proyectos escalan bastante mal, pero eso dará para otros posts. De momento centrémonos en lo que ocurría.
Problema 1: Cookie de sticky sessions mal configurada
El proyecto en cuestión usaba el controlador ingress de NGINX y este habilita el soporta para sticky sessions a través de una cookie. Revisé el recurso ingress y efectivamente todas las anotaciones necesarias estaban:
|
|
Pero una simple navegación con las developer tools del navegador activadas dejaba claro que el navegador no mandaba la cookie. Parece que la recibía, pero no la mandaba de vuelta. El error ahí es que faltaba la anotación nginx.ingress.kubernetes.io/session-cookie-path
.. Esta anotación es requerida cuando ingress usa expresiones regulares en los paths de las reglas, como en este ejemplo (y nuestro caso):
|
|
Si no ponemos la anotación nginx.ingress.kubernetes.io/session-cookie-path
, entonces NGINX manda la cookie pero con el valor de Path
idéntico a la expresión regular (p. ej. /mysite(/|$)(.*)
), por lo que el navegador no mandará de vuelta la cookie, ya que no estamos realmente en este path.
Una vez añadida a la anotación, y viendo que ahora la cookie se mandaba, ejecutamos los tests para verificar las sticky sessions… y de nuevo fallaron… Y eso nos trae a la parte interesante del post.
No trates a ingress como un recurso compartido
Sin entrar demasiado en detalles, diré que se trataba de un proyecto con varios servicios, cada uno desplegado en su espacio de nombres. Pero ingress se desplegaba como un recurso compartido, en un espacio de nombres propio. Y ahí vienen los problemas: no hay manera en ingress de enrutar hacia un servicio de otro espacio de nombres. Al menos de momento, aunque se está valorando para una futura versión de ingress.
Así intentar enrutar desde ingress a un servicio mysvc
que estuviese en un espacio de nombres otherns
NO FUNCIONA:
|
|
En general, da igual, si pones un punto en el serviceName
vas a recibir un error al desplegar el recurso ingress al cluster. Cuando la gente se encuentra con este problema, usa un truco del almendruco, que en algunos casos funciona:
Este truco consiste en declarar un servicio ExternalName
que apunte al DNS del servicio real al que quieres llamar:
|
|
Y luego configura el ingress para que use el servicio mysvc-proxy
en lugar de mysvc:
|
|
Esto funciona: ingress enruta a msvc-proxy
que a su vez enruta via DNS interno a mysvc.otherns.svc.cluster.local
que es el servicio real que redirigirá la llamada a un pod. ¡Todo perfecto!…
… Hasta que quieres usar sticky sessions. Y es que esta aproximación rompe cualquier gestión de sticky sessions que el controlador ingress (en nuestro caso NGINX) pueda hacer. La razón es que, a pesar de que nosotros en el ingress declaramos un servicio, NGINX no usa el servicio para enrutar las llamadas. NGINX se salta al servicio, y enruta las llamadas directamente a los pods.
Pregunta: Todos los controladores ingress hacen eso de saltarse el servicio y llamar a los pods directamente? No tiene por qué (la definición del estándard no dice nada al respecto), pero si el controlador ingress quiere ofrecer servicios avanzados (como es el caso de NGINX) no tiene otra opción que saltarse el servicio.
Para saber qué pods están “bajo el paraguas” del servicio se usa una API de Kubernetes, que es la endpoint API:
El comando kubectl get endpoints mysvc
devuelve los endpoints asociados al servicio mysvc
. Estos endpoints son, las IPs de los pods que están bajo este servicio. Esta API es la que usa NGINX: Para cada ingress NGINX mantiene una lista con sus endpoints (los pods subyacentes). Esta lista de endpoints, devuelta por Kubernetes, ya tiene en cuenta p. ej. que un pod puede no estar listo para recibir peticiones. Así NGINX puede enrutar directamente a uno de esos pods saltándose el servicio. Hacer esto le permite tener sus propias políticas de load balancing y implementar, entre otras, sticky sessions. Por supuesto NGINX actualiza esa lista de endpoints cuando un endpoint es creado o eliminado.
Así que lo que tenemos es un escenario en el que:
- Llega una petición al controlador ingress (NGINX)
- El controlador ingress, a partir de la ruta y el host de la petición, mira a que servicio debe pasar la petición
- De este servicio, elige uno de sus endpoints y le manda la petición directamente (sin pasar por el servicio en sí).
Es decir, solo usa el servicio para saber a qué endpoints (pods) debe mandar la petición.
¿Y qué ocurre cuando usamos el truco de usar un servicio ExternalName
para llamar a un servicio que está en otro espacio de nombres que el del recurso ingress? Pues que entonces, el servicio del cual NGINX mantiene sus endpoints, es el servicio ExternalName
(el que está en el ingress). Y los servicios ExternalName
tienen siempre un solo endpoint: el DNS al que apuntan.
Así, lo que ocurre es que, a pesar de tener las sticky sessions habilitadas en NGINX, al usar un servicio ExternalName
el escenario es el siguiente:
- Llega una petición al controlador ingress (NGINX)
- El controlador ingress, a partir de la ruta y el host de la petición, mira a que servicio debe pasar la petición
- De este servicio, elige el endpoint basándose en la cookie de sticky sessions (si existe, si no, elige uno y manda la cookie).
- Pero, como el servicio es
ExternalName
, y NO tiene endpoints, entonces NGINX le manda la peticion al servicioExternalName
directamente - El servicio
ExternalName
es una mera redirección DNS al servicio real. - La petición llega al servicio real (el que está en otro espacio de nombres) quien la manda a uno de sus pods.
- Resultado: Las sticky sessions se pierden
En resumen: tener los recursos ingress en otro espacio de nombres a los servicios a los que apuntan es una mala idea. Y eso es porque (al menos en su concepción actual), el recurso ingress NO es un recurso compartido. Forma parte de tu aplicación.
El controlador ingress SÍ es compartido, pero los recursos ingress no: ten presente que, el controlador ingress “recoge” todo los recursos ingress y los “mezcla”. Así que no tienes por qué desplegar todos los recursos ingress juntos, ni desplegarlos en un espacio de nombres común, ni desplegarlos en el espacio de nombres donde esté el controlador ingress instalado.
En función de tus necesidades puedes, por supuesto, tener más de un recurso ingress en el mismo namespace. Al final, todos ellos se combinan en el controlador ingress (aunque también es posible tener varios controladores ingress, pero esto daría para otro post). Así el usuario realiza una petición, esa es recibida por el controlador ingress, y luego en base al host y el path de la petición es enrutada directamente, al pod que pueda atender dicha petición.
Así que ya sabes: trata a ingress como un recurso de tu aplicación, que se despliegue junto a esta. ¡Te evitarás problemas!