arrow-rightgithublinkedinscroll-to-topyoutubezig-zag

A complete guide for running Spring Boot MVC in the cloud using AWS Lambda

Last Updated On

Table of contents

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

Spring Boot setup

To skip setup:

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 {
        springBootVersion = '2.0.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


// Task for building the zip file for upload
task buildZip(type: Zip) {
    // 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
    baseName = "springBootServerless"
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtime
    }
}

build.dependsOn buildZip

task wrapper(type: Wrapper) {
    gradleVersion = '4.7'
}

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) {
        SpringApplication.run(SpringBootServerlessApplication.class, args);
    }
}

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 {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(SpringBootServerlessApplication.class);
        } catch (ContainerInitializationException e) {
            // if we fail here. We re-throw the exception to force another cold start
            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);

        // just in case it wasn't closed by the mapper
        outputStream.close();
    }
}

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.