Table of contents
- Table of contents
- Foreword
- What we'll build
- What we'll need
- Spring Boot setup
- Configure Folder Structure and Gradle
- Deploy to AWS
- Important things to know when working with Spring and AWS Lambda
Foreword
Serverless approach is a great alternative to a "server" way of developing and hosting the applications. It can save a lot of time when dealing with architecture, plus it manages all scalability under the hood. Another great benefit is the price: in a case of AWS Lambda, it costs $0 per 1M requests a month and $0.20 per the next 1M. Although the price may vary depending on functions memory, it's still pretty cheap. Let's see how to setup and run basic Spring Boot application in the cloud.
What we'll build
We'll build simple Spring Boot application running on AWS that respond with a JSON.
What we'll need
- 20 minutes of time
- Account on AWS
- AWS CLI. Here's a link for AWS CLI configuration
- Serverless Framework
- JDK 1.8 +
- Gradle 4+
Spring Boot setup
To skip setup:
- Clone the repository:
git clone https://github.com/skryvets/spring-boot-serverless
- cd into
spring-boot-serverless
- jump ahead to deployment section
Configure Folder Structure and Gradle
In the project directory (in this example we named it as
spring-boot-serverless
) create the following folder
structure:
|--spring-boot-serverless
|-- src
|-- main
|-- java
|-- springBootServerless
gradle.build
Here's gradle.build
that will take care of build process
(should be created right below src folder):
{
buildscript {
ext = '2.0.2.RELEASE'
springBootVersion }
{
repositories mavenCentral()
}
{
dependencies classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin
= 1.8
sourceCompatibility
{
repositories mavenCentral()
}
// Task for building the zip file for upload
buildZip(type: Zip) {
task // Using the Zip API from gradle to build a zip file of all the dependencies
//
// The path to this zip file can be set in the serverless.yml file for the
// package/artifact setting for deployment to the S3 bucket
//
// Link: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html
// set the base name of the zip file
= "springBootServerless"
baseName
from compileJava
from processResourcesinto('lib') {
.runtime
from configurations}
}
.dependsOn buildZip
build
wrapper(type: Wrapper) {
task = '4.7'
gradleVersion }
{
dependencies compile('org.springframework.boot:spring-boot-starter-web')
compile('com.amazonaws.serverless:aws-serverless-java-container-spring:1.1')
compile('io.symphonia:lambda-logging:1.0.1')
}
As a next step, we will create a model that represents the response:
Create a simple Response model
Path: spring-boot-serverless/src/main/java/springBootServerless/models/Response.java
package springBootServerless.models;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Response {
@JsonProperty("success")
private boolean success;
@JsonProperty("message")
private String message;
public Response(boolean success, String message) {
this.success = success;
this.message = message;
}
}
As a next step we will create a simple controller that will handle get requests at the root level:
Create a simple controller
Path: spring-boot-serverless/src/main/java/springBootServerless/controllers/HelloWorldController.java
package springBootServerless.controllers;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import springBootServerless.models.Response;
@RestController
public class HelloWorldController {
@RequestMapping(value = "/", method = RequestMethod.GET)
@ResponseBody
public Response hello() {
return new Response(true, "Hello World");
}
}
We will finish Spring part by creating application class:
Create an application class
Path: spring-boot-serverless/src/main/java/springBootServerless/SpringBootServerlessApplication.java
package springBootServerless;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SpringBootServerlessApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
.run(SpringBootServerlessApplication.class, args);
SpringApplication}
}
At that point of time, we created basic Spring Boot Application. If
you run it using ./gradlew bootRun
you should be able to
see JSON response in the browser:
{
"success": true,
"message": "Hello World"
}
However, this app doesn't have anything in common with serverless and Lambda. We're going to address that as our next step by creating Lambda Handler.
Create AWS Lambda Handler
Lambda Handler is a class that will make Spring Boot work correctly
with AWS Lambda. You can think of it as a proxy or communication layer,
that will capture all requests and transfer them to our Spring Boot. The
SpringLambdaContainerHandler
object from AWS library is
responsible exactly for that. As you can see below it wraps
SpringBootServerlessApplication
using
getAwsProxyHandler
method to achieve this
functionality.
handleRequest
method will be called each time when AWS
Lambda function is invoked to pass user data and execute
handle
logic.
Path: spring-boot-serverless/src/main/java/springBootServerless/StreamLambdaHandler.java
package springBootServerless;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.SpringBootLambdaContainerHandler;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.amazonaws.services.lambda.runtime.Context;
public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
= SpringBootLambdaContainerHandler.getAwsProxyHandler(SpringBootServerlessApplication.class);
handler } catch (ContainerInitializationException e) {
// if we fail here. We re-throw the exception to force another cold start
.printStackTrace();
ethrow new RuntimeException("Could not initialize Spring Boot application", e);
}
}
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
.proxyStream(inputStream, outputStream, context);
handler
// just in case it wasn't closed by the mapper
.close();
outputStream}
}
We will wrap up this guide by creating serverless configuration file.
Create Serverless configuration file
This is a configuration file for serverless framework that helps us to configure AWS Lambda, AWS API Gateway, AWS IAM permissions automatically to avoid performing manual tweaks in the AWS Console hereby saving a good portion of the time.
It should be created at the root level of the project:
Path: serverless.yml
service: spring-boot-serverless
provider:
name: aws
runtime: java8
package:
artifact: build/distributions/springBootServerless.zip
functions:
springBootServerless:
handler: springBootServerless.StreamLambdaHandler::handleRequest
events:
- http:
path: /
method: get
timeout: 30
As you can see in the handler
section of the yml file we
specified earlier created handleRequest
as the main entry
point for this app.
Deploy to AWS
To deploy this application to AWS you need to run the following commands:
./gradlew build
sls deploy
By running ./gradlew build
grade will initiate a build
process where we will end up having compiled jar
packed
into springBootServerless.zip
archive. Serverless framework
will pick this archive up and deploy it to AWS. After successful
deployment, it will show created endpoint for the lambda function. In my
case it was similar to this:
Serverless: Stack update finished...
Service Information
service: spring-boot-serverless
stage: dev
region: us-east-1
stack: spring-boot-serverless-dev
api keys:
None
endpoints:
GET - Endpoint URL Goes here
functions:
springBootServerless: serverless-java-dev-springBootServerless
Serverless: Publish service to Serverless Platform...
Service successfully published! Your service details are available at:
https://platform.serverless.com/services/username/spring-boot-serverless
Serverless: Removing old service versions...
Important things to know when working with Spring and AWS Lambda
- Cloud functions are stateless. Every time lambda function is invoked internal mechanism of cloud provider spins up a container and keeps it for a while. After some idle time, it is shut down. Because of that, there is no state and all app's data is lost.
- Lambda cold starts as an issue. A cold start is an event when lambda function is executed for the first time. As I mentioned above cloud engine spins up a container thereby it "bootRuns" Spring Boot Server right after the request. So, the heavier the app the longer bootstrapping time would be. This problem can be solved by creating a service that will trigger the function periodically to keep it "warm" and by increasing AWS Lambda memory size.
- Avoid relying on shared resources. In the traditional application, there are singleton services (for HTTP Connections, DB connections, etc.) that are established once at a startup time and kept for the whole application life. Since AWS Lambda is stateless and scalable (there might be multiple instances of lambda functions) it's possible to have unexpected behaviors due too many opened connections coming from each instance of lambda function.