Deploying Java-based AWS Lambda functions as OCI images and JAR files
By: Heeki Park
Tags: Aws Api Gateway, AWS Lambda, AWS, Java, Serverless

To test out and demo the announcement around container support for AWS Lambda functions, I built a demo to investigate what it would take to transition a Spring Boot REST API, which might be deployed as a container, into a native API Gateway + Lambda setup, which could deployed both as a container and a jar file. This announcement is potentially compelling for enterprises that have mature toolchains and deployment pipelines that are based on containers. These customers can leverage these existing toolchains and now add Lambda as an additional deployment target.

Anyhoo. Here are some lessons learned in my journey to implement the demo.

Look into those deprecation warnings

I was using the developer guide for Java events with API Gateway which uses APIGatewayProxyRequestEvent and APIGatewayProxyResponseEvent . The APIGatewayProxyRequestEvent is now deprecated. The reason this was important was that the proxy integration payload that was passed from API Gateway (HTTP API) into the Lambda function was missing a section in the RequestContext which included the HTTP metadata that I needed.

"http": {

"method": "GET",

"path": "/dev/",

"protocol": "HTTP/1.1",

"sourceIp": "10.20.30.40",

"userAgent": "curl/7.54.0"

},

I could have caught that with the maven warnings, but being a novice Java developer, I didn’t know to what they referred. Looking at the source code, my colleague George Mao found the deprecated snippets and pointed out the need to use the updated classes.

[WARNING] OciHandler.java uses or overrides a deprecated API.

[WARNING] Recompile with -Xlint:deprecation for details.

After replacing the deprecated APIGatewayProxyRequestEvent with APIGatewayV2HTTPEvent , the deprecation warnings went away.

But, when I looked at the new class, I wasn’t quite sure how to get at the fields that I needed, as I didn’t know how the lombok library was generating the getters and setters. After hacking at it a little bit, I found just adding get/set before each of the private properties enabled me to access the elements that I needed. For example:

if (event.getIsBase64Encoded()) {

body = new String(Base64.getDecoder().decode(event.getBody()));

} else {

body = event.getBody();

}

Use the right set of properties

I generally always default to defining my resources using infrastructure as code. SAM is really helpful for doing that for AWS serverless resources (and can add in regular CloudFormation for other resources). I find that reading the CloudFormation specifications helps in understanding the properties of the particular resource. Below is an example definition of a container-based function resource.

FnOci:

Type: AWS::Serverless::Function

Properties:

PackageType: Image

ImageUri: !Ref imageUri

Role: !GetAtt FnExecRole.Arn

Simple, I know. But it took me a little while to realize that I needed to use the new property PackageType and set that to Image . The Runtime , Handler , and Layers properties aren’t allowed when doing a container-based deployment. Also, keep an eye out if you’ve set any of those properties, e.g. Runtime , in your Globals section, as that will cause your template to fail too.

Also, if you mistakenly deployed your function as Zip type, you’ll need to delete the function and redeploy it, as you can’t switch PackageType on an existing deployment.

Try.. try.. try again

And I had a very hard to figuring out the proper configuration for the Dockerfile for building the docker image for a Java application. Fortunately, sam init provides starter templates, which is where I was able to find a good working example. Repeating here for easy reference.

FROM amd64/maven as build-image

WORKDIR "/task"

COPY src/ src/

COPY pom.xml ./

RUN mvn -q clean install

RUN mvn dependency:copy-dependencies -DincludeScope=compile FROM public.ecr.aws/lambda/java:11

COPY --from=build-image /task/target/classes /var/task/

COPY --from=build-image /task/target/dependency /var/task/lib

CMD ["your.package.name.yourClassName::handleRequest"]

And success!

After all that, I was able to deploy the function as both a container and a jar file. I then hooked up each of those functions to different endpoints on my HTTP API just to demonstrate calling the different functions, e.g.

curl -s -XGET http://api.example.com/dev/customer/jar | jq

curl -s -XGET http://api.example.com/dev/customer/oci | jq

Welp. I hope that was helpful. Until next time.

Epilogue

The Dockerfile that’s provided by sam init does a clean install every time an image is built, which means that the dependencies are downloaded every time too. For iterative development, I prefer compiling locally and then packaging in the container. So I run the mvn dependency:copy-dependencies -DincludeScope=compile command locally, which after the first run already had all the dependencies downloaded and cached. Then, with the guidance of the sam init template, I was able to modify my Dockerfile as such:

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

COPY target/classes /var/task/

COPY target/dependency /var/task/lib

CMD ["your.package.name.yourClassName::handleRequest"]

This significantly reduced build times and allowed me to test more rapidly. Of course, when going into production, stick with the clean installs.