AWS Lambda is a great technology to get your code up and running in a matter of minutes. I’m heavily using AWS Lambda for various automation and marketing tasks for this blog and my online courses. I recently gave Spring Cloud Function with Kotlin for AWS Lambda a try. It’s a powerful combination if you’re familiar with the Spring ecosystem and want to use the same techniques for your Lambda functions. With this blog post, we go through the necessary steps to deploy a Kotlin Spring Boot application to AWS Lambda using Spring Cloud Function.

Maven Project Setup for Spring Cloud Function

The baseline for our project is a Spring Boot with Kotlin skeleton project that we generate at start.spring.io. When generating this project, we add the Spring Reactive Web (WebFlux) dependency to the mix to get access to the WebClient .

On top of these basic Spring Boot starter dependencies, we need the following Spring Cloud Function and AWS Lambda-related dependencies for our project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Spring Cloud Function -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-web</artifactId>
  <version>3.2.0-M2</version>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-aws</artifactId>
  <version>3.2.0-M2</version>
</dependency>

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

While Spring Cloud Function works perfectly with a Java codebase, there were some issues for Kotlin Spring Boot projects. These have been solved with version 3.2.0-M2 of Spring Cloud Function.

JAR Layout for AWS Lambda

Next, we have to tweak the .jar layout to run our Spring Boot project on AWS Lambda.

When deploying our Kotlin project to AWS Lambda, we can only select a single .jar file. Hence our build artifact must contain both our source code as well as all the dependencies. That’s why we need to shade all our dependencies into a single .jar file to create an uber jar.

For this purpose, we add the maven-shade-plugin to the build section of our pom.xml :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.2.4</version>
  <configuration>
    <createDependencyReducedPom>false</createDependencyReducedPom>
    <shadedArtifactAttached>true</shadedArtifactAttached>
    <finalName>${project.artifactId}</finalName>
    <shadedClassifierName>aws</shadedClassifierName>
  </configuration>
</plugin>

Depending on how many dependencies we add to our project, the size of our build artifact will be multiple megabytes. As we have to upload the build artifact on each deployment to AWS, we should try to keep our .jar file as thin as possible.

That’s why we add the spring-boot-thin-layout dependency to our spring-boot-maven plugin configuration. This experimental thin layout dependency will create a small executable .jar file :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot.experimental</groupId>
      <artifactId>spring-boot-thin-layout</artifactId>
      <version>${wrapper.version}</version>
    </dependency>
  </dependencies>
</plugin>

For our project example, the thin layout helps reduce the final size .jar from 51MB to 27MB. The smaller we get our build artifact, the faster our deployments are.

AWS Lambda Implementation with Spring Cloud Function and Kotlin

With Spring Cloud Function, we can run any code as an AWS Lambda function that fits the Java FunctionalInterface specification. Common choices are the Function , Consumer or Supplier interface from java.util.function .

Depending on our use case, we choose one of these interfaces:

  • our AWS Lambda function consumes and produces a value (e.g., REST API): Function
  • we only process incoming values but don’t return anything (e.g., event-based): Consumer
  • we don’t take any input and only process information to return a value (e.g., cron-based): Supplier

For showcasing purposes, let’s implement a background job that frequently fetches data from a remote API.

As we stay within the Spring ecosystem, we can use the well-known WebClient for HTTP communication. In this example, we fetch a daily random quote from a public REST API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
class WebClientConfig {

  @Bean
  fun randomQuotesWebClient(webClientBuilder: WebClient.Builder): WebClient {
    val httpClient = HttpClient.create()
      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4_000) // millis
      .doOnConnected {
        it.addHandlerLast(ReadTimeoutHandler(4)) // seconds
        it.addHandlerLast(WriteTimeoutHandler(4)) //seconds
      }

    return webClientBuilder
      .baseUrl("https://quotes.rest/")
      .clientConnector(ReactorClientHttpConnector(httpClient))
      .build()
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
class FunctionConfiguration(
  private val randomQuotesWebClient: WebClient
) {

  @Bean
  fun fetchRandomQuote(): (Message<Any>) -> String {
    return {
      // our code to run on AWS Lambda
    }
  }
}

As seen above, we use Spring’s dependency injection mechanism for our AWS Lambda function and inject the pre-configured WebClient . We mark our Function ( (Message<Any>) -> String ) with @Bean to make it discoverable for Spring Cloud Function.

Spring Cloud Function wraps the incoming payload and headers as a org.springframework.messaging.Message . If we want access to the AWS Lambda Context, for example, we can extract this object from the headers and get access to the Lambda logger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Bean
fun fetchRandomQuote(): (Message<Any>) -> Unit {
  return {

    val awsContext = it.headers["aws-context"] as Context
    val logger = awsContext.logger

    logger.log("Going to fetch a random quote")

    val response = randomQuotesWebClient
      .get()
      .uri("/qod?language={language}", "en")
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(JsonNode::class.java)
      .block()

    val quote = response?.get("contents")?.get("quotes")?.get(0)?.get("quote")
    val author = response?.get("contents")?.get("quotes")?.get(0)?.get("author")

    "Quote of the day: $quote from $author"
  }
}

This implementation fetches a random quote and returns it to the caller of our Spring Cloud AWS Lambda function.

Serverless AWS Lambda Setup

We can choose from a variety of tools and technologies to deploy our function to AWS Lambda. AWS offers at least four different solutions: uploading the code via the web console, CloudFormation, AWS CDK, and AWS SAM (Serverless Application Model).

Various other tools allow a unified deployment of functions to typical FaaS providers. One of these tools is the Serverless Framework that we’re going to use for this article.

Discussing the pros and cons of each deployment mechanism is out of scope for this article. For an introduction to the Serverless framework, consider one of the previous articles on Java and AWS Lambda.

In short, Serverless abstracts the underlying AWS resources, and we declare our function deployment inside a serverless.yml file. Plus, we get a CLI to create, remove, invoke, and filter logs for our functions.

The three most important configuration values for our AWS Lambda function are:

  • the fully qualified class name of the Java handler class that AWS Lambda should invoke
  • the location of our build artifact (our uber .jar )
  • how and when to invoke the function (e.g., cron-based, event-based, behind an API Gateway, manually, etc.)

For our AWS Lambda example, we configure these configuration values as part of the serverless.yml file at the root of our project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
service: aws-kotlin-example

provider:
  name: aws
  runtime: java11
  stage: production
  region: eu-central-1
  timeout: 120
  memorySize: 1024
  lambdaHashingVersion: 20201221

package:
  artifact: target/spring-cloud-function-kotlin-aws.jar

functions:
  fetch-random-quotes:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker
    description: Showcasing Spring Cloud Function for AWS, Kotlin and Spring Boot
    events:
      - schedule: rate(5 minutes)
    environment:
      SPRING_CLOUD_FUNCTION_DEFINITION: fetchRandomQuote

As the handler function, we configure the FunctionInvoker class from the Spring Cloud Function AWS adapter. This acts as an entry point to deserialize the incoming payload, invoke our implementation and serialize our response.

Using the SPRING_CLOUD_FUNCTION_DEFINITION environment variable, we specify the name of our function bean that we want to invoke with this AWS Lambda. We can leverage this environment variable and implement various AWS Lambda functions within the same project.

When using Kotlin, we have to set this variable even though there’s only one suitable handler.

Deploying the AWS Lambda with Serverless

What’s left is to deploy the AWS Lambda with Serverless.

For the following command to work, make sure to install and configure the Serverless framework.

1
2
mvn package
serverless deploy --aws-profile your-aws-profile

Once the deployment is finished, we can either wait for the function to run automatically (based on the cron expression) or trigger it manually. With Serverless, we can invoke the deployed function with a single command from:

1
2
3
4
$ serverless invoke -f fetch-random-quotes --aws-profile your-aws-profile

Serverless: Running "serverless" installed locally (in service node_modules)
"Quote of the day: \"If you have dreams it is your responsibility to make them happen.\" from \"Bel Pesce\""

As an alternative, we can also manually trigger the AWS Lambda function within the AWS web console.

The first invocation of our Kotlin AWS Lambda function takes some seconds as it’s a cold start. Every subsequent invocation (and assuming our AWS Lambda environment is still running) is faster.

For this sample project, the function invocation with a cold start takes about 6 seconds, and each following invocation 2 – 3 seconds:

1
2
REPORT Duration: 6388.71 ms Billed Duration: 6389 ms Memory Size: 1024 MB Max Memory Used: 281 MB Init Duration: 6245.84 ms # cold start
REPORT Duration: 2498.83 ms Billed Duration: 2499 ms Memory Size: 1024 MB Max Memory Used: 301 MB 

(Remember we’re reaching out to a remote API which slows down our operation)

To remove the Kotlin Spring Cloud function from our AWS account, we can run the following command:

1
serverless remove --aws-profile your-aws-profile

This command will also clean up any additional CloudFormation resources.

Tweaks for our Kotlin AWS Lambda Function

When we operate our AWS Lambda function in production, we want to know when things break.

The Serverless Framework comes with a plugin ecosystem for which A Cloud Guru published a plugin to create CloudWatch alerts for our AWS Lambda function with ease.

We install this plugin with npm :

1
npm install serverless-plugin-aws-alerts

Once installed, we can configure an alert to inform us whenever there’s an invocation error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins:
  - serverless-plugin-aws-alerts

# ...

custom:
  alerts:
    stages:
      - production
    definitions:
      functionErrors:
        period: 300 # evaluate every 5 minutes
    topics:
      alarm:
        topic: ${self:service}-production-alerts-alarm
        notifications:
          - protocol: email
            endpoint: your@email.com
    alarms:
      - functionErrors

The configuration above creates an Amazon CloudWatch alarm that will send an email on the first invocation error. CloudWatch will check the status of our AWS Lambda every five minutes.

While this is a basic example of the AWS Alerts Plugin, the plugin allows to monitor further error scenarios (e.g., throttles & duration) and configure custom setups. We create this alert by redeploying our Kotlin AWS Lambda function with Serverless.

Another useful tweak for our Kotlin AWS Lambda Serverless setup is to set the log retention to 7 or 14 days. By default, the logs won’t expire and will remain in Amazon CloudWatch forever (or at least until we delete them):

1
2
3
provider:
  # ...
  logRetentionInDays: 7

As part of the Serverless Framework, we can even fetch the logs for our deployed AWS Lambda function without leaving our shell:

1
sls logs -f fetch-random-quotes --startTime 10m --aws-profile your-aws-account

On top of this, we can write tests for our AWS Lambda function like for any other Spring Boot application. As our function has a well-defined interface, we can test the happy-path, outages of the remote API, or slow responses:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class FunctionConfigurationTest {

  private lateinit var mockWebServer: MockWebServer
  private lateinit var functionConfiguration: FunctionConfiguration

  @BeforeEach
  fun setUp() {
    mockWebServer = MockWebServer()
    mockWebServer.start()

    functionConfiguration = FunctionConfiguration(
      WebClient.builder().baseUrl(mockWebServer.url("/").toString()).build()
    )
  }

  @Test
  fun `should return quote with author on successful API response`() {

    val mockResponse = MockResponse()
      .addHeader("Content-Type", "application/json")
      .setBody(FunctionConfigurationTest::class.java.getResource("/stubs/successful-quote-response.json").readText())

    mockWebServer.enqueue(mockResponse)

    val result = functionConfiguration
      .fetchRandomQuote()(GenericMessage("", MessageHeaders(mapOf("aws-context" to TestLambdaContext()))))

    assertThat(result)
      .contains("Bel Pesce")
  }
}

For the test above, we use the MockWebServer from OkHttp to locally simulate the remote REST API. This is a common recipe when writing tests that involve an HTTP client to avoid extensive mocking.

Spring Cloud Function for Kotlin and AWS Lambda Summary

I can already hear you scream that this tech setup is way too much overhead for AWS Lambda. A cold start requires a JVM start and launching our Spring context. That’s one of the main cons of this approach. However, IMHO this is negligible if our use case isn’t a request-response pattern that relies on fast responses. For background jobs or event-driven functions, this is usually not the case.

While this example is also dead simple, there are many knobs to turn to tweak the performance of our Lambda function: warmup plugin, tiered compilation, AWS Java SDK improvements, etc.

On top of this, we can reduce the initial startup time by going native and compiling our function to a native binary with Spring Native. That’s something I’ll investigate next and blog about. Stay tuned!

One of the biggest pros I see is staying within the well-known Spring ecosystem and using the tools and techniques from developing Spring Boot applications.

Using the Serverless Framework, we have our function up and running in a matter of minutes coupled with a CLI to operate our functions with ease.

For further recipes on how to use Java on AWS Lambada, take a look at the following articles:

The source code for this AWS Lambda with Kotlin and Spring Cloud Function example is available on GitHub.

Have fun deploying your Kotlin functions to AWS Lambda with Spring Cloud Function,

Philip

Reference https://rieckpil.de/aws-lambda-with-kotlin-and-spring-cloud-function/