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 the scope of 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 (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 takes 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 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 minutes, coupled with a CLI to operate our functions easily.
For further recipes on how to use Java on AWS Lambada, take a look at the following articles:
- Java AWS Lambda Container Image Support (Complete Guide)
- AWS Lambda Example with Java, Serverless, and Maven
- Java AWS Lambda Functions with Spring Cloud Function
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