aws-lambdagraalvmjava-21

Gradle Plugin which generate a GraalVM binary of a Java 21 AWS Lambda?


I have searched for some time for a Gradle plug-in which will:

I noticed that with Micronaut it is quite easy to do by adding the following to the build.gradle file:

plugins {
    id("io.micronaut.application") version "4.4.4"
    id("io.micronaut.aot") version "4.4.4"
    ...
}

...

graalvmNative.toolchainDetection = false

micronaut {
    runtime("lambda_provided")
    testRuntime("junit5")
    processing {
        incremental(true)
    }
    aot {
        // Please review carefully the optimizations enabled below
        // Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details
        optimizeServiceLoading = false
        convertYamlToJava = false
        precomputeOperations = true
        cacheEnvironment = true
        optimizeClassLoading = true
        deduceEnvironment = true
        optimizeNetty = true
        replaceLogbackXml = true
    }
}

tasks.named("dockerfileNative") {
    jdkVersion = "21"
    args(
            "-XX:MaximumHeapSizePercent=80",
            "-Dio.netty.allocator.numDirectArenas=0",
            "-Dio.netty.noPreferDirect=true"
    )
}

Then building a GraalVM Zip is as simple as:

./gradlew clean buildNativeLambda

This will create a file (which can be uploaded to an AWS Lambda) e.g.

However, I'm try to do this on a plain Java project i.e. with no frameworks as I want to get the code to build and to execute as fast as possible.

There seem to be some Maven plugin solutions and the SAM CLI tool only seems to support Java 17 - but no Gradle Plugins unless looking in wrong place.


Solution

  • Posting an answer here (tested on Mac) which includes a working "Hello World Lambda" example at:

    In brief, the solution was to cherry-pick files from 2 locations:

    1. https://github.com/aws/serverless-java-container/tree/main/samples/springboot3/pet-store-native - official AWS Lambda example (note this uses Maven not Gradle).
    2. https://github.com/aws/aws-sam-cli - SAM CLI tool (supports Gradle but there was issues with Docker file / Java version).

    Firstly I copied the Dockerfile from the AWS example and removed the maven section (this would never be used):

    FROM public.ecr.aws/amazonlinux/amazonlinux:2023
    
    RUN yum -y update \
        && yum install -y unzip tar gzip bzip2-devel ed gcc gcc-c++ gcc-gfortran \
        less libcurl-devel openssl openssl-devel readline-devel xz-devel \
        zlib-devel glibc-static zlib-static \
        && rm -rf /var/cache/yum
    
    # Graal VM
    ENV GRAAL_VERSION 21.0.2
    ENV ARCHITECTURE aarch64
    ENV GRAAL_FILENAME graalvm-community-jdk-${GRAAL_VERSION}_linux-${ARCHITECTURE}_bin.tar.gz
    RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAAL_VERSION}/${GRAAL_FILENAME} | tar -xvz
    RUN mv graalvm-community-openjdk-${GRAAL_VERSION}* /usr/lib/graalvm
    ENV JAVA_HOME /usr/lib/graalvm
    
    # Gradle
    ENV GRADLE_VERSION 7.4.1
    ENV GRADLE_FOLDERNAME gradle-${GRADLE_VERSION}
    ENV GRADLE_FILENAME gradle-${GRADLE_VERSION}-bin.zip
    RUN curl -LO https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip
    RUN unzip gradle-${GRADLE_VERSION}-bin.zip
    RUN mv $GRADLE_FOLDERNAME /usr/lib/gradle
    RUN ln -s /usr/lib/gradle/bin/gradle /usr/bin/gradle
    
    VOLUME /project
    WORKDIR /project
    
    WORKDIR /lambda
    

    We can now build a docker image al2023-graalvm21:native-web (which extends the Amazon amazonlinux:2023 container and installs GraalVM / Gradle tooling):

    docker build -t al2023-graalvm21:native-web .
    

    Secondly I ran the SAM CLI tool i.e. sam init and selected following options:

    The SAM CLI then generated an example and from that I copied the following elements from the build.gradle:

    plugins {
        ...
        id("application")
        id("org.graalvm.buildtools.native") version "0.10.6"
    }
    
    ...
    
    application {
        mainClass.set('com.amazonaws.services.lambda.runtime.api.client.AWSLambda')
    }
    
    graalvmNative {
        binaries {
            main {
                imageName = "native"
                verbose = true
                fallback = false
            }
        }
    }
    

    We can then use our Docker image to build our code base and this will generate a native image in the correct OS (Amazon Linux) by running:

    docker run -it -v `pwd`:`pwd` -w `pwd` -v ~/.gradle:/root/.gradle al2023-graalvm21:native-web \
      ./gradlew clean build -x test nativeCompile
    

    This creates the following build native executable file:

    However, for a Lambda to work it needs a Zip file with both the native executable and a boostrap entry-point file e.g.

    #!/bin/sh
    set -e
    ./native "lambda.test.HelloLambdaHandler"
    

    Interestingly the SAM CLI uses a Makefile to create the final Zip e.g.

    build-HelloWorldFunction:
        rm -rf ./build
        ./gradlew clean
        ./gradlew nativeCompile
        cp ./build/native/nativeCompile/native $(ARTIFACTS_DIR)
        cp ./build/resources/main/bootstrap $(ARTIFACTS_DIR)
    

    However, I didn't want to use SAM CLI or Make but instead added a new Gradle task buildNativeLambda to zip these 2 files:

    // Build ZIP file which includes the `bootstrap` script (required for AWS Lambdas) and `native` GraalVM executable
    task buildNativeLambda(type: Zip) {
        archiveBaseName = rootProject.name + "-native"
        from(files('build/native/nativeCompile/native'))
        from(files('build/resources/main/bootstrap'))
    }
    
    buildNativeLambda.dependsOn nativeCompile
    

    I then created a handy build-native.sh script file to do everything i.e. build docker image, run docker image with buildNativeLambda task:

    #!/usr/bin/env bash
    
    docker build -t al2023-graalvm21:native-web .
    docker run -it -v `pwd`:`pwd` -w `pwd` -v ~/.gradle:/root/.gradle al2023-graalvm21:native-web \
      ./gradlew clean build -x test buildNativeLambda
    

    Running this script (./build-native.sh) creates the following build Zip file:

    Which can then be uploaded to AWS Lambda:

    However, in AWS Lambda test screen I got some reflection errors so I copied all the reflect-config.json files from the SAM CLI generated app and it finally worked!

    More detail exists in the example GitHub README.md.