Packaging AWS Java lambdas in Docker
By: Alexander Smirnov
Tags: Docker, AWS Lambda, Maven, Gradle, Java

AWS Lambda is a great service, but you need to be careful when packaging Java programs for AWS Lambda. It is really easy to make a silly mistake and spend a day trying to understand what’s going on, why classes can’t be found, and looking for mediation.

In this article I will guide you through packaging of Docker-based Java function, but many ideas are also applicable to ZIP functions too.

First, let’s pickup a simple Java function for the purpose of the exercise. Go ahead and clone java-basic project. It has both maven and gradle build files, thus you can use any of the build tool. Take 5 minutes to familiarise yourself with the handlers’ code, but do not go deep with the bash scripts in the root, as we won’t use them for OCI deployment. All we need is the Java code.

Second, we will make up a Dockerfile. If you are using gradle, use this one:

FROM public.ecr.aws/lambda/java:11



COPY build/classes/java/main /var/task

COPY build/dependency/* /var/task/lib/



CMD [ "example.Handler::handleRequest" ]

Let’s walk through it:

FROM defines a base image. It is a good idea to derive from the official AWS image baked for Java 11

COPY class files from build directory into Lambda’s task root

COPY runtime dependencies from build directory to the image’s lib directory. We will define a gradle task to collect the runtime dependencies a little bit later.

CMD defines Lambda’s entry point. It must match Java package name and handler name. Please note the semicolons between them. This notion is different from standard Java notion, where fully qualified name of a method utilises number sign - com.example.ClassName#methodName

In order to collect runtime dependencies in gradle you can use the following task.

task copyRuntimeDependencies(type: Copy) {

from configurations.runtimeClasspath

into 'build/dependency'

}



build.dependsOn copyRuntimeDependencies

After everything is copied, the resulting directory structure will look like:

/var/task/

├── example

│ ├── Handler.class

│ ├── HandlerDivide.class

│ └── <other class files here>

│

└── lib

├── aws-lambda-java-core-1.2.1.jar

└── gson-2.8.6.jar

Now we should be ready to build the project and package it into a docker image:

$ gradle build && docker build -t java-basic-oci .

To test it locally, let’s run a container:

$ docker run -p 9000:8080 java-basic-oci

and in the other terminal tab:

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'

Examine function output in both terminals.

Homework exercise #1: run container for another function from the project Homework exercise #2: run an OCI function in the cloud

Common mistakes you should avoid:

copying dependency jar files into /var/task directory. They are just invisible for lambda. Always use /var/task/lib for dependencies

directory. They are just invisible for lambda. Always use for dependencies copying main code jar files into /var/task directory. This directory must contain *.class files only, or directories with class files (packages), but not jars.

directory. This directory must contain files only, or directories with class files (packages), but not jars. naming jar files with extension different from jar , for example function.JAR or function.Jar . Such files are also invisible for lambda. Correct: function.jar

As a bonus, below you can find Dockerfile for Maven layout and a plugin setup to copy runtime dependencies:

FROM public.ecr.aws/lambda/java:11



COPY target/classes /var/task

COPY target/dependency/* /var/task/lib/



CMD [ "example.Handler::handleRequest" ]

include maven-dependency-plugin to the project to copy runtime dependencies to target/dependency

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-dependency-plugin</artifactId>

<version>3.1.2</version>

<executions>

<execution>

<id>copy-dependencies</id>

<phase>package</phase>

<goals>

<goal>copy-dependencies</goal>

</goals>

<configuration>

</configuration>

</execution>

</executions>

</plugin>

</plugins>

</build>

use the following command to compile the project and collect the dependencies:

$ mvn compile dependency:copy-dependencies -DincludeScope=runtime

That’s it! Happy coding!

I used materials from https://gallery.ecr.aws/lambda/java for this article.