Kotlin on AWS Lambda

Elena van Engelen - Maslova
PostNL Engineering
Published in
6 min readSep 8, 2022

--

Our journey started just over a year ago at PostNL when our software engineering team was formed. We chose Kotlin as a backend programming language because it is a very expressive and concise language that supports lightweight concurrency that make it faster with smaller footprint. Additionally it supports multiple platforms: JVM, JS and Native , we shall dive deeper into why this is beneficial for running on AWS Lambda later in this blog.

Although JVM is a typical platform for Kotlin, Kotlin is supports multiple platforms which gives it an advantage when running on AWS Lambda, specifically when reducing cold start and improving performance are important. In this blog we shall look at different platforms and optimisations Kotlin supports and see how this compares when running on AWS Lambda. Specifically we’ll be looking at AWS Lambda Java rumtime with Kotlin/JVM, Kotlin/JVM with C1 compiler ; AWS NodeJS Runtime with Kotlin/JS ; Custom runtime with Kotlin/GraalVM Native.

Kotlin / JVM

JVM is the most commonly used platform for Kotlin. We can create an AWS Lambda using Kotlin/JVM by selecting latest version of Java Runtime for AWS Lambda, while using the latest version of Kotlin (currently 1.7). Kotlin is agnostic of Java runtime on AWS Lambda which means we can upgrade Kotlin version without needing to wait for AWS Lambda runtime upgrades. Our performance tests showed that ARM64 architecture improves efficiency and performance of Kotlin AWS Lambdas. The following snippet of Typescript CDK code sets up a Koltin AWS Lambda on JVM:

const koltinLambdaJVM = new lambda.Function(this, 'koltinLambdaJVM', {
description: 'Kotlin/JVM AWS Lambda ',
runtime: lambda.Runtime.JAVA_11,
architecture: lambda.Architecture.ARM_64,
code: lambda.Code.fromAsset('../build/dist/my-kotlin-lambda.zip'),
handler: 'nl.postnl.kotlin.lambda.KotlinLambda::handleRequest',
timeout: Duration.seconds(240),
memorySize: 2048,
logRetention: logs.RetentionDays.THREE_MONTHS
});

Kotlin / JVM C1 compiler

Although JVM is one of the fastest and most efficient runtimes, it does cause a cold start. One of the ways to reduce a cold start according to AWS Compute Blog is to use JVM C1 compiler. Lets update our Kotlin/JVM lambda to use C1 compiler by setting tiered compilation to 1 in our CDK:

const koltinLambdaJVM_C1 = new lambda.Function(this, 'koltinLambdaJVM_C1', {
description: 'Kotlin/JVM C1 AWS Lambda ',
runtime: lambda.Runtime.JAVA_11,
architecture: lambda.Architecture.ARM_64,
code: lambda.Code.fromAsset('../build/dist/my-kotlin-lambda.zip'),
handler: 'nl.postnl.kotlin.lambda.KotlinLambda::handleRequest',
timeout: Duration.seconds(240),
memorySize: 2048,
environment: {
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1",
},
logRetention: logs.RetentionDays.THREE_MONTHS
});

When we run the AWS Lambda we can see the log line which advises us that JVM option has been applied:

CloudWatch log
CloudWatch

Before we compare our two JVM AWS Lambdas, let’s create the JS and GraalVM Native variants so we can compare them all.

Kotlin / JS

Kotlin /JS AWS Lambda runs on AWS Lambda NodeJS runtime. This also means that writing code is a somewhat different from Kotlin/JVM. Although existing Kotlin/JS or Kotlin/Multiplatform libraries can be used seamlessly, using third party JS libraries such as AWS SDK requires metadata (externals) for this to work. Metadata can be generated. Example of a generated external from AWS SDK for Dynamo looks like this:

DynamoDB.kt

Then we can deploy our Kotlin/JS AWS Lambda with CDK as follows:

const lambdaFunction = new lambda.Function(this, 'kotlin-js-lambda-function', {
description: "Kotlin Js Handler",
runtime: lambda.Runtime.NODEJS_16_X,
architecture: lambda.Architecture.ARM_64,
timeout: Duration.seconds(240),
memorySize: 2048,
handler: 'kotlin-experiments-software.handleRequest',
code: lambda.Code.fromAsset(path.join(__dirname, '../../build/dist/kotlin-js.zip')),
environment: {
REGION: Stack.of(this).region,
},
logRetention: logs.RetentionDays.THREE_MONTHS
});

Kotlin / GraalVM Native

Firstly, GraalVM native uses ahead of time compilation, which means reflection does not work out of the box. External libraries that do not provide GraalVM Native Image Support would typically need (generated) metadata added to src/main/resources/META-INF/native-image. Thankfully AWS SDK for Java (from v 2.16.1) has GraalVM Native Image support, we can use this from Kotlin code seamlessly. However for libraries that do not include native image metadata, we need to provide metadata ourselves. Example of such metadata is Kotlin coroutines, whereby we needed to add the following in src/main/resources/META-INF/native-image/org.jetbrains.kotlinx/kotlinx-coroutines-core:

reflect-config.json

In order to run Kotlin AWS Lambda on GraalVM Native we also need to set up a custom runtime as AWS Lambda does not provide out of the box runtime for it. GraalVM Docker runtime is already available (for more information refer to aws samples):

const graalVMNativeLambda = new lambda.Function(this, 'graalVMNativeLambda', {
description: 'GraalVM Native Kotlin lambda',
runtime: lambda.Runtime.PROVIDED_AL2,
code: lambda.Code.fromAsset('../software/',
{
bundling: {
image: DockerImage.fromRegistry("marksailes/al2-graalvm:al2-21.2.0"),
volumes: [{
hostPath: process.env.HOME + "/.m2/",
containerPath: "/root/.m2/"
}],
user: "root",
outputType: BundlingOutput.ARCHIVED,
command: ["-c",
"cd products " +
"&& mvn clean install -P native-image "
+ "&& cp /asset-input/products/target/function.zip /asset-output/" ]
}
}),
handler: 'nl.postnl.kotlin.lambda.KotlinLambda::handleRequest',
timeout: Duration.seconds(120),
memorySize: 2048,
logRetention: logs.RetentionDays.THREE_MONTHS
});

The comparison

The choice which platform to use for Kotlin on AWS Lambda can have different factors. Easiest to develop is the JVM variants (JVM and JVM with c1 compiler), with JS and GraalVM requiring more work due to externals and meta data respectively. Let’s compare the cold start and average performance before drawing conclusion on possible factors contributing to the choice of platform.

Bench mark

Before starting the comparison we need to have a bench mark. There is no point in comparing HelloWorld code, so we are going to take production like code example with parallel scans on DynamoDB table with 350K of data items and Kotlin coroutines and deploy it to each platform variant. We are also going to use Lambda Power Tuner to compare on different AWS Lambda memory size.

Cold start

Cold start comparison

Cold start clear winner is Kotlin on GraalVM Native with JVM variant as slowest variant.

Average Speed

Execution time comparison

Once lambda is warm then JVM and GraalVM Native are the fastest. Notice how Kotlin/JS and Kotlin/JVM c1 loose in performance to Kotlin/JVM after the cold start.

Conclusion

If cold starts are not an issue then JVM platform would be the simplest choice. Whether or not we should use JVM C1 compiler optimisation depends on whether the lambda has frequent cold starts. For instance, scheduled AWS Lambda that runs once an hour would benefit from this one liner setting, however an AWS Lambda with DynamoDB Stream event source of a DynamoDB table where data is frequently updated would be more efficient on a standard JVM.

If there is an end user waiting for a lambda response, then GraalVM native would be worth the effort. Whereas Koltin/JS could be used for Lambda@Edge which supports only Node.js and Python runtimes .

If we create shared libraries with metadata for team’s frequently used third party libraries then Kotlin/GraalVM Native development becomes as easy as with Kotlin/JVM. Same can be said about Kotlin/JS and externals it needs in order to use third party NodeJS libraries. Both platforms Kotlin/GraalVM Native and Kotlin/JS need some up front investment of time & effort in order to keep development time down.

--

--