GraalVM + Java 11 + AWS Lambda + PostgreSQL + DynamoDB + Docker

En esta ocasión mostraré la travesía que fue conseguir compilar un jar con GraalVM y que luego corra en AWS Lambda.

¿Por qué hacerlo?
En la actualidad, el cold start de lanzar un jar en un lambda es alto, varios segundos (en node puede ser medio segundo o menos), pero luego de eso cada invocación tiene un tiempo de respuesta bueno, son solo algunos milisegundos. Entonces mi motivación es hacer cada vez el cold start menor y si mejora el tiempo de respuesta de las siguientes invocaciones es aún mejor 😉.

¿Que se necesita?
  1. Java 11
  2. Docker
  3. Maven
  4. Acceso a Lambdas en AWS

¿Cómo hacerlo?
Esta es la parte entretenida, en un principio fue compleja pero luego de entenderla fue divertido.
  1. Para poder lanzar el jar compilado hay que utilizar AWS Lambdas con un custom runtime.
  2. El custom runtime tiene un comportamiento que debe ser seguido y se describe en este link y acá. En resumen: existe una URL para obtener un evento, otra para indicar si hay un error al iniciar, también una para enviar la respuesta del evento y la última para notificar error al procesar el evento. Considerar que la aplicación debe mantenerse en ejecución esperando nuevos eventos.
  3. Antes de poder tener el jar compilado hay que compilarlo... Para eso usé GraalVM con Docker.
Con esas mínimas consideraciones comenzamos...

El siguiente código (en un proyecto usando maven) es lo mínimo necesario para cumplir: 
public class App {

    public static void main(String[] args) throws MalformedURLException, IOException {

        while (true) {
            final var url = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/next")
                            .toString());

            final var conn = url.openConnection();
            final var requestid = conn.getHeaderField("Lambda-Runtime-Aws-Request-Id");

            try (final var is = conn.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }

            ((HttpURLConnection) conn).disconnect();
            final var ok = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/")
                            .append(requestid).append("/response")
                            .toString());

            final var responsehttp = (HttpURLConnection) ok.openConnection();
            responsehttp.setRequestMethod("POST");
            responsehttp.setDoOutput(true);
            responsehttp.connect();

            try (final var os = responsehttp.getOutputStream()) {
                os.write("ok".getBytes());
                os.flush();
            }

            try (final var is = responsehttp.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }
            responsehttp.disconnect();
        }
    }
}

Explicación del código:
  • Usa el while(true) para quedar corriendo mientras espera nuevos eventos.
  • Crea la URL para acceder a los eventos.
  • Cuando recibe un evento obtiene el Lambda-Runtime-Aws-Request-Id (necesario para luego enviar la respuesta, está descrito en los links de arriba).
  • Lee desde el InputStream los datos del evento recibido y los muestra.
  • Crea la URL para enviar la respuesta.
  • Escribe ok como respuesta al evento recibido (en un POST).
  • Lee el InputStream del POST enviado (necesario para poder recibir un nuevo evento).
  • Vuelve a esperar un evento.
  • Usa AWS_LAMBDA_RUNTIME_API para obtener la ip y puerto al que se le envía la respuesta y se obtienen los eventos.
Luego de eso hay que generar el jar con mvn package y continuar con la compilación usando GraalVM y Docker. El siguiente es el Dockerfile:

FROM oracle/graalvm-ce:19.3.1-java11 as graalvm
COPY . /app
WORKDIR /app
RUN gu install native-image
RUN native-image --no-server -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:+AddAllCharsets -H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI -H:+TraceServiceLoaderFeature -H:+StackTrace  -jar target/lambda-graal-1.0-SNAPSHOT.jar

FROM frolvlad/alpine-glibc
COPY --from=graalvm /app/lambda-graal-1.0-SNAPSHOT /app/bootstrap

Lanzarlo con:
docker build -t demo .

Ahora que tenemos la imagen hay que extraer el binario generado con GraalVM y dejarlo ejecutable (el nombre tiene que ser bootstrap): 
docker run --rm demo cat /app/bootstrap > bootstrap
chmod +x bootstrap

Luego generamos el zip para subirlo a AWS Lambda:
zip function.zip bootstrap

Y eso es todo!

Para crear una aplicación nativa PERO más compleja usando ServiceLoader, DynamoDB y PostgreSQL Necesité lo siguiente:
  • Hay que tener el archivo libsunec.so y cacerts.
  • Ocupé el plugin Shade para generar un uber-jar.
  • En la dependencia de DynamoDB quité apache-client y netty-nio-client.
  • Usé url-connection-client como dependencia (por eso quité los anteriores).
  • Agregué slf4j-simple para evitar el error UnresolvedElementException con org.slf4j.impl.StaticLoggerBinder.
  • Generé el archivo scripts.sh para realizar todo el proceso.
  • Usé la variable de ambiente _HANDLER que entrega lo que se escribe en la consola de AWS :

  • Creé una interface para servicios (LambdaService) y otra para los handlers (LambdaHandler).
  • La clase que se escribe en el handler de la consola de AWS tiene que ser un LambdaHandler para que sea invocado (es el nombre de la clase, no incluye el método).
  • Luego de configurar la lambda con 128mb de memoria, 5 segundos de timeout y subir el zip, el resultado es el siguiente:

El resultado es excelente 😊 !!!

El código queda disponible en GitHub 😎 y los enlaces que me ayudaron fueron:







No hay comentarios.:

Publicar un comentario