Going native with Graal on AWS Lambda

In this guide, we'll run you through the steps required to get an http4k application deployed and running on AWS Lambda with GraalVM and available to call over the internet using AWS ApiGateway. If you're not familiar with the http4k concepts for HTTP and Serverless apps, then we advise you read them here and here. To make an app you can follow the Your first http4k app tutorial. Then follow the steps in the Serverless http4k with AWS Lambda tutorial before tackling this guide.

We'll take an existing http4k application built with Gradle and deployed with Pulumi, add the bits that are important to GraalVM Serverless HTTP apps, then compile it natively and deploy it to AWS Lambda and API Gateway using Pulumi. The resulting Lambda has super-quick startup time and low memory footprint.

Pre-requisites:


Step 1

We need to add the http4k AWS Lambda Serverless Runtime module to our project. Install it into your build.gradle file with:

implementation("org.http4k:http4k-serverless-lambda-runtime:${http4kVersion}")

This custom runtime is a lightweight, zero-reflection module which allows you to deploy both Java and GraalVM based binaries to AWS.

Step 2

Lambdas working from a native binary have to supply their own main function to launch the runtime, instead of implementing the standard Request/StreamHandler interfaces. To use it on our app, we simply create a launcher and wrap our http4k HttpHandler with the appropriate FnHandler class before starting the Runtime. Put this into a new HelloServerlessHttp4k.kt (different package to before:

package guide.tutorials.going_native_with_graal_on_aws_lambda

import org.http4k.core.Method.GET
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.routing.bind
import org.http4k.routing.path
import org.http4k.routing.routes
import org.http4k.serverless.ApiGatewayV2FnLoader
import org.http4k.serverless.AwsLambdaRuntime
import org.http4k.serverless.asServer

val http4kApp = routes(
    "/echo/{message:.*}" bind GET to {
        Response(OK).body(
            it.path("message") ?: "(nothing to echo, use /echo/<message>)"
        )
    },
    "/" bind GET to { Response(OK).body("ok") }
)

fun main() {
    ApiGatewayV2FnLoader(http4kApp).asServer(AwsLambdaRuntime()).start()
}

Update the Pulumi config to point to the new file:

Step 3

Compile the Lambda code into a GraalVM file is a 2 stage process. First, install and configure the ShadowJar plugin into build.gradle to merge the entire application into a single JAR file with a known main class. Update/add the following sections:

buildscript {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
    dependencies {
        classpath("com.github.johnrengelman:shadow:8.1.1")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
    }
}

apply(plugin = "java")
apply(plugin = "com.github.johnrengelman.shadow")

mainClassName = "guide.tutorials.going_native_with_graal_on_aws_lambda.HelloServerlessHttp4kKt"

shadowJar {
    manifest.attributes["Main-Class"] = mainClassName
    archiveBaseName.set(project.name)
    archiveClassifier.set(null)
    archiveVersion.set(null)
    mergeServiceFiles()
}

Run the new task with:

./gradlew shadowJar

... and then take a note of the JAR file that appears in build/libs.

Step 4

Now that we have our JAR file, we need to create a GraalVM image and package it into a ZIP file which can be uploaded to AWS. http4k supplies a convenience Docker image that uses the native-image program to create the binary and then packages the ZIP file:

docker run -v $(pwd):/source  --platform=linux/amd64 \
    http4k/amazonlinux-java-graal-community-lambda-runtime \
    build/libs/HelloWorld.jar \
    HelloHttp4k.zip

GraalVM will churn away for a few minutes and all being well, the HelloHttp4k.zip file will be generated in the main directory.

graalvm output

Step 5

We need to update our Pulumi configuration to upload the new binary. This is pretty simple and just involves changing the runtime, ZIP target and handler in our index.ts. We can also remove the timeout as the native binary will startup in milliseconds:

const lambdaFunction = new aws.lambda.Function("hello-http4k", {
    code: new pulumi.asset.FileArchive("HelloHttp4k.zip"),
    handler: "unused",
    role: defaultRole.arn,
    runtime: "provided.al2"
});

Step 6

Deploy your ZIP file to AWS with:

pulumi up --stack dev --yes

Pulumi will churn for a bit and all being well will display the URL at the end of the process.

pulumi output

Step 7

You can now call your deployed lambda by visiting: https://{publishedUrl}/echo/helloHttp4k. You should see helloHttp4k in the response body. Notice that the response time is super-super quick, especially after the lambda is warm. If we invoke it from the console, you should see something similar:

pulumi output

Step 8

To avoid any unwanted AWS charges, don't forget to delete all of the resources in your stack when you've finished by running:

pulumi destroy --stack dev --yes

Congratulations!

You have successfully compiled an http4k application with GraalVM, then deployed and invoked it as a Lambda in AWS!