Ingress, sticky sessions y servicios

 ·  ☕ 6 min  ·  ✍️ eiximenis

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.

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:

1
2
3
4
5
6
7
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/affinity-mode: persistent
    nginx.ingress.kubernetes.io/session-cookie-name: lbsticky
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"    

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):

1
2
3
4
5
paths:
- backend:
    serviceName: mysvc
    servicePort: http
  path: /mysite(/|$)(.*)

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:

1
2
3
4
5
http:
paths:
- backend:
    serviceName: mysvc.otherns.svc.cluster.local
    servicePort: http

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Service
metadata:
  name: mysvc-proxy
  namespace: "mismo-namespace-que-el-ingress"
spec:
  type: ExternalName
  externalName: mysvc.otherns.svc.cluster.local
  ports:
  - port: 80  

Y luego configura el ingress para que use el servicio mysvc-proxy en lugar de mysvc:

1
2
3
4
5
http:
paths:
- backend:
    serviceName: msvc-proxy
    servicePort: 80

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:

Salida de “kubectl get endpoints un-servicio donde se ve tres IPs

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:

  1. Llega una petición al controlador ingress (NGINX)
  2. El controlador ingress, a partir de la ruta y el host de la petición, mira a que servicio debe pasar la petición
  3. 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:

  1. Llega una petición al controlador ingress (NGINX)
  2. El controlador ingress, a partir de la ruta y el host de la petición, mira a que servicio debe pasar la petición
  3. De este servicio, elige el endpoint basándose en la cookie de sticky sessions (si existe, si no, elige uno y manda la cookie).
  4. Pero, como el servicio es ExternalName, y NO tiene endpoints, entonces NGINX le manda la peticion al servicio ExternalName directamente
  5. El servicio ExternalName es una mera redirección DNS al servicio real.
  6. La petición llega al servicio real (el que está en otro espacio de nombres) quien la manda a uno de sus pods.
  7. 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.

Diagrama arquitectónico con varios ingress en distintos namespaces

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!

Si quieres, puedes invitarme a un café xD

eiximenis
ESCRITO POR
eiximenis
Compulsive Developer