Overview
- In cloud computing, serverless functions have become an integral part of the backend architecture. One downside in the JVM ecosystem has been the slow Cold Start issue, which has led to its exclusion from the choice of cloud serverless function execution, including AWS Lambda. However, in recent years, there have been significant changes and developments to dramatically improve this. Azul released the
CRaC API
, which, in collaboration with AWS, eventually evolved into the AWS Lambda SnapStart
feature. Also, the emergence of lightweight and fast toolkits like http4k
, a robust alternative to the traditionally heavy Spring Boot for function deployment, along with the advent of GraalVM
images, has begun to fundamentally address the issue of Cold Start.
- This article is inspired by Faster Kotlin APIs on AWS Lambda by Andrew O'Hara (a contributor to http4k), and summarizes how to write and deploy a super-fast AWS Lambda Function using Kotlin based on JVM 21 runtime.
Strategies
- Apply every possible technology and tuning to reduce the
Cold Start
delay, a chronic issue of the JVM, without compromising on development convenience.
- Build using the ultra-lightweight
http4k
toolkit, optimized for serverless computing, instead of the heavy Spring Boot framework.
- Activate
SnapStart
feature and enable the C1 compiler by adding the JAVA_TOOL_OPTIONS=-XX:+TieredCompilation -XX:TieredStopAtLevel=1
option.
- Find a compromise between billing and minimal latency to set an appropriate memory size.
Project Creation
- Launch IntelliJ IDEA and create a new Kotlin project as follows.
Launch IntelliJ IDEA
→ New → Project
→ Name: hello-lambda (enter any project name)
→ Language: [Kotlin] selected
→ Build system: [Gradle] selected
→ Gradle DSL: [Groovy] selected
→ [Create]
build.gradle
- Replace the build.gradle file in the project root with the following content.
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
classpath "com.github.johnrengelman:shadow:8.1.1"
}
}
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
id 'application'
id "com.github.johnrengelman.shadow" version "8.1.1"
}
mainClassName = "com.example.HelloWorldKt"
group = 'org.example'
version = '1.0-SNAPSHOT'
compileKotlin.kotlinOptions.jvmTarget = "21"
tasks.register('buildLambdaZip', Zip) {
from compileKotlin
from processResources
into('lib') {
from configurations.compileClasspath
}
}
shadowJar {
manifest.attributes["Main-Class"] = mainClassName
archiveBaseName.set(project.name)
archiveClassifier.set(null)
archiveVersion.set(null)
mergeServiceFiles()
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
implementation("org.http4k:http4k-aws:5.12.1.0")
implementation("org.http4k:http4k-core:5.12.1.0")
implementation("org.http4k:http4k-format-jackson:5.12.1.0")
implementation("org.http4k:http4k-serverless-core:5.12.1.0")
implementation("org.http4k:http4k-serverless-lambda:5.12.1.0")
implementation("org.http4k:http4k-serverless-lambda-runtime:5.12.1.0")
implementation("com.amazonaws:aws-lambda-java-events:3.11.4")
}
test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
Implementing AwsLambdaEventFunction
- Below is an example of implementing an AwsLambdaEventFunction that receives and processes events triggered from Amazon S3. (http4k also provides an
ApiGatewayV1LambdaFunction
interface capable of handling requests from Amazon API Gateway.)
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.events.S3Event
import org.http4k.client.JavaHttpClient
import org.http4k.core.HttpHandler
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.server.SunHttp
import org.http4k.server.asServer
import org.http4k.serverless.AwsLambdaEventFunction
import org.http4k.serverless.FnHandler
import org.http4k.serverless.FnLoader
fun eventFnHandler(http: HttpHandler) =
FnHandler { s3Event: S3Event, context: Context ->
s3Event.records.forEach {
http(
Request(Method.GET, "{webhook-url}")
.query("eventSource", it.eventSource)
.query("eventName", it.eventName)
.query("eventTime", it.eventTime.toString())
.query("bucket", it.s3.bucket.name)
.query("key", it.s3.`object`.key)
.query("size", it.s3.`object`.sizeAsLong.toString())
)
}
"Hello World"
}
fun eventFnLoader(http: HttpHandler) = FnLoader { _: Map<String, String> ->
eventFnHandler(http)
}
val httpServer: HttpHandler = {
Response(Status.OK).body("Hello World")
}
fun main() {
httpServer
.asServer(SunHttp(8080))
.start()
.block()
}
class HelloAwsLambdaEventFunction : AwsLambdaEventFunction(eventFnLoader(JavaHttpClient()))
Building AWS Lambda Function
- Build the written project into a .zip file as follows.
$ ./gradlew buildLambdaZip
Creating and Deploying AWS Lambda Function
- Create and deploy a lambda function based on Java 21 runtime using the previously built .zip file.
$ aws lambda create-function \
--function-name helloLambda \
--runtime "java21" \
--environment "Variables={JAVA_TOOL_OPTIONS=-XX:+TieredCompilation -XX:TieredStopAtLevel=1}" \
--zip-file fileb://build/distributions/hello-lambda-1.0-SNAPSHOT.zip \
--handler com.example.HelloAwsLambdaEventFunction::handleRequest \
--role {role-arn} \
--memory-size 2048 \
--snap-start ApplyOn=PublishedVersions \
--publish
$ aws lambda update-function-code \
--function-name helloLambda \
--zip-file fileb://build/distributions/hello-lambda-1.0-SNAPSHOT.zip \
--publish
Invoking AWS Lambda Function
- Invoke the deployed Lambda Function as below to confirm its proper operation.
$ aws lambda invoke --function-name helloLambda --cli-binary-format raw-in-base64-out --payload file://input.json output.json
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
$ cat output.json
Hello World
Final Thoughts
- The final cold start time for a dummy function, after using the lightweight module for AWS Lambda from the http4k toolkit, activating SnapStart, optimizing JVM C1 environment variables, and setting the memory to 2048 MB, is 100.36ms, and the warm start time is 1.43ms. This is a significant improvement compared to the initial startup time of traditional Spring Boot-based applications. Now, I feel confident in writing Kotlin-based AWS Lambda Functions.
References and Further Reading