Deploy AWS Lambda with Spring Boot, OpenTofu and Java
Run Spring Boot with Spring Cloud Function in AWS Lambda provisioned by OpenTofu
⭐️ Introduction
In this post, I am going to show you how to develop a Spring Boot AWS Lambda with JDK 21 and Spring Cloud Functions. We will provision the infrastructure with OpenTofu, a popular Terraform-Fork. Check out the article below for more information on OpenTofu.
λ What is a Lambda
A Lambda is a serverless function or computing service that is, in this case, provided by AWS. With the help of AWS Lambda, we can run our code without provisioning a server. Lambdas are event-driven, so you can invoke your lambda by events like a new item in your S3 file storage or by calling a function URL. There are several options on how you can invoke your lambda. Besides that, you can wait synchronously for the response or invoke it asynchronously and immediately return a response. Lambdas have a short lifespan and there are cold starts, the first request in a longer period that the lambda serves. There are also warm starts, that is when existing lambda runtimes are reused. Thus, you can instantiate database connections outside the lambda handler method and reuse them across different invocations. Another aspect is the long startup time from Spring Boot, as you ideally can spin up a lambda within a few milliseconds. We will see how this works out and discuss alternatives.
✅ Benefits
Why do you might consider using AWS Lambda?
It offers a serverless environment for your application and removes the overhead of managing servers
Lambdas are pretty elastic and you can easily scale during peak times while you have a pay-per-execution pricing model so it can reduce costs in comparison to traditional servers
Easy event-driven data processing with IoT capabilities or from event sources like S3 or Kinesis
You can choose from a wide variety of programming languages like Java, Python, Node.js, and more
⛔ Limitations
In an AWS Lambda environment, you have limited computing and memory available. If the function was not invoked for a long time the environment has to be set and you will have a cold start. Also, Spring and Java are slower in the initial startup than Python for example. So in general you should aim for small functions that are not too big and that are efficient and fast.
Maximum memory is capped at 10 GB, and CPU power is directly related to the amount of RAM
The function will timeout after 15 Minutes. So the Lambda is not suitable for very long-running jobs.
Bigger JARs can lead to slower cold starts. You can use Provisioned Concurrency to pre-warm your function even when there is no traffic
Connection pooling can cause issues as connections are held but might not be used anymore after a warm start and you can exhaust resources. You can use RDS Proxy for MySQL or Postgres to make your application more resilient.
At most 1000 concurrent executions across all functions in an AWS Region. This can be problematic if you have many functions in your account with much traffic.
💻 Setup
We are using JDK21 with Spring Boot 3.2.3 and Spring Dependency Management 1.1.4. In the following paragraphs, I am going to present you with some of the key components that we are going to use. If you want to check out the code for yourself see the GitHub repository.
🚨 Costs
If you follow along this could introduce costs to your AWS bill, so act at your own risk.
Spring Cloud Function Lambda
Here we have a basic example Lambda with Spring Boot, where the Bean of type Function<String, String> acts as our Cloud Function. This function uses a simple way to obfuscate the input String by encoding it with Base64.
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());
}
}
In essence, this implements AWS’s RequestStreamHandler and you do not have to specify anything else besides the handler. There are also other types like the regular RequestHandler, but it is limited to a more trivial deserialization while the stream solutions allow custom Types you can use for deserialization. For instance, you could use the library com.amazonaws:aws-lambda-java-events
to allow deserialization into an S3Event that triggered the lambda. Also if you want to have more functions in your application but separated into different lambdas, you can use the application property spring.cloud.function.definition
and set it dynamically via environment variables to deploy different lambdas from that codebase.
Gradle Setup
We have to add the dependency for the spring cloud function adapter. Besides that, a shaded jar is needed to upload it to AWS Lambda. A regular Spring Boot application creates an Uber JAR that contains all dependencies unaltered to package everything needed to run the application into a single JAR. A shaded Jar goes one step further by also renaming packages to avoid classpath conflicts and offers a more robust solution for dependency management.
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
The key resource in our example is the lambda function. We have a lifecycle trigger that ensures we update our function if the jar file in the s3 bucket changes. This is checked via the object version ID which we have through the s3 etag which is a sha256. Besides that, the handler is a key property and has to be set exactly as shown in the following code block.
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
Before we are going to deploy our lambda, we have to build it. For this purpose run:./gradlew clean assemble bootJar
or the appropriate command for Windows if you are not using a Unix-based system.
Next, we need to log in to our AWS account. This can happen through various ways, we will just set the AWS_ACCESS_KEY_ID AND SECRET environment variables. Now we are navigating into our /tf
directory and run the following commands:
tofu init
tofu plan →
check the output if it matches, should create 7 resourcestofu apply
🔎 Testing
So after successful deployment, we want to test our function. For the sake of simplicity, we just visit the UI navigate to Lambda, and select the one we have just created.
When we select the Test tab and fire the example event we see the successful response in the console that obfuscates the input with Base64 as we have expected.
To avoid costs I have executed tofu destroy
afterward so that all the created resources are destroyed.
🏁 TLDR;
AWS Lambda Overview: Serverless computing service by AWS that allows running code without server management, supporting both synchronous and asynchronous executions.
Event-Driven Architecture: Triggered by events such as updates in S3 storage or direct URL calls, suitable for various programming languages including Java and Python.
Key Benefits: Eliminates server overhead, is highly scalable, cost-effective with pay-per-execution pricing, and facilitates easy integration with event sources like IoT devices.
Limitations: Includes cold and warm starts, limited to 10 GB of memory, 15-minute max execution time, and potential issues with connection pooling.
Setup and Implementation: Uses JDK21 and Spring Boot 3.2.3; for example Lambda function with Spring Cloud demonstrates event-driven data processing.
Deployment and Testing: Deployment involves Gradle tasks and AWS deployment with OpenTofu and simple testing through the AWS Lambda UI.
📚 Resources
🔔 Connect with me on LinkedIn.