AWS Lambda with Spring Boot
The typical deployment scenario for a Spring Boot application in AWS involves running the Java application on an EC2 instance 24 hours a day. Of course, the application could be deployed in AWS ECS as a Docker container, but it still runs continuously on an EC2 instance. In each case, the EC2 instances need to be monitored and you pay for compute capacity used by that EC2 instance.
AWS Lambda provides low cost compute with zero maintenance. Lambda runs your code on demand, without provisioned and managed servers. Lambda automatically runs and scales your code. You are charged for every 100ms your code executes and the number of times your code is triggered. If the code isn't running, you pay nothing.
Lambda has clear cost and maintenance benefits, but what does it take to run the standard Spring Boot application as a Lambda? How does it work? What are the drawbacks? These are the questions that will be answered in this article through a tangible example.
Lambdas Are Triggered By Events
Lambdas have many use cases. They can be triggered by events on S3 buckets, SNS, Kinesis Streams, and DynamoDB tables. In addition, AWS API Gateway can be used to trigger Lambda code.
Today, we'll discuss running a Spring Boot application as a Lambda and accessing the APIs via an API Gateway. The API Gateway will give a nice demonstration of accessing our Spring Boot code via a REST API.
For good measure, we'll use Cloud formation scripts to make this setup reproducible and version controlled. Think infrastructure as code.
Let's Get Started
First, navigate here: https://github.com/gemerick/spring-boot-lambda. The repository provides a basic Spring Boot application with a single REST API that we'll use for testing. The project README.md provides detailed instructions on running the application or you can simply use your favorite IDE.
Let's clone the repository and start the application:
git clone https://github.com/gemerick/spring-boot-lambda.git
cd spring-boot-lambda
mvn spring-boot:run
Now use cURL or a REST client to test the API: GET http://localhost:8080/languages.
curl http://localhost:8080/languages
As you'll see, the response is a list of languages supported by AWS Lambda.
Conversion
The Lambda branch of the repository contains the completed code that enables running the application as a Lambda.
git checkout lambda
Let's discuss the key parts.
Dependency
AWS Labs provides the AWS Serverless Java Container. We must add this dependency to the Maven pom.xml.
<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-spring</artifactId>
<version>[0.1,)</version>
</dependency>
There are also two build profiles defined. See the profiles section of the pom.xml. The default is now to build for Lamda, while a command line option on MVN will build a standard Spring Boot application running Tomcat.
mvn package -Dboot
Handler and Configuration
Create the LambdaHandler
class in the same package as the application. This code is the same as that is detailed by the AWS Serverless Java container quick start. The only addition is to enable the lambda Spring Boot profile.
handler.activateSpringProfiles("lambda");
Create a standard Spring Boot configuration class Config. We are disabling beans used for HTTP handling and optimizing for Lambda execution. @EnableWebMvc
is required for Lambda deployment. The @Profile
annotation makes these beans only load when the Lambda profile is enabled. Thus, the code can still run as a Spring Boot application outside of Lambda.
@EnableWebMvc
@Profile("lambda")
Deployment with SAM
AWS CloudFormation provides a common language for describing and provisioning all the infrastructure resources in your cloud environment.
AWS SAM is a model used to define serverless applications on AWS. SAM is based on Cloudformation and provides a simplified syntax. Using SAM greatly simplifies the code to create serverless applications. A sam.yaml
template is defined for this project.
The deployment steps will require AWS CLI to be installed and configured. Follow these steps to deploy the Lambda:
- Clean and rebuild the code as a shaded jar, not as a Spring Boot jar.
mvn clean package
- Create an S3 bucket to hold the application code. This bucket name must be unique across S3, so adjust for your use in the next two steps.
aws s3 mb s3://spring-boot-lambda-0403
- Copy the jar file to the S3 bucket and update the information into a SAM template.
aws cloudformation package --template-file sam.yaml --output-template-file target/output-sam.yaml --s3-bucket spring-boot-lambda-0403
- Deploy a Cloudformation stack from the SAM template. We must provide the
--capabilities
to allow the deploy to succeed because SAM will be creating IAM roles and policies needed to allow the API Gateway to execute the Lambda function.aws cloudformation deploy --template-file target/output-sam.yaml --stack-name spring-boot-lambda --capabilities CAPABILITY_IAM
- Describe the stack, which will display the URL of the API in the outputs.
aws cloudformation describe-stacks --stack-name spring-boot-lambda Ie. "OutputValue": "https://xjmaifytx3.execute-api.us-east-2.amazonaws.com/Stage/languages"
Test
The API can now be tested with cURL or a REST Client.
curl http://localhost:8080/languages
What Was Created?
AWS SAM created many AWS resources to bring the application to life.
Log in to the AWS Console and visit CloudFormation. Click on the spring-boot-lambda
stack and expand the resources section. Here, you can see the seven individual resources that make up the stack. The outputs section will also display the API URL found in the describe-stacks
command above.
Update Code
We have two choices when we need to update the Lambda code. We can delete and re-create the entire cloud formation stack or simply update the code directly on the Lambda function.
Let's detail updating the Lambda function here. Our conclusion will discuss deleting a stack.
Update LanguageResource.java
to add go
as a supported Lambda language. Add the following to line 17 of LanguageResource
:
new Language("go")
First, find the full name of the Lambda:
aws lambda list-functions
=> FunctionName: boot-lambda-LambdaSpringBootFunction-CW288NK5BFM9
Build the updated code.
mvn clean package
Update the function. Be sure to adjust the full function-name
.
aws lambda update-function-code --function-name spring-boot-lambdaSpringBootFunction --zip-file fileb://target/spring-boot-lambda-1.0.0-SNAPSHOT.jar
Monitoring
One final AWS Resource to investigate is CloudWatch. CloudWatch stores the logs of the Lambda application and provides filtering. Navigate to CloudWatch in the AWS Console and select "Logs" from the left menu. Next, select the "LogGroup" for the Lambda to see the available log stream.
Another more direct route is to navigate to Lambda services in the AWS console. Then, select the spring-boot-lambda
. Next, select the Monitoring tab. To jump directly to the logs, select any of the "Jump to Logs" links.
This page also provides several metrics that are worth investigating.
Drawbacks
As you can see in this example duration graph that follows, it took eight seconds for Lambda to execute once. The average duration of four requests is two seconds. This leads us to the only drawback of using Spring Boot via API Gateway and Lambda. We find that the first invocation of a Lambda function must start the Java Virtual Machine and Spring Boot, a Lambda cold start. Subsequent executions, while the Lambda is warm, take less than 200 milliseconds.
While Lambdas are kept running (warm) for approximately five minutes, it's also worth noting that a cold start happens once for each concurrent execution of your function. As you consider using Spring Boot — or Java in general — you must keep these numbers in mind and decide if your application can afford the cold start times.
Cold start times may make Spring Boot a less than ideal choice for a Lambda accessed via API Gateway. It's certainly worth noting that it can be ideal for processing events triggered by S3 buckets, SNS, Kinesis Streams, and DynamoDB. In these scenarios, the cold start time of a few seconds is insignificant given that it isn't visible to a user.
Conclusion
Spring Boot makes it easy to create stand-alone, production-grade, Spring-based applications that you can "just run."
We've shown that these applications can quickly be updated to run in AWS Lambda, which provides low cost, zero maintenance compute that automatically scales. At the same time, we can build and test the applications prior to deployment like we are accustomed to.
When the application is ready, it can easily be deployed with CloudFormation and AWS SAM can do a lot of the heavy lifting. Cold start times must also be taken into account when choosing Lambda.
Cleanup
It's always a good habit to clean up unused resources in AWS. Unlike other resources, Lambdas won't cost unless you use them, but let's keep our good habits intact:
aws cloudformation delete-stack --stack-name spring-boot-lambda