Introduction
Several years ago, I wrote an article series Lambda function using Docker Container Image, where I explained how to convert an application deployed on AWS Lambda as a managed Java runtime to one deployed on AWS Lambda as a (Docker) container image. As we saw, the cold start time of a Lambda function using a container image was quite high. Back then, no Lambda cold-start performance optimization approaches were available, such as Lambda SnapStart. This has changed recently, as AWS Lambda now supports SnapStart for container image functions. Now it's time to revisit this topic and evaluate this option. In this part of the series, we'll refresh our knowledge about how to convert the sample application to one using a container image as a deployment artifact on AWS Lambda and measure the performance of the Lambda functions.
In the next part, we'll enable SnapStart on the Lambda functions as we did in part 3. We'll also apply the same priming techniques that we applied in parts 4-5 of this series. Of course, we'll measure the performance of the Lambda functions for all scenarios.
Sample application using a Container Image
You can find the final version of our sample application in the aws-lambda-java-25-dynamodb-as-container-image GitHub repository, which we can deploy on AWS Lambda as a container image. Let's go through the changes that we had to make to our application from part 1. First of all, we need to define a Dockerfile:
FROM public.ecr.aws/lambda/java:25
COPY target/aws-lambda-java-25-with-dynamodb-as-container-image-*.jar ${LAMBDA_TASK_ROOT}/lib/
Here we use the AWS Lambda base Docker image public.ecr.aws/lambda/java:25 for Java 25 from the public Amazon ECR repository. Here you can find all Lambda base Docker images for Java provided by AWS. In the Docker image, we also copy the application's code packaged as a JAR file to the path of our Lambda function code. This path is defined as the runtime environment variable LAMBDA_TASK_ROOT.
Next, let's answer the question of how to create and push a Docker container image to the ECR repository. You can do it automatically when you execute sam build, and sam deploy command chain, or if you build and package the application outside of SAM and only deploy with SAM, you need to do it manually, like this:
# Build and package your application
mvn clean package
# Build Docker image with name aws-lambda-java-25-with-dynamodb:
sudo docker build --no-cache -t aws-lambda-java-25-with-dynamodb:v1 .
# Log in to Amazon ECR in case the credentials have expired:
aws ecr get-login-password --region {aws_region} | sudo docker login --username AWS --password-stdin {aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com
# Create Amazon ECR repository if it does not exist with the name aws-lambda-java-25-with-dynamodb:
aws ecr create-repository --repository-name aws-lambda-java-25-with-dynamodb --image-scanning-configuration scanOnPush=true --region {aws_region}
# Tag our local Docker image and link it with the Amazon ECR repository.
# Exactly this image is referenced as ImageUri in the Lambda function:
sudo docker tag aws-lambda-java-25-with-dynamodb:v1 {aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1
# Push the Docker image to the Amazon ECR repository:
sudo docker push {aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1
Please replace the values of the variables {aws_account_id} and {aws_region} with your own values. If you build a version (image tag) other than v1, please replace the version reference as well.
You can find all other changes in the AWS SAM template. Instead of defining Runtime (as Java 25), CodeUri, and Handler parameters, we have to define the following for all Lambda functions:
GetProductByIdFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: GetProductByIdJava25WithDynamoDBAsCI
PackageType: Image
ImageUri: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1
ImageConfig:
Command: [ "software.amazonaws.example.product.handler.GetProductByIdHandler::handleRequest" ]
First, we set the PackageType to Image and point the ImageURI to the ECR function's code in the ECR repository. We created the repository with the name aws-lambda-java-25-with-dynamodb above. Please pay attention to the version (image tag). We use v1 here, but you can use latest instead. Then, in the ImageConfig, we define the Command property, which is specific to each Lambda function and should point to the function's handler. For a function defined as a container image, Lambda resolves the image tag to an image digest. In Amazon ECR, if you update the image tag to a new image, Lambda does not automatically update the function. You need to use update-function-code instead.
To deploy our application, we need to invoke sam deploy --image-repository {aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/aws-lambda-java-25-with-dynamodb command.
Please note that SnapStart charges apply to the container images, while SnapStart for supported Java managed versions remains free on AWS Lambda.
Measurements of cold and warm start times of our application using a Container Image
We'll measure the performance of the GetProductById Lambda function mapped to the GetProductByIdHandler. We will trigger it by invoking curl -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25CI" https://{$API_GATEWAY_URL}/prod/products/1.
We designed the experiment exactly as described in part 2.
The deployed artifact size of the container image of this application was 138.53 MB.
Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:
| Approach | c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Lambda Container Image | 2788 | 2884 | 3063 | 3374 | 3456 | 3460 | 5.25 | 5.73 | 6.30 | 9.46 | 56.82 | 1481 |
Conclusion
In this part of the series, we learned how to convert the sample application to one using a container image as a deployment artifact on AWS Lambda and measured the performance of the Lambda functions. We observed that the Lambda cold start times are quite high. But there is a noticeable improvement compared to my last measurements, which I did with Java 21. I refer to the article that describes AWS efforts to reduce container cold start times in general.
In the next part, we'll enable SnapStart on the Lambda functions using a container image. We'll also apply priming techniques and measure the performance of the Lambda functions for all scenarios.
Please also watch out for another series where I use a relational serverless Amazon Aurora DSQL database and additionally the Hibernate ORM framework instead of DynamoDB to do the same Lambda performance measurements.
If you like my content, please follow me on GitHub and give my repositories a star!
Please also check out my website for more technical content and upcoming public speaking activities.
Top comments (0)