Migrating a Spring Boot 2.x.x App to AWS Lambda Serverless

Migrating a Spring Boot 2.x.x App to AWS Lambda Serverless

Hello everyone, I'm Jahid Momin , and today I'll walk you through the process of migrating a serverful Spring Boot application 2.x.x to a serverless architecture using AWS Lambda. This guide includes everything you need to get started. We'll cover the prerequisites, the necessary configurations, and provide a detailed step-by-step guide to ensure you have a seamless migration to serverless .

In this article , we will convert a simple Spring Boot application with two endpoints to run on AWS Lambda. We'll start by understanding what serverless computing is and why it's beneficial, followed by a practical guide to making your Spring Boot application serverless.


What is Serverless Lambda?

Serverless computing allows you to build and run applications without thinking about servers. AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources for you.


Advantages of Serverless Over Serverful Architecture

  • Cost-Efficiency: Pay only for the compute time you consume.
  • Scalability: Automatically scales your application by running code in response to each trigger [trigger might be api gateway , event bridges etc ].
  • Reduced Operational Overhead: No need to manage or provision servers or to choose EC2s.


How a Serverless Application Works ?

In a serverless application, the cloud provider [In our case its aws] manages the infrastructure and automatically scales the application in response to incoming requests. AWS Lambda, in particular, runs your code[language independent In our case its spring boot java application] in response to various events, such as HTTP requests via API Gateway, file uploads to S3, or changes in a DynamoDB table.

When a request is received, Lambda automatically initializes and executes your app/code, then scales down once the request has been processed. This elasticity ensures that you only pay for what you use and can handle varying levels of traffic seamlessly.


Before we begin, ensure you have the following tools installed and configured :


Creating a Spring Boot REST API Project

Let's start by creating a simple Spring Boot application version 2.7.5 with two endpoints: one for testing and another for fetching data from a database. to setup project you can use https://start.spring.io/ as well but right now you can find new versions of spring boot . we will look serverless for spring boot 3 in upcoming article.

pom.xml - dependencies

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>

	</dependencies>        

Now let's move forward and setup application.properties

spring.application.name=demoSprintboot2TestApp
spring.datasource.url=*******
spring.db.driver="com.mysql.cj.jdbc.Driver"
spring.datasource.username=*******
spring.datasource.password=********
spring.jpa.hibernate.ddl-auto=update        

Create an Entity & JPA Repository , I have used lombok you can use if you want otherwise just create getters & setters .

package com.test2.demo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "test_table")
public class MyTestEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;
}        
package com.test2.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MyTestEntityRepository extends JpaRepository<MyTestEntity, Long> {
}        

It's time to create an rest controller with two endpoints

package com.test2.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyRestController {

    @Autowired
    private MyTestEntityRepository myTestEntityRepository;

    @GetMapping("/api/test")
    public ResponseEntity<String> test() {
        return ResponseEntity.ok("Testing");
    }

    @GetMapping("/api/getData")
    public ResponseEntity<?> getData() {
        return ResponseEntity.ok(myTestEntityRepository.findAll());
    }
}        

You can now run and test the endpoints locally.

mvn spring-boot:run        

Our application is ready now but still serverful. You can run and test the endpoints locally. Now, we will do the steps to move into a serverless architecture.


Migrating to Serverless Architecture

Let's move on to converting our serverful application to a serverless one using AWS Lambda. I hope you already installed aws cli and aws sam in your system and setup secret and accesskey for aws user with all privileges [ s3,stack,lambda,api gateway full privileges recommended ]

Update your pom.xml to include the necessary dependencies for running your Spring Boot application on AWS Lambda.

Adding AWS SAM Dependencies

To migrate our Spring Boot application to AWS Lambda, we need to add specific dependencies to our pom.xml. These include the aws-serverless-java-container-springboot2 dependency which helps in running Spring Boot applications in a serverless environment. This dependency bridges the gap between AWS Lambda and Spring Boot, enabling the application to handle HTTP requests within a Lambda function.

<dependency>
    <groupId>com.amazonaws.serverless</groupId>
    <artifactId>aws-serverless-java-container-springboot2</artifactId>
    <version>[1.9,2.0.0-M1)</version>
</dependency>        

Adding the Maven Shade Plugin

The Maven Shade Plugin packages all dependencies into a single JAR, which is necessary for deployment to AWS Lambda. This process ensures that all required classes and libraries are bundled together, making it easy for Lambda to execute our application without any missing dependencies.

	<profiles>
		<profile>
			<id>shaded-jar</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-shade-plugin</artifactId>
						<version>3.2.4</version>
						<configuration>
				<createDependencyReducedPom>false</createDependencyReducedPom>
						</configuration>
						<executions>
							<execution>
								<phase>package</phase>
								<goals>
									<goal>shade</goal>
								</goals>
								<configuration>
									<artifactSet>
										<excludes>											<exclude>org.apache.tomcat.embed:*</exclude>
										</excludes>
									</artifactSet>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>        

Updated pom.xml

We exclude Tomcat because AWS Lambda provides its own runtime environment. The aws-serverless-java-container-springboot2 dependency is crucial for running Spring Boot applications on AWS Lambda. This library adapts AWS Lambda's event model to the Spring Boot framework, enabling us to run our application as a serverless function. The version range ensures compatibility with the Spring Boot 2.x series.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.amazonaws.serverless</groupId>
    <artifactId>aws-serverless-java-container-springboot2</artifactId>
    <version>[1.9,2.0.0-M1)</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>        


Creating the Lambda Handler

To handle requests in AWS Lambda, we need to create a handler class. This class implements RequestStreamHandler and sets up the SpringBootLambdaContainerHandler to handle incoming requests.

package com.test2.demo;

import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder;
import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;

public class StreamLambdaHandler implements RequestStreamHandler {
    private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;

    static {
        try {
            handler = new SpringBootProxyHandlerBuilder<AwsProxyRequest>()
                            .defaultProxy()
                            .asyncInit()
                            .springBootApplication(DemoApplication.class)
                            .buildAndInitialize();
// optional if you want to avoid filter just comment below code
            handler.onStartup(servletContext -> {
                FilterRegistration.Dynamic registration = servletContext.addFilter("JwtValidationFilter", JwtValidationFilter.class);
                registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
            });
        } catch (ContainerInitializationException e) {
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring Boot application", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}        

  • SpringBootLambdaContainerHandler: This is the core component that adapts the Spring Boot application to run within the Lambda environment.
  • The StreamLambdaHandler is our entry point for AWS Lambda. It initializes the Spring Boot application and handles incoming requests.

  • Why Handler: The handler class bridges the gap between AWS Lambda's event-driven model and the Spring Boot application. It sets up the application context and handles requests by delegating them to the Spring Boot application.
  • Async Initialization: We use asyncInit for non-blocking initialization, which can reduce cold start times.
  • JWT Validation: Optional , example includes a filter for JWT validation, demonstrating how you can add security layers if needed.

Final step to setup our template.yaml file for AWS SAM.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example Test Spring Boot 2 API written with SpringBoot with the aws-serverless-java-container library

Globals:
  Api:
    EndpointConfiguration: REGIONAL

Resources:
  TestSpringBoot2Function:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.test2.demo.StreamLambdaHandler::handleRequest
      Runtime: java17
      CodeUri: .
      MemorySize: 512
      Policies: AWSLambdaBasicExecutionRole
      SnapStart:
        ApplyOn: PublishedVersions
      Environment:
        Variables:
          MAIN_CLASS: com.test2.demo.DemoApplication
      Timeout: 60
      Events:
        HttpApiEvent:
          Type: HttpApi
          Properties:
            TimeoutInMillis: 20000
            PayloadFormatVersion: '1.0'

Outputs:
  SpringBoot2TestApi:
    Description: URL for the API
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/"        

  • AWS SAM Template: This YAML file defines the AWS resources required for your serverless application. It specifies the Lambda function, its configuration, and the HTTP API event source.
  • Handler: Specifies the entry point of the Lambda function (Here my lambda function name is TestSpringBoot2Function).
  • Runtime: Defines the runtime environment as Java 17.
  • Memory Size and Timeout: Configures the memory and timeout settings for the Lambda function . [ I have kept 128 mb earlier but when application grows i mean if you added jpa then it needs more memory to initialization ]
  • Environment Variables: Sets environment variables, including the main class of the Spring Boot application.
  • Events: Defines an HTTP API event to trigger the Lambda function , you can add any event like scheduler,event bridges queues etc as per aws doc . In above example we are using event as a AWS API Gateway to trigger my spring boot 2 application .


Springboot app takes time to initialize - use SnapStart always

The SnapStart property in the AWS SAM template is used to optimize the cold start performance of AWS Lambda functions. When SnapStart is configured with ApplyOn: PublishedVersions, AWS Lambda pre-warms the function when new versions are published. This helps reduce the initial invocation latency by allowing Lambda to reuse previously initialized resources.

Deploying

First, build your project using Maven.

mvn clean package        

This command will generate a shaded JAR file containing all your application's dependencies.


Deploy your application using the AWS SAM CLI.

sam build
sam deploy --guided

first time deployment if you used --guided then it will ask some configuration questions to generate samconfig.toml file .

stack_name = "springboot-2-api-stack"
resolve_s3 = true
s3_prefix = "springboot-2-api-stack"
region = "ap-south-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = [] 

Next time you can avoid --guided .        

sam build compiles and prepares AWS SAM application dependencies, while sam deploy --guided interactively deploys the application with user-specified configurations.

It will create manage resources for you.

And Last You will get API Gateway URL

Hit the URL with your REST Endpoints .

Feel free to connect with me on LinkedIn Jahid Momin for any questions or further discussions on deploying Spring Boot applications on AWS Lambda. For more blogs , visit my medium.

Thank you for reading! I hope this guide has been helpful in understanding how to migrate your Spring Boot 2.x.x applications to a serverless architecture using AWS Lambda with AWS SAM.

Keep Sharing :)


References :

https://github.com/aws/serverless-java-container

https://github.com/aws/serverless-java-container/wiki/Quick-start---Spring-Boot2

https://github.com/jahidmomin/springboot2testserverless - master branch .


要查看或添加评论,请登录

Jahid Momin的更多文章

社区洞察

其他会员也浏览了