En Mi Local Funciona

Technical thoughts, stories and ideas

Serverless con Quarkus: AWS Lambda Functions

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

Arquitectura de SolucionesServerlessAWS LambdaCloudJavaQuarkus

Introducción

Algunos de los principales inconvenientes a la hora de construir servicios Java para integrarlos a las ya comúnmente conocidas arquitecturas Cloud Serverless son la cantidad de memoria y el tiempo que emplea la Java Virtual Machine (JVM) para iniciar y así poder tener listo nuestra aplicación para su uso.

Estos factores claramente dejan a Java en desventajas frente a otros lenguajes de programación como Node.js o Go; a la hora de construir soluciones Severless basados en un modelo FaaS (Function as a Service), ya que estos últimos lenguajes mencionados otorgan mejores números en cuanto a utilización de memoria (memory footpring), tamaño de la aplicación y tiempo de inicio de nuestra aplicación antes de poder atender peticiones (cold start), que son factores críticos a la hora de evaluar el performance y sacar el mejor provecho a nuestras herramientas y servicios Cloud.

Aquí entra en escena Quarkus, un Framework para desarrollar aplicaciones Java que promete dar solución a estos problemas de rendimiento en las que aplicaciones Java y sus frameworks de desarrollo mayormente utilizados como Spring, los cuales no están del todo optimizados para este tipo de arquitecturas. Quarkus se caracteriza principalmente por otorgarle a nuestras aplicaciones, sobre todo en modo bytecode nativo, propiedades como:

  • Aligerar los tiempos de arranque
  • Un menor consumo de memoria
  • Tamaño de aplicación más reducido

Ejecutables Quarkus

Código binario JVM optimizado

Modo en el cual el framework compila nuestro código (bytecode), crea un artefacto y es ejecutado (interpretado) a través de la JVM, pero con algunos ajustes por parte de Quarkus. En tiempo de construcción del artefacto se ajustan tareas como la construcción de entidades de base de datos, el procesado de anotaciones, el análisis y carga de configuraciones; y en tiempo de ejecución el framework se reduce la información dinámica que necesita la aplicación, consiguiendo que tareas que normalmente realizadas en fase de ejecución (runtime) de la aplicación sean ejecutadas durante la construcción (build) del artefacto, así se consigue reducir los tiempos de arranque de la aplicación, factor importante para entornos contenerizados o Serverless (short-lived).

Código binario nativo

El otro modo de Quarkus es native image, donde se da un paso más en la optimización de las aplicaciones y el resultado de la construcción del artefacto es un ejecutable (standalone executable) que no necesita una JVM para su ejecución; ya que este genera código máquina que es perfectamente legible e interpretado por el hardware en el que se ejecuta, y contiene toda las clases y dependencias (incluidas las de la JDK) que precisa la aplicación para funcionar. El soporte de Quarkus para generar este tipo de artefactos impacta notablemente en la reducción de los consumos de memoria y tiempos de arranque de aplicaciones Java en este caso.


Creación Quarkus Lambda Function [Caso Práctico]

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 2 opciones al desarrollador, uno a través de Maven (utilizando el arquetipo Maven de Quarkus) y la otra mediante su página web de Quarkus Code.

Generación Maven

Se inicia la construcción del arquetipo:

mvn archetype:generate \  
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
       -DarchetypeVersion=2.8.0.Final

Se introducen los atributos básicos para el proyecto Maven:

[INFO] Generating project in Interactive mode
[INFO] Archetype repository not defined. Using the one from [io.quarkus:quarkus-amazon-lambda-archetype:0.28.1] found in catalog remote
Define value for property 'groupId': com.at
Define value for property 'artifactId': lambda-quarkus
Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
Define value for property 'package' com.at: : com.at.lambda
Confirm properties configuration:
groupId: com.at
artifactId: lambda-quarkus
version: 1.0.0-SNAPSHOT
package: com.at.lambda
Generación Quarkus Code

Quarkus expone una herramienta a través de su página web, para ayudar al desarrollador con una interfaz gráfica más amigable la creación de los proyectos Maven con las dependencias necesarias y las propiedades del artefacto a construir:

Con la herramienta podemos filtrar por plataforma “lambda” y seleccionar la extensión “AWS Lambda”.

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.

Desarrollo De La Lambda

Para la aplicación es importante definir el punto de entrada a la lambda, y esto se logra implementando la interfaz RequestStreamHandler o RequestHandler de manera obligatoria dentro del código, de otra forma la extensión quarkus-amazon-lambda generará un error en tiempo de compilación.

@Named("personLambda")
public class PersonLambda  
       implements RequestHandler<InputObject, OutputObject> {
 … 
@Named("personLambda")
public class PersonLambda implements RequestStreamHandler {  
 …

El proyecto creado por el arquetipo vendrá ya con una implementación básica de la interfaz RequestHandler que podemos modificar de acuerdo con los requisitos particulares de la Lambda.

Para más información sobre el desarrollo con Quarkus pulse aquí.

En caso de que el código implemente más de una la interfaz RequestStreamHandler o RequestHandler la extensión permite distinguir cual de todas las implementaciones debe lanzar AWS Lambda a través de la propiedad quarkus.lambda.handler de la aplicación:

quarkus.lambda.handler= personLambda  

Obsérvese que el valor de la propiedad quarkus.lambda.handler (/src/main/resources/application.properties) tiene que coincidir con el valor asignado en la anotación “named” de la clase PersonLambda que implementa la interfaz RequestHandler:

@Named("personLambda")
public class PersonLambda implements RequestHandler<Person, String> {  
    @Inject
    PersonService personService;

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

    @Override
    public String handleRequest(Person input, Context context) {
        return prefixGreeting + " " +  personService.getName(input);
    }
}

La implementación de la interfaz RequestHandler también define los objetos de entrada y salida de la Lambda, en esta implementación se recibe un Objeto tipo Person y retorna un String como objetivo de salida.

El método handleRequest que implementa la clase principal además de definir los objetos de entrada y salida, también recibe como parámetro el contexto runtime en el que se ejecuta la Lambda tales como Aws Request Id, Function Name, Function Version, Invoked Function Arn, Memory LimitIn MB, etc.

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
Para el uso del AWS Java Runtime:

./mvnw clean package

Quarkus

quarkus build  

Creación Artefacto Optimizado JVM
Maven
Para el uso de binarios AWS Linux (Ejecutable Nativo):

./mvnw clean package -Dnative

Es importante resaltar que la construcción del artefacto en modo nativo es costosa en términos de tiempo y recursos, puede emplear varios minutos en ello, por lo que la recomendación para desarrollos es dejar el proceso de creación y despliegue de este tipo de artefactos para entornos productivos y manejar la versión JVM para desarrollos y pruebas para entornos previos incluyendo local.

En caso de tener un 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
Para el uso de binarios AWS Linux (Ejecutable Nativo):

quarkus build --native  

En caso de tener un 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 archivos dentro de la carpeta target del proyecto:

De los cuales destacamos los siguientes:

  • <<artefactId>> + <<version>> + ".jar": Corresponde al empaquetado final del artefacto Quarkus construido.
  • <<artefactId>> + <<version>>+"-runner.jar": Corresponde al empaquetado final del artefacto Quarkus versión nativa construido.
  • function.zip: Archivo de despliegue lambda
  • manage.sh: Script que encapsula las llamadas a AWS CLI
  • sam.jvm.yaml: Archivo que define propiedades para local testing usando SAM CLI
  • sam.native.yaml: Archivo que define propiedades de la versión nativa para local testing usando SAM CLI
Estructura Empaquetado Lambda

El empaquetado function.zip de la carpeta target contiene el código Lambda finalmente a desplegar en AWS y su estructura interna varía dependiendo el tipo de compilado generado:

  • JVM Optimizado
  • Binario Nativo

Creación de Role IAM

Antes de subir y desplegar nuestro código Quarkus Java Lambda, debemos antes crear o asignar un rol de ejecución a nuestro recurso en AWS para poder ejecutar este tipo de servicios. Para efectos de nuestras pruebas se otorgarán permisos de invocación de cualquier Lambda en nuestra cuenta:

Una vez se ha creado el rol y otorgado los permisos necesarios, se edita el archivo target/manage.sh para agregar esta propiedad al script antes de proceder a desplegar:

FUNCTIONNAME=LambdaQuarkus
HANDLER=io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
RUNTIME=java11
.
.
.
LAMBDAROLE_ARN="arn:aws:iam::0000000000:role/lambda-role"

Despliegue Artefacto Optimizado JVM
Para crear la función Lambda utilizando JVM, se lanza el siguiente comando estando en la raíz del proyecto:

sh target/manage.sh create  

El archivo target/manage.sh es construido cada vez que se lanza el proceso de empaquetado, por consiguiente, la información del rol del paso anterior se tiene que agregar manualmente después de este proceso para poder realizar el proceso de creación de la función nuevamente; o la otra alternativa es enviar como parámetro este rol en el proceso de creación de la función:

LAMBDA_ROLE_ARN="arn:aws:iam::0000000000:role/lambda-role" sh target/manage.sh create  

Despliegue Artefacto Nativo
Para la compilación del código java a un ejecutable nativo, se lanza el siguiente comando estando en la raíz del proyecto:

sh target/manage.sh native create  

Una vez creada la función lambda, verificar que la variable de entorno DISABLE_SIGNAL_HANDLERS ha sido creada con la ejecución del script, de lo contrario crearla manualmente:

Ejecución Función Lambda

Ejecución Remota
Para la ejecución de la función sobre el entorno Cloud, en este caso AWS, se lanza el siguiente comando:

sh target/manage.sh invoke  

El script de ejecución (manage.sh --payload file://payload.json) toma como payload de entrada el archivo payload.json localizado en la raíz del proyecto, este se puede modificar para customizar la entrada del servicio al objeto esperado.

aws lambda invoke response.txt \  
  ${inputFormat} \
  --function-name ${FUNCTION_NAME} \
  --payload file://payload.json \
  --log-type Tail \
  --query 'LogResult' \
  --output text

Output ejecución:

lambda-quarkus$ ./target/manage.sh invoke  
Invoking function  
++ aws lambda invoke response.txt --function-name LambdaQuarkusNative --payload file://payload.json --log-type Tail --query LogResult --output text
++ base64 --decode
START RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee Version: $LATEST  
2022-04-11 15:00:47,454 INFO  [io.quarkus] (main) lambda-quarkus 1.0.0-SNAPSHOT native (powered by Quarkus 2.7.5.Final) started in 0.160s.  
2022-04-11 15:00:47,472 INFO  [io.quarkus] (main) Profile prod activated.  
2022-04-11 15:00:47,472 INFO  [io.quarkus] (main) Installed features: [amazon-lambda, cdi]  
END RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee  
REPORT RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee  Duration: 5.66 ms       Billed Duration: 374 ms Memory Size: 256 MB     Max Memory Used: 53 MB  Init Duration: 367.37 ms  
"Hello Bill Roshi"

Igualmente, al crearse un log group para la Lambda en AWS con el despliegue de la misma mediante SAM, los logs de la aplicación pueden verse a través de CloudWatch:

Ejecución Local
Para la ejecución local de la función Lambda utilizamos la herramienta AWS SAM, la cual nos permite a través de líneas de comando la creación y manejo de aplicaciones Serverless de forma local:

sam local invoke --template target/sam.jvm.yaml --event payload.json  

Output ejecución:

lambda-quarkus$ sam local invoke --template target/sam.jvm.yaml  
Invoking io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest (java11)  
Decompressing /home/onoriel/aws/sam/lambda-quarkus/target/function.zip  
Skip pulling image and use local one: public.ecr.aws/sam/emulation-java11:rapid-1.43.0-x86_64.

Mounting /tmp/tmp15osbzmz as /var/task:ro,delegated inside runtime container  
START RequestId: 56247499-d992-4639-926e-50ef462be871 Version: $LATEST  
2022-04-07 13:02:17,771 INFO  [io.quarkus] (main) lambda-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.7.5.Final) started in 0.671s.  
2022-04-07 13:02:17,773 INFO  [io.quarkus] (main) Profile prod activated.  
2022-04-07 13:02:17,773 INFO  [io.quarkus] (main) Installed features: [amazon-lambda, cdi]  
"Hello Vegeta"
END RequestId: 56247499-d992-4639-926e-50ef462be871  
REPORT RequestId: 56247499-d992-4639-926e-50ef462be871  Init Duration: 0.15 ms  Duration: 833.79 ms     Billed Duration: 834 ms Memory Size: 256 MB     Max Memory Used: 256 MB  

Si la versión a ejecutar es nativa se utiliza el siguiente comando:

sam local invoke --template target/sam.native.yaml --event payload.json  
Actualización Función Lambda

Una vez hechas las modificaciones en el código, se empaqueta la aplicación y se redespliega el artefacto sobre AWS cambiando el parámetro de entrada en el script de despliegue a update:

sh target/manage.sh update  

Si existen problema para la actualización por versiones previas de la misma sobre los recursos de AWS una solución alternativa puede ser eliminando y creando nuevamente la función lambda del entorno:

sh target/manage.sh delete create  

Conclusiones

En este artículo hemos visto como a partir del framework Quarkus podemos optimizar el ciclo de vida de nuestras aplicaciones Java con el objetivo de poder sacar el máximo rendimiento de las soluciones Serverless en la nube, como puede ser AWS y sus Lambda Functions. Con Quarkus se logra cerrar un poco más la brecha de desempeño que existe frente a otros lenguajes de programación otorgándole mejores tiempos de arranque, ejecutables más ligeros y consumo de recursos reducidos.
En próximas ediciones trataremos de recrear el trabajo de este capítulo, pero en diferentes plataformas Serverless para sacar más conclusiones y obtener más datos con qué comparar.

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

Omar N. Muñoz Mejía
Autor

Omar N. Muñoz Mejía

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