Enmilocalfunciona

Thoughts, stories and ideas.

Serverless con Quarkus: Knative - Aplicaciones Serverless en K8s

Publicado por Omar N. Muñoz Mejía el

QuarkusServerlessAWS LambdaCloudJavaArquitectura de Soluciones

Introducción

En un anterior artículo estuvimos analizando las características y ventajas que aporta Quarkus como framework de desarrollo para generar aplicaciones Java, funciones Lambda en AWS para el caso de estudio anterior, como solución a los ya conocidos problemas de los frameworks actuales de generación de aplicaciones en este lenguaje a la hora de construir artefactos que puedan ser integrados en una solución Cloud Serverless.


En esta ocasión seguiremos analizando Quarkus, esta vez veremos cómo podemos construir y desplegar aplicaciones construidas con este framework pero ahora en una solución Serverless un poco menos conocida que AWS, en este caso hablamos de Knative, la solución para dotar a Kubernetes de los componentes esenciales para construir y ejecutar aplicaciones Serverless.

Knative: Solución K8s Serverless

Solo para para tener una conceptualización, Knative otorga a Kubernetes o a cualquiera de las distribuciones basadas en él, como puede ser OpenShift, Rancher, EKS, PKS, Minikube entre otras; la posibilidad de desplegar aplicaciones sobre su cluster y que sean ejecutadas cuando se necesiten, posibilitando al desarrollador a centrarse en el código de su aplicación y sin preocuparse de configurar una infraestructura para ello. Knative se encargará de escalar, incluso a cero, las instancias (pods) necesarias para atender las peticiones sobre el servicio.
Knative está basado en 3 componentes Build, Eventing y Serving; y para nuestras pruebas con Quarkus y Knative utilizaremos este último, Serving, ya que nos proporciona todo lo necesario para desplegar nuestra aplicación Quarkus en contenedores en un contexto Serverless, los cuales escalarán dependiendo de la carga y Knative se encargará además de configurar todos los elementos necesarios para el enrutamiento y configuración de red de nuestros servicios.

Aplicación Quarkus [Caso Práctico]

Para el caso práctico seguiremos utilizando los 2 tipos de ejecutables que ofrece Quarkus; código binario optimizado para ejecución en una JVM y código binario nativo. Con respecto a la aplicación ejemplo, en esta ocasión construiremos una aplicación en la que expondremos al menos un servicio REST para poder realizar las pruebas de arranque y acceso al servicio.

Prerrequisitos

Opcionalmente se puede utilizar Mandrel o GraalVM para la construcción de imágenes nativas.

Generación del proyecto base

Para la generación de proyecto Quarkus ofrece varias opciones al desarrollador, a través de Maven (utilizando el arquetipo Maven de Quarkus), otra mediante su página web de Quarkus Code y una tercera opción llamada Quarkus CLI. Sin embargo, para la creación de la aplicación de nuestro proyecto base vamos a utilizar Quarkus CLI, la cual nos permite crear aplicaciones Quarkus a través de líneas de comando.

Generación Quarkus CLI

Se inicia la construcción del proyecto Maven:

$ quarkus create app com.at:quarkus-knative:1.0.0-SNAPSHOT --extension=quarkus-resteasy-jackson,minikube,jib,smallrye-health

Del anterior comando podemos resaltar:

  • <create app>: Crea una nueva aplicación (proyecto) Quarkus.
  • <groupId:artifactId:version>: com.at:quarkus-knative:1.0.0-SNAPSHOT
  • <extension>: Lista de extensiones a agregar al proyecto.

Extensiones

En la creación del proyecto Maven base se configuran una lista de extensiones básicas que dotan al proyecto de dependencias para lograr construir funcionalidades esenciales en el microservicio a construir:

  • quarkus-resteasy-jackson: Agrega soporte de endpoints REST implementando JAX-RS y serialización Jackson.
  • Minikube:  Generación automática de recursos minikube (k8s) como resultado del proceso de construcción del artefacto.
  • Jib: Construcción de imágenes Docker.
  • smallrye-health: Permite informar acerca del estado de aplicación a elementos observadores externos como K8s o cualquier entorno Cloud.

Como resultado, se agregan las dependencias Maven en el POM de proyecto relacionadas con la lista de extensiones configuradas en la creación del proyecto:

<dependencies>  
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-minikube</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-container-image-jib</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
  </dependency>
.
.
.
<dependencies>  

Estructura

Las anteriores acciones generan el proyecto Maven con los parámetros seleccionados y preparado para ser importado en cualquier IDE que soporte este tipo de proyecto.

Configuración e Implementación Microservicio

REST Endpoints

Basándonos en las dependencias de RestEasy del proyecto generamos la implementación para exposición de un endpoint HTTP que nos ayude a ejecutar nuestra lógica y responder a una petición:

@Path("/v1")
public class PersonLambda {  
    @Inject
    PersonService personService;

    @ConfigProperty(name = "greeting.prefix")
    String prefixGreeting;

    @POST
    @Path("/serverless")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public PersonResponse handleRequest(Person input) {
        return new PersonResponse(prefixGreeting + " " +  personService.getName(input));
    }
}

El componente PersonService solo es una clase que retorna ya sea el nombre de usuario enviado en el parámetro de entrada Person o un nombre aleatorio de una lista almacenada en las propiedades de la aplicación:

#persons
person.persons[0].name=Vegeta  
person.persons[1].name=Kakaroto  
person.persons[2].name=Roshi  

Health Endpoints

Quarkus CLI genera un proyecto Maven con accesos a los endpoints de información de estado del servicio configurado con valores por defecto, por lo que haremos una sobre escritura del endpoint de health para definir un nuevo prefijo propio del microservicio. Se modifica el archivo application.properties de la carpeta src/resources de la aplicación para sobre escribir el root-path por defecto (/q/health):

quarkus.smallrye-health.root-path=/health  

Con este cambio, nuestra aplicación expone 3 nuevos endpoints sobre el path configurado:

  • /health/live – Indica si la aplicación está lista y funcionando.
  • /health/ready - Indica si la aplicación se encuentra disponible para recibir peticiones.
  • /health/started - Indica si la aplicación ha iniciado.

Empaquetado y Despliegue

Creación Artefacto Optimizado JVM

Para la creación solo es utilizar Maven o Quarkus CLI para construir el empaquete del artefacto, lanzando la instrucción correspondiente sobre la ruta raíz del proyecto:

Maven

./mvnw clean package

Quarkus CLI

quarkus build  

Creación Artefacto Nativo

Maven

./mvnw clean package -Dnative
En caso de tener el entorno de desarrollo diferente a Linux, la recomendación es utilizar Docker como herramienta para que la extensión de Quarkus construya el artefacto nativo:
./mvnw package -Dnative -Dquarkus.native.container-build=true

Quarkus CLI

quarkus build --native  
En caso de tener el entorno de desarrollo diferente a Linux, la recomendación es utilizar Docker como herramienta para que la extensión de Quarkus construya el artefacto nativo:
quarkus build --native -Dquarkus.native.container-build=true  

Resultado Compilación

Esta acción genera una serie de artefactos o carpetas dentro de la carpeta target del proyecto:

De los cuales destacamos los siguientes:

  • <<artefactId>> + <<version>> +”.jar”: Corresponde al empaquetado final del artefacto Quarkus construido.
  • kubernetes: Archivos propios de K8s o Minikube que contiene la configuración necesaria para construir los Kubernetes/Minukube Deployments y Services de la aplicación.

Contendio minikube.yml

---
apiVersion: v1  
kind: Service  
metadata:  
  annotations:
    app.quarkus.io/commit-id: 718663f7823ed12dc9067eb371cf336e95c45c2b
    app.quarkus.io/build-timestamp: 2022-05-03 - 11:21:23 +0000
  labels:
    app.kubernetes.io/name: knative-quarkus
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
  name: knative-quarkus
spec:  
  ports:
    - name: http
      nodePort: 30984
      port: 80
      targetPort: 8080
  selector:
    app.kubernetes.io/name: knative-quarkus
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
  type: NodePort
---
apiVersion: apps/v1  
kind: Deployment  
metadata:  
  annotations:
    app.quarkus.io/commit-id: 718663f7823ed12dc9067eb371cf336e95c45c2b
    app.quarkus.io/build-timestamp: 2022-05-03 - 11:21:23 +0000
  labels:
    app.kubernetes.io/name: knative-quarkus
    app.kubernetes.io/version: 1.0.0-SNAPSHOT
  name: knative-quarkus
spec:  
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: knative-quarkus
      app.kubernetes.io/version: 1.0.0-SNAPSHOT
  template:
    metadata:
      annotations:
        app.quarkus.io/commit-id: 718663f7823ed12dc9067eb371cf336e95c45c2b
        app.quarkus.io/build-timestamp: 2022-05-03 - 11:21:23 +0000
      labels:
        app.kubernetes.io/name: knative-quarkus
        app.kubernetes.io/version: 1.0.0-SNAPSHOT
    spec:
      containers:
        - env:
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          image: onoriel/knative-quarkus:1.0.0-SNAPSHOT
          imagePullPolicy: IfNotPresent
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /health/live
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 0
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 10
          name: knative-quarkus
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /health/ready
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 0
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 10

Despliegue Artefacto

En este punto, el artefacto ya ha sido construido y para el proceso de despliegue el tipo de artefacto ya bien sea nativo u optimizado para JVM, es indiferente ya que el proceso que a continuación de define es el mismo para ambos.

Construcción Imagen

Para el proceso de despliegue en Knative necesitaremos construir una imagen del servicio, la cual estará subida sobre un Docker registry público. La imagen se construirá con los archivos de configuración generados en la construcción del artefacto:

Imagen JVM

docker build -t onoriel/quarkus -f src/main/docker/Dockerfile.jvm .  

Imagen Nativa

docker build -t onoriel/quarkus-native -f src/main/docker/Dockerfile.native .  

Resultado

Upload image

Una vez creada la imagen es necesario subirlo a un repositorio de imagen público, esto solo es necesario para nuestras pruebas locales y para agilidad en nuestro caso de prueba, por lo que utilizamos en este caso Dockerhub para realizar el proceso de subida y que Knative pueda hacer pull de la imagen en el momento de crear el servicio:

docker login -u <<username>>  

Con esto el CMD pedirá las credenciales de acceso para el usuario. Con el usuario ya logado podremos realizar proceso de pull y push de imágenes en repositorios del usuario.

Una vez logado, procedemos a hacer push de la imagen de nuestro microservicio al repositorio del usuario:

Resultado hub.docker.com

Creación Knative Service

Dentro de la raíz del proyecto creamos una carpeta llamada k8s, puede ser nombrada como queramos, y dentro de ella creamos un nuevo archivo YAML con la definición de servicio Knative:

apiVersion: serving.knative.dev/v1  
kind: Service  
metadata:  
  name: knative-quarkus-app
spec:  
  template:
    spec:
      containers:
      - image: onoriel/quarkus-native
        ports:
          - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health/live
        readinessProbe:
          httpGet:
            path: /health/ready

O podemos crear el servicio utilizando directamente Knative CLI:

kn service create <<service_name>> --image <<image_name>> --port <<app_port>>  
$ kn service create knative-quarkus-app --image onoriel/quarkus-native --port 8080
Creating service 'knative-quarkus-app' in namespace 'default':

  0.045s The Route is still working to reflect the latest desired specification.
  0.048s ...
  0.087s Configuration "knative-quarkus-app" is waiting for a Revision to become ready.
 14.883s ...
 15.000s Ingress has not yet been reconciled.
 15.203s Waiting for load balancer to be ready
 15.226s Ready to serve.

Service 'knative-quarkus-app' created to latest revision 'knative-quarkus-app-00001' is available at URL:  
http://knative-quarkus-app.default.127.0.0.1.sslip.io

Probando la Aplicación Serveless

Una vez desplegada llegamos a lo más interesante, probar y ver el comportamiento del cluster K8s, la gestión de pods para servir el servicio cuando este sea solicitado y la liberación posterior de recursos.
Una vez ha terminado de desplegar el servicio podemos acceder a probar en la URL generada por Knative, en nuestro caso http://knative-quarkus-app.default.127.0.0.1.sslip.io

curl --location --request POST 'http://knative-quarkus-app.default.127.0.0.1.sslip.io/v1/serverless' \  
--header 'Content-Type: application/json' \
--data-raw '{
 "name": "John"
}'

Petición POST al servicio utilizando Postman:

Como podemos observar nuestro servicio se encuentra respondiendo a nuestras peticiones sin problemas. Ahora veamos que sucedes con los pods y la infraestructura de nuestro cluster cuando estas peticiones son realizadas.

Pod activo en el cluster

Actividad de los pods asociados al servicio:

kubectl get pod -l serving.knative.dev/service=<<app_service_name>> -w  

Se aprecia la creación y eliminación de pods automáticamente en el cluster, luego de un tiempo nuestros pods empiezan a terminar su ejecución y posterior eliminación; y en cuanto nuestro servicio es solicitado nuevamente Knative se encarga de desplegar y servir otro pod de la aplicación de forma automática.

Terminación Pod Automática

Logs Servicio

Una vez teniendo la aplicación arriba y disponible como un pod de Knative podemos acceder a los logs a través de los comandos propios de K8s, en este caso con su versión de Minikube:

kubectl logs <<pod_name>> -c user-container  

Conclusiones

En este artículo hemos visto como es el proceso end to end para construir aplicaciones Java utilizando el framework Quarkus y desplegar nuestro artefacto sobre una plataforma Serverless como Kubernetes + Knative. Con esta aproximación contamos con una nueva alternativa para plantear soluciones Serverless utilizando artefactos construidos con Quarkus y aprovechar sus caracteristicas de aligeramiento de aplicaciones y construcción de artefactos nativos en código máquina para obtener el máximo rendimiento y mejores respuestas en este tipo de arquitecturas cloud con nuestras aplicaciones Java.

¡Síguenos en Twitter para estar al día de próximas entregas!

Autor

Omar N. Muñoz Mejía

Arquitecto de soluciones en knowmad mood. Realmente me apasiona aportar soluciones simples a problemas complejos y saber el gran impacto que mi trabajo puede generar en muchas personas. #techlover.