Leveraging Spring Boot with AWS Lambda for Serverless Solutions
Written on
Introduction to Spring Boot and AWS Lambda
In this article, I will guide you through the process of creating a Spring Boot application that utilizes AWS Lambda, employing JDK 21 and Spring Cloud Functions. We will set up the necessary infrastructure using OpenTofu, a widely used Terraform fork. For further insights on OpenTofu, refer to the linked article below.
What Is AWS Lambda?
AWS Lambda is a serverless computing service that enables you to execute code without the need for server management. This service can be triggered by various events, such as new items in your S3 storage or by invoking a function URL. You have multiple options for triggering your Lambda function, including synchronous waits for a response or asynchronous invocations that return immediately.
AWS Lambda functions are ephemeral, leading to cold starts, which occur when the function is invoked after a period of inactivity. Conversely, warm starts happen when existing Lambda runtimes are reused, allowing for database connections to be established outside the function handler for reuse across invocations. We'll explore how to manage these aspects and discuss potential alternatives.
Benefits of Using AWS Lambda
Why should you consider AWS Lambda for your applications? Here are some compelling reasons:
- Serverless Environment: Eliminates the hassle of server management.
- Elastic Scaling: Easily handles demand spikes with a pay-per-execution pricing model that can lower costs compared to traditional server setups.
- Event-Driven Processing: Facilitates simple data processing from IoT devices or other event sources like S3 or Kinesis.
- Language Flexibility: Supports various programming languages such as Java, Python, and Node.js.
Limitations of AWS Lambda
While AWS Lambda provides numerous advantages, it also has certain limitations:
- Resource Constraints: Limited computing power and memory (maximum of 10 GB).
- Cold Starts: Initial invocation after a period of inactivity can slow down response times.
- Execution Time: Maximum execution time is capped at 15 minutes, making it unsuitable for lengthy processes.
- Deployment Size: Larger JAR files can lead to slower cold starts. Consider using Provisioned Concurrency to mitigate cold starts.
- Connection Management: Connection pooling can create issues due to held connections that might not be utilized, which can exhaust resources.
Setup Environment
For this project, we will utilize JDK 21 alongside Spring Boot 3.2.3 and Spring Dependency Management 1.1.4. Below, I will outline some key components used in this setup. You can also explore the complete code in the GitHub repository.
Costs Involved
Please be aware that following this guide may incur costs on your AWS bill, so proceed with caution.
Spring Cloud Function Lambda Example
Here’s a simple example of a Lambda function using Spring Boot. The function processes a String input and encodes it in Base64 format:
package com.espectro93.awsjdk21lambda;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.Base64;
import java.util.function.Function;
@SpringBootApplication
public class AwJdk21LambdaApplication {
public static void main(String[] args) {
SpringApplication.run(AwJdk21LambdaApplication.class, args);}
@Bean
public Function<String, String> obfuscate() {
return str -> Base64.getEncoder().encodeToString(str.getBytes());}
}
This implementation follows AWS's RequestStreamHandler and requires minimal configuration beyond the handler. There are alternative types such as RequestHandler, but those are limited in their deserialization capabilities.
Gradle Configuration
To deploy this application, we need to include the Spring Cloud Function adapter as a dependency. Additionally, a shaded JAR must be created for AWS Lambda uploads. A shaded JAR not only packages the necessary dependencies but also renames packages to prevent classpath conflicts.
assemble.dependsOn = [thinJar, shadowJar]
shadowJar.mustRunAfter thinJar
shadowJar {
archiveClassifier = 'aws'
manifest {
inheritFrom(project.tasks.thinJar.manifest)}
dependencies {
exclude(dependency("org.springframework.cloud:spring-cloud-function-web"))}
// Required for Spring
mergeServiceFiles()
append 'META-INF/spring.handlers'
append 'META-INF/spring.schemas'
append 'META-INF/spring.tooling'
append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports'
append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports'
transform(PropertiesFileTransformer) {
paths = ['META-INF/spring.factories']
mergeStrategy = "append"
}
}
OpenTofu Resources
In our example, the critical resource is the Lambda function. We have set a lifecycle trigger to ensure that our function updates whenever the JAR file in the S3 bucket changes, verified through the S3 etag.
resource "aws_s3_object" "lambda_jar" {
bucket = aws_s3_bucket.bucket.id
key = "test-lambda"
source = local.lambda_payload_file
etag = filesha256(local.lambda_payload_file)
}
resource "aws_lambda_function" "test_lambda" {
function_name = "aws_jdk21_lambda"
role = aws_iam_role.iam_for_lambda.arn
handler = "org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest"
s3_bucket = aws_s3_bucket.bucket.id
s3_key = aws_s3_object.lambda_jar.key
s3_object_version = aws_s3_object.lambda_jar.version_id
runtime = "java21"
environment {
variables = {
FUNCTION_NAME = "obfuscate"}
}
lifecycle {
replace_triggered_by = [
aws_s3_object.lambda_jar.version_id]
}
}
Deployment Process
To deploy our Lambda function, we first build the project using the command:
./gradlew clean assemble bootJar
If you are on a Windows system, use the appropriate command for your environment. After that, log into your AWS account by setting the AWS_ACCESS_KEY_ID and SECRET environment variables, then navigate to the /tf directory and run:
tofu init
tofu plan
tofu apply
Testing the Lambda Function
Once the deployment is successful, we can test the function through the AWS Lambda console. After selecting the "Test" tab and triggering an example event, we should see a successful Base64-encoded response.
To avoid incurring costs, I recommend executing tofu destroy afterward to delete all created resources.
Summary
AWS Lambda provides a serverless computing environment that allows for the execution of code without managing servers, supporting both synchronous and asynchronous executions. It is event-driven and integrates seamlessly with various programming languages.
Key benefits include reduced server management overhead, scalability, cost-effectiveness, and easy integration with event sources. However, it also has limitations, such as cold starts, memory restrictions, and a maximum execution time.
Resources
- AWS Lambda Documentation
- Spring Cloud Function Documentation
- GitHub Repository
This video demonstrates how to deploy a Spring Boot 3 API on AWS Lambda in just 10 minutes.
In this video, learn how to deploy a Spring Boot application to AWS Lambda utilizing an API Gateway.