Quarkus is a great framework that I discovered a week ago. I was looking for ways to reduce the time of the cold-start + startup time of our Microservice running in Amazon Coretto 11 and written in our favorite programming language: Kotlin. Since the traffic on our service is very low, the response time of up to 7 seconds was clearly not going to be very interesting.
This is where Quarkus helped me. It enabled me to package my Lambdas and, thanks to GraalVM, to compile it in a native image targeting the same runtime that my Lambda would run in in AWS using Docker. GraalVM is a substrateVM and allows us to compile Ahead of Time a native image for a given platform.
According to https://www.graalvm.org:
GraalVM is a universal virtual machine for running applications written in JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Groovy, Kotlin, Clojure, and LLVM-based languages such as C and C++.
Native images compiled with GraalVM ahead-of-time improve the startup time and reduce the memory footprint of JVM-based applications.
The Problem
There were only one hiccup, Quarkus allows you to package multiple Lambdas in the same java/module but only one can be active at the time. to receive Lambda events. Meaning that if you want to use the same native-image as the binary of multiple Lambdas you will either need to compile the module for each Lambda you would like to use or simply break down your module in multiple ones with only one Lambda in each one of them.
I have tried breaking down my module into smaller ones and the compilation time of all those images increased almost tenfold even though that the nice decoupling it created was an improvement to the design. Also, a lot of duplication appeared between the new build.gradle.kts.
The (a) Solution
Quarkus allows you to include the scripts which bootstraps the native image binary, it is really well documented here: Custom bootstrap script. All you need to do is to create a file named bootstrap inside src/main/zip.native. Here is the content of my bootstrap script:
#!/usr/bin/env bash
./runner -Dquarkus.lambda.handler=$HANDLER_NAME
Thanks to the property quarkus.lambda.handler, I can tell Quarkus which Lambda to use for processing the Lambda event. And finally, since it is possible to set environment variables in for Lambdas, I simply added the following definitions to my SAM template.yaml.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
Token Payment
Parameters:
DeploymentEnvironment:
Type: String
Default: "develop"
AccountingTableName:
Type: String
Default: ""
AccountingServiceIndex:
Type: String
Default: ""
ReportingTableName:
Type: String
Default: ""
DomainSslCertificateArn:
Type: String
Default: ""
DomainName:
Type: String
Default: ""
CompilationType:
Type: String
Default: "aot"
Mappings:
HandlerMap:
jit:
AuthenticatingHandler: com.payment.authenticating.LambdaHandler::handleRequest
AccountingHandler: com.payment.accounting.LambdaHandler::handleRequest
ReportingHandler: com.payment.reporting.LambdaHandler::handleRequest
PricingHandler: com.payment.pricing.LambdaHandler::handleRequest
Runtime: java11
CodeUri: infrastructure
AreSignalHandlersDisabled: false
aot:
AuthenticatingHandler: not.used.in.provided.runtime
AccountingHandler: not.used.in.provided.runtime
ReportingHandler: not.used.in.provided.runtime
PricingHandler: not.used.in.provided.runtime
Runtime: provided
CodeUri: infrastructure/build/function.zip
AreSignalHandlersDisabled: true
Globals:
Function:
Runtime: !FindInMap [ HandlerMap, !Ref CompilationType, Runtime ]
MemorySize: 4096
Timeout: 20
AutoPublishAlias: !Ref DeploymentEnvironment
Environment:
Variables:
DISABLE_SIGNAL_HANDLERS: !FindInMap [ HandlerMap, !Ref CompilationType, AreSignalHandlersDisabled ]
Resources:
TokenPaymentHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Domain:
CertificateArn: !Ref DomainSslCertificateArn
DomainName: !Ref DomainName
Auth:
Authorizers:
BearerTokenLambdaAuthorizer:
AuthorizerPayloadFormatVersion: 2.0
EnableSimpleResponses: True
FunctionArn: !GetAtt BearerTokenAuthenticationFunction.Arn
Identity:
Headers:
- Authorization
DefaultAuthorizer: BearerTokenLambdaAuthorizer
BearerTokenAuthenticationFunction:
Type: AWS::Serverless::Function
Properties:
Handler: !FindInMap [ HandlerMap, !Ref CompilationType, AuthenticatingHandler ]
CodeUri: !FindInMap [ HandlerMap, !Ref CompilationType, CodeUri ]
Environment:
Variables:
HANDLER_NAME: authenticating
GetProcessingPriceFunction:
Type: AWS::Serverless::Function
Properties:
Handler: !FindInMap [ HandlerMap, !Ref CompilationType, PricingHandler ]
CodeUri: !FindInMap [ HandlerMap, !Ref CompilationType, CodeUri ]
Events:
GetProcessingPrice:
Type: HttpApi
Properties:
ApiId: !Ref TokenPaymentHttpApi
Path: /pricing/v1/token/{processing_price}/{number_of_units}
Method: GET
Environment:
Variables:
HANDLER_NAME: pricing
PayBillFunction:
Type: AWS::Serverless::Function
Properties:
Handler: !FindInMap [ HandlerMap, !Ref CompilationType, AccountingHandler ]
CodeUri: !FindInMap [ HandlerMap, !Ref CompilationType, CodeUri ]
Events:
PayBill:
Type: HttpApi
Properties:
ApiId: !Ref TokenPaymentHttpApi
Path: /accounting/v1/bill/{bill_id}/pay
Method: PATCH
Environment:
Variables:
HANDLER_NAME: accounting
ReportProcessingServiceFunction:
Type: AWS::Serverless::Function
Properties:
Handler: !FindInMap [ HandlerMap, !Ref CompilationType, ReportingHandler ]
CodeUri: !FindInMap [ HandlerMap, !Ref CompilationType, CodeUri ]
Events:
ReportProcessing:
Type: HttpApi
Properties:
ApiId: !Ref TokenPaymentHttpApi
Path: /reporting/v1/processing
Method: POST
Environment:
Variables:
HANDLER_NAME: reporting
It is important that you do not forget to set your bootstrap file as executable.
Conclusion
I was able thanks to Quarkus to generate a native image for Rapid Linux using GraalVM for my four Lambdas and reduce the response time of my cold starts + first invocation by a up to a factor of 10.