Spring Cloud Function Example for AWS Lambdas

Published
Updated

The Spring Cloud Function library combined with Spring Boot is an excellent way to create an application framework which can run quickly and easily within an AWS Lambda. This article will guide you on how to create a Spring Cloud Function and deploy this application within an Amazon AWS Lambda function.

Getting Started with Spring Cloud Functions

The Spring Cloud Function library allows you to create functional applications all while retaining the benefits of provided by Spring Boot: Autowiring, Dependency Injection, auto-configuration, etc. They follow the standard Java 8 functional support with Supplier, Function, and Consumer support.

Thanks to the ‌spring-cloud-function-adapter-aws library, it is easy to integrate your app directly into Amazon’s AWS ecosystem. It provides native interactions for handling requests, streams, transparent type conversion, and even JSON serialization / deserialization which you may not otherwise have available using an out of the box AWS SDK implementation.

Spring Cloud Function Supplier and Consumer example REST HTTP methods

Creating a Spring Cloud Function for AWS Lambda

To start, you will want to create a boilerplate Spring Boot application class that defines a Main method and uses the FunctionalSpringApplication entry point:

package com.codetinkering.example;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.context.FunctionalSpringApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class LambdaApp {

    public static void main(String[] args) {
        FunctionalSpringApplication.run(LambdaApp.class, args);
    }

    @Bean
    public ExampleFunction exampleFunction() {
        return new ExampleFunction();
    }

    @Bean
    public ExampleConsumer exampleConsumer() {
        return new ExampleConsumer();
    }

    @Bean
    public ExampleSupplier exampleSupplier() {
        return new ExampleSupplier();
    }
}

Here is the pojo bean class code:

package com.codetinkering.example;

import lombok.Data;

@Data
public class MyPojo {

    private String exampleField;

    public MyPojo() { }

    public MyPojo(String field) {
        this.exampleField = field;
    }
}

Next you will want to create a class function which will actually perform the function of your application. This may be any business logic or overall function intended by your application. Implement the java Function interface and specify the parameters you are expecting to ingest and return. In this example, I am providing a simple bean called MyPojo and returning a String. It will take a field from the bean and return the uppercase value of the String.

It should be noted that Spring will automatically handle JSON serialization and injection into the bean for you so you do not need to do any parsing or formatting. Additionally, this also applies to the return value, you can return a POJO or bean class that will automatically be converted into JSON.

package com.codetinkering.example;

import lombok.Data;
import org.springframework.stereotype.Component;

import java.util.function.Function;

@Component
public class ExampleFunction implements Function<ExampleFunction.MyPojo, String> {

    @Override
    public String apply(MyPojo myPojo) {
        // Do something in your lambda function here
        return myPojo.getExampleField().toUpperCase();
    }
}

Spring Cloud Supplier Example

The following code creates a functional Spring Cloud Supplier (basically a Spring Cloud Function that takes no input). This implementation is useful if you have a GET REST endpoint which provides data.

package com.codetinkering.example;

import org.springframework.stereotype.Component;

import java.util.function.Supplier;

@Component
public class ExampleSupplier implements Supplier<MyPojo> {

    @Override
    public MyPojo get() {
        return new MyPojo("Example Data");
    }
}

Spring Cloud Consumer Example

Though not as frequently used, Spring Cloud Consumers can be used to consume an input but not produce an output. This can oftentimes be helpful for forms which are consumed or other tasks which may not generate a result for the front-end.

package com.codetinkering.example;

import org.springframework.stereotype.Component;

import java.util.function.Consumer;

@Component
public class ExampleConsumer implements Consumer<MyPojo> {

    @Override
    public void accept(MyPojo myPojo) {
        System.out.println("Received: " + myPojo.getExampleField());
    }
}

Multiple Spring Cloud Functions in one project

In an AWS Lambda, only one function is intended to be used at a time. Rather than having multiple projects defining their own function, we can bundle them together in the same application and use a property file to specify which function is to be used by the Lambda.

spring.cloud.function.definition=exampleFunction

In effect, you can have the same project deployed multiple times (across multiple lambdas) with each corresponding to a specific endpoint. You can then use Spring profiles to define which Lambda uses what function.

Configuring your Lambda Application Maven Pom.xml

The following Maven Pom.xml contains all of the needed components and build plugins for your application. It uses a Spring Boot Starter parent, along with a build configuration targeting Java 1.8. Finally, it has all of the needed Maven build plugins for building a shaded jar and uses the Spring-Boot-Thin-Layout plugin to strip down the size of your application.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/>
    </parent>

    <groupId>com.codetinkering</groupId>
    <artifactId>aws-lambda-java-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
            <version>3.1.2</version>
        </dependency>

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <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>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>

                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>

                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.codetinkering.example.LambdaApp</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>

                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.26.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

Building your Lambda Application

You can build your application using the following maven commands:

mvn clean package 

This will build your application into a JAR file that you can deploy into AWS for your Lambda. The following steps will show you how to deploy it to AWS.

Building your cloud function using Spring Native

See my full article here on how to configure your Spring Cloud function to be built into a native Lambda image or application using GraalVM.

Deploying your Spring Cloud Function to an AWS Lambda

Sign into your Amazon AWS Console and navigate to the AWS Lambda service page. From this dashboard, click the button to create a new Lambda Function and select the option to author from scratch.

AWS Lambda Console create function from scratch

You will need to provide a name for your lambda. Do so in the Function Name box and make it descriptive enough that you will remember what it does should you add more Lambdas later. Additionally, for the AWS Lambda Runtime, pick Java 8 or Java 11. Either are acceptable for this application. Finally, click the Create Function button to continue.

AWS Lambda Console create function parameters for Java and Function Name

Upload your application JAR file to the AWS Lambda

You should now have an empty Lambda created within AWS and it should look something like the following:

AWS Lambda function created screen

Simply click the upload from button and navigate to the target folder of your project. Select the JAR file and upload it to AWS by pressing the Save button. If your JAR file is large, it may be more ideal to store your JAR file in S3 and reference it there instead of uploading it via the Console each time you redeploy.

Upload a Spring Cloud Function JAR file to an AWS Lambda

Configuring Lambda Handler for Spring Cloud Function

Now that your application has been uploaded into AWS, Amazon needs to understand how to properly invoke our Spring Cloud Function. Spring has a class called FunctionInvoker and a generic handler method called handleRequest which is provided by the spring-cloud-function-aws-adapter library.

On the Runtime Settings pane for your Lambda, click the Edit button and change the Handler field to the following. Then click save.

org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest

Amazon configure Lambda function handler for Spring Cloud

Testing your Spring Cloud Function Lambda

Click the test tab and provide the following JSON into your event:

{
    "exampleField": "test-value"
}

Spring will automatically serialize the data into your object and it will be available in your Lambda function code.

AWS Test your lambda user provided input

Click the test button to run your lambda and see the results of your Spring Application’s return value:

AWS Lambda execution results for Spring Cloud Function Succeeded

Note the execution times, memory, and billed durations. Subsequent calls to this lambda will take significantly less time as the application would have already been started and the caches would be warm within the MicroVMs of AWS. You can adjust memory usage up or down to better fit your applications needs from the configuration tab as well.

Lambda Configuration, Monitoring and Logging

You can do a number of different things from here, you can create an ALB (Application Load Balancer) and link it via a trigger to your Lambda so that it can be accessed via the web. Otherwise your Lambda will not be accessible to traffic via AWS.

You can also monitor the logs of the execution and statistics via the Monitor panel in AWS to see various stats and log entries.

AWS Lambda CloudWatch metrics and Spring Cloud Function application logs

Failed to lookup function to route based on the value of ‘spring.cloud.function.definition’ property when defining multiple Spring Cloud functions

This can happen if you have multiple cloud functions defined within the same project. If your project is structured this way, you will need to define in your application properties which function you want to use for the lambda (there is a limit of one function per lambda). Do so by adding the following line in your properties file:

spring.cloud.function.definition=exampleFunction

Note the casing of the function name, this aligns with the bean definition for your function, not the class name. In the above example I define my bean as exampleFunction even though it is a class of ExampleFunction.

Checkout this project from Github

git clone https://github.com/code-tinkering/aws-lambda-java-example
Download Zip