domingo, 15 de julio de 2018

Redis - Externalización de sesiones

Redis es muchas cosas..., un almacén de datos estructurados en memoria, un gestor de sesiones extenalizado, una forma de compartir datos entre aplicaciones, ....

Nos centramos en esta entrada en la función de gestión de sesiones.

En el ecosistema de microservicios cloud, el tema de sesiones puede ser un problema para la transformación de aplicaciones monolíticas en microservicios. Habitualmente todas las aplicaciones monolíticas, de una u otra forma, persisten datos de manera que se permita una navegación fluida para un usuario dado.

Por ejemplo, todas las aplicaciones protegidas por login, han de proveer una gestión de datos que garantice una navegación con "estado" mediante un protocolo, como http, sin estado. Al menos han de guardar el usuario logado (al margen de algunos datos asociados al mismo), asignar una sesión http con su caducidad, etc..., de forma que pueda navegar por aquellos apartados permitidos en la web protegida.

Cuando pasamos a microservicios cloud, donde las buenas prácticas prohíben usar sesiones de servidor, qué soluciones podemos utilizar. Bien, una de ellas es Redis.

Utilizaremos para esta prueba

  • Un contenedor docker Redis
  • Un microservicio Spring boot
  • Entorno Kubeadm de prueba.

Todo el código lo podés encontrar en el gitlab


Pod
Usaremos una image docker proporcionada por Bitnami
El contenido del redis.yaml (deploy y service) es este:

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: redis
    spec:
      replicas: 1
      template:
        metadata:
          labels:
            app: redis
            role: master
            tier: backend
        spec:
          containers:
            - name: redis-master
              image: bitnami/redis:latest
              ports:
                - containerPort: 6379
              env:
                - name: ALLOW_EMPTY_PASSWORD
                  value: "yes"
    ---
    kind: Service
    apiVersion: v1
    metadata:
      name: redis
      labels:
        name: redis
    spec:
      type: NodePort
      ports:
      - port: 6379
        protocol: TCP
        targetPort: 6379
      selector:
        app: redis


Podríamos configurar varias réplicas, pero para nuestra prueba nos basta con una, la del pod master.
Como vemos, el puerto expuesto es el 6379. Es a este puerto al que haremos apuntar nuestra app.

Para levantar el pod lanzaremos el comando
    kubectl apply -f redis.yaml

Tras unos instantes ya podemos ver el pod levantado
    kubectl get pods



Revisamos el arranque
    kubectl logs -f redis-587d9bd764-hzbzj


Podemos acceder al pod para ver la información que iremos generando
    kubectl exec -it redis-587d9bd764-hzbzj bash

Una vez dentro lanzamos el cliente redis
    redis-cli monitor

    nos devuelve un OK y queda a la espera de accesos



Spring boot
Construyamos un microservicio básico de prueba con Spring boot

pom.xml

Como dependencias específicas redis debemos añadir

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>

Incorporamos también swagger para facilitar las pruebas

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>


Java

Añadimos una clase de configuración con el siguiente contenido


    @Configuration
    @EnableRedisHttpSession
    public class RedisConfig {

        private @Value("${spring.redis.host}") String redisHost;
        private @Value("${spring.redis.port}") int redisPort;

        @Bean
        public LettuceConnectionFactory connectionFactory() {
           return new LettuceConnectionFactory(redisHost, redisPort);
        }
    }

Para usar la session lo hacemos de la forma habitual

    HttpSession misession= request.getSession(true);
    misession.setAttribute("variable", "valor");
    etc...

El endpoint que explicamos en la prueba de más abajo tiene este código

    @GetMapping("/fecha")
     public @ResponseBody String date(HttpServletRequest request) {
log.info("Acceso al metodo rest 'fecha'");
return "Hola desde el RestController en fecha <br><h2>" 
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()) 
            " !!!<h2>" + "<hr/>" + "<h3>app: '" + appName + "'</h3>" + "<p>host name / ip: <b>" + 
            Utils.getIp() + "</b></p>" + "<p>Session: <b>" 
            request.getSession().getId() + "</b></p>" + "<hr/><hr/>";
    }

Sólo queremos que muestre el server (pod en el caso de kubernetes) desde el que se ejecuta, la hora y la session http.


application.yml

Las propiedades específicas son estas (application.yml)

    spring:
      application:
        name: redis-session
      profiles:
        active: pro
      session:
        store-type: redis
      redis:
        host: redis
        port: 6379 

En host hemos puesto el nombre "redis" que se corresponde con el servicio creado según el fichero "redis.yaml" descrito más arriba.


Dockerfile

Este es su contenido:

    FROM java:openjdk-8-jdk-alpine

    # add el jar con el nombre del "finalName" del pom.xml
    ADD redis-session.jar /app.jar

    # se modifica la fecha a la actual
    RUN sh -c 'touch /app.jar'

    # comando a ejecutar
    CMD ["java", "-jar", "/app.jar"]

lo necesitamos para poder construir la imagen a desplegar en kubeadm.
Debemos hacer un "mvn clean install" para generar el artefacto "redis-session.jar".

copiamos el jar a nuestro ubuntu y lanzamos el siguiente comando
    sudo docker build -t "redis-session:1.0.0" .

 
desde el directorio que contiene el Dockerfile y el redis-session.jar (el nombre de la imagen siempre ha de ser en minúsculas).
Validamos con "sudo docker images" (en extras veremos como eliminar el molesto sudo)



Deployment y service

Creamos, a partir de la imagen nuestro deploy y service. Para ello necesitamos el siguiente yaml (redis-session.yaml):

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: redis-session
      labels:
        name: redis-session
    spec:
        replicas: 3
        template:
          metadata:
            labels:
              app: redis-session
              tier: backend
          spec:
            containers:
              - name: redis-session
                image: redis-session:1.0.0
                ports:
                  - containerPort: 8080
                readinessProbe:
                  httpGet:
                    path: /redis-session/readiness
                    port: 8080
                  initialDelaySeconds: 60
                  timeoutSeconds: 5  
                livenessProbe:
                  httpGet:
                    path: /redis-session/liveness
                    port: 8080
                  initialDelaySeconds: 60
                  timeoutSeconds: 5
            
    ---
    kind: Service
    apiVersion: v1
    metadata:
      name: redis-session
      labels:
        name: redis-session
        tier: backend
    spec:
      type: LoadBalancer
      ports:
      - port: 8080
        protocol: TCP
      selector:
        app: redis-session

        tier: backend

Aplicamos:
    kubectl apply -f redis-session.yaml

Importante, usamos tres réplicas, que nos generarán tres pods que darán servicio a las peticiones que reciba nuestra aplicación.

Podemos revisarlo con "kubectl get pods"



También hemos añadido dos "probes", el readinessProbe (indica a kubernetes cuando un pod está preparado para recibir peticiones) y livenessProbe (indica a Kubernetes si un pod está activo). Se corresponden con dos endpoints ubicados en el "MonitorController.java".



Pruebas
Para acceder desde fuera del ubuntu lanzamos el siguiente comando en nuestro ubuntu:
    sudo kubectl port-forward svc/redis-session 9090:8080

y mapeo desde VirtualBox de Windows:

Ahora, desde un navegador de nuestro windows tenemos varios endpoints arrancados para probar:
el de swagger: http://localhost:9090/redis-session/swagger-ui.html

endpoint "fecha" que nos permite ver el juego de sesiones
    http://localhost:9090/redis-session/fecha

Si nos centramos en este último endpoint y probamos obtenemos el siguiente resultado:


Si ahora paramos el pod que nos ha respondido (vemos su id) con
    kubectl delete pod id

Se levantará de forma automática uno nuevo en su sustitución. Si ahora refrescamos en nuestro navegador (el mismo que hemos usado antes) veremos el cambio de pod que responde, pero seguiremos viendo la misma session id:


Es decir, mientras no cerremos navegador, aunque se paren todos los pods de nuestro microservicio y se levanten otros en su sustitución, la session permanecerá inalterable, ya que su gestión, se está derivando hacia el pod de redis. Si introducimos datos en session, estos los seguiremos encontrando ahí. Está todo persistido en el pod de redis.

Si me permitís una imagen gráfica (con sólo dos pods spring boot) esto es lo que está pasando:



Podemos jugar ahora con swagger con los endpoints "addDataSession" y "getDataSession" del "main-controller", parando pods, etc... Cada vez que hagamos un delete del pod deberemos relanzar el "sudo kubectl port-forward..." descrito más arriba.

También lo podemos hacer vía curl de la siguiente forma.Para insertar datos en session (hemos de grabar la session en fichero):
    curl -X PUT -c sessionfile http://localhost:9090/redis-session/addDataSession?name=Antonio
    Session grabada: 760d26e8-3ab1-4103-bada-8fe8d999a1d0

Ahora recuperamos el dato enviando la session grabada en el sessionfile:
    curl -X GET -b sessionfile http://localhost:9090/redis-session/getDataSession
    respuesta: ["Antonio"]

De nuevo añadimos datos, esta vez el comando es:
curl -X PUT -b sessionfile -c sessionfile http://localhost:9090/redis-session/addDataSession?name=es
Session grabada: 760d26e8-3ab1-4103-bada-8fe8d999a1d0

curl -X PUT -b sessionfile -c sessionfile http://localhost:9090/redis-session/addDataSession?name=panadero
Session grabada: 760d26e8-3ab1-4103-bada-8fe8d999a1d0

Como vemos, siempre se nos devuelve el mismo valor de session (760d26e8-3ab1-4103-bada-8fe8d999a1d0). No nos importa que pod pueda estar respondiendo.

Finalmente recuperamos todo:
    curl -X GET -b sessionfile http://localhost:9090/redis-session/getDataSession
    ["Antonio","es","panadero"]

Eliminamos los tres pods "kubectl delete pods id1 id2 id3". Tras unos segundos se reinician y volvemos a levantar el port-forward y lanzamos de nuevo la consulta, obteniendo el mismo resultado
    curl -X GET -b sessionfile http://localhost:9090/redis-session/getDataSession
    ["Antonio","es","panadero"]

Todas estas peticiones dejan logs en el pod de redis, que mediante su acceso "kubectl exec -it id_pod bash" y posterior lanzamiento del cliente redis "redis-cli monitor" vemos:



Recordad que podemos tracear el pod mediante "kubectl logs -f id". Ahí podremos ver el pod que recibe la petición, etc...

Además de lo dicho, también veremos en los logs de cada pod, cada 30 segundos, la salida que se generan con la monitorización que hemos introducido (liveness y readiness).



Extra
Si queremos usar redis en kubeadm desde nuestro entorno de desarrollo local deberemos realizar estas acciones previas

La primera, lanzar el siguiente comando
    sudo kubectl port-forward svc/redis 6379:6379
 
    es decir, damos acceso al puerto expuesto por el servicio (svc/redis), el 6379, desde fuera

Ahora, en nuestro VirtualBox (donde corre el ubuntu con kubeadm) configuramos el mapeo de puertos:


Por otro lado, si no queremos usar siempre "sudo" para la ejecución de cualquier comando "docker XXX" podemos incluir nuestro usuario ubuntu en el grupo "docker" de esta forma
    sudo usermod -aG docker $(whoami)

Deberemos cerrar sesión y abrir una nueva para que sea efectiva la nueva configuración. De esta forma, ahora para usar comandos del tipo "docker images", "doker ps", etc... no necesitamos añadir "sudo" delante.


Más info
Por supuesto, todo el código está disponible en
    https://gitlab.com/gincol-blog/redis-session.git

Como siempre, la información principal está en las páginas oficiales
  Spring:  https://projects.spring.io/spring-data-redis/
  Redis:   https://redis.io/
               https://redis.io/commands/monitor
               https://docs.bitnami.com/virtual-machine/components/redis/
               https://docs.bitnami.com/kubernetes/get-started-kubernetes/

Otras funciones Redis:
    https://spring.io/guides/gs/spring-data-reactive-redis/
    https://spring.io/guides/gs/messaging-redis/

No hay comentarios:

Publicar un comentario