Serverless AWS with Kotlin, Gradle, and CDK
Recently I have been making the transition to a severless architecture mindset for several of my mobile apps. One app in particular has a backend that is composed of a single rest call. This call is triggered by a developer each time and updates are then sent to mobile clients. To share codebases with the mobile side I had a KTOR server instance and was renting a server to host it.
I came across Lambda while exploring serverless options and found it would be a perfect fit for this use case. A single lambda function can be setup quickly and with the current volume of trigger requests it would be cheaper than an entire physical instance. API Gateway could sit on the front end and configure the exact rest scheme and any load balancers. The desired architecture would look like this:
While setting this up I came across AWS CDK. The cloud development kit lets you configure your stack with code using a language of your choice. Since I already had a pure Kotlin project and the CDK offers bindings for Java it was an easy choice to make. Kotlin and gradle are not officially supported by the cloud toolset provided by AWS but with some tweaking it is very possible to get working and wrap your head around. This post will walk you through setting up a CDK stack using Kotlin, Gradle, AWS API Gateway, and AWS Lambda.
I am going to assume working knowledge of gradle and kotlin and wont dive into the exact process of setting up of your multi-module project.
Setup
To get started we will set up our project using gradle init
. We are going to be choosing an application project, using Kotlin as language of choice, saying yes to subprojects, and selecting Kotlin as our script DSL language.
Delete all but two of the subprojects that gradle generates and rename them to lambdas
and stack
. lambdas
will hold all of our core business logic, any additional assets needed for execution, and the actual AWS lambda handler. stack
will hold the code needed for CDK to synthesize and deploy our stack to different environments. Create a file in the lambda project for the function handler and two in the stack project to hold the stack related code. The resulting structure should look something like the following:
Next we are going to add our respective dependencies. The build.gradle.kts
for the lambda project will contain non-CDK AWS dependencies and should behave as a component that can be deployed outside of the stack architecture. I will be using Kotinx serialization as my json parser of choice here but you can add any that you’re familiar with. Shadow jar will ensure that the jar that gets zipped and set to s3 contains all the required dependencies to run. Add the following to the build file for lambdas
:
Our CDK specific code will live in stack
and will reference any constructs that we know are present in the lamba module. Add the following to the stack’s build file:
With these in place we are ready to start fleshing out the pieces of our stack. To start we will create the lambda that will handle the logic of our requests. For demonstration purposes we are going to define a function that takes in a JSON object expecting a single key of “message”. The handler will echo that message back with some additional text also in JSON form to the caller. For now our response is a stringified map that contains the response message.
The next step is to set up our stack to actually use a lambda function. Fill out Stack.kt
file to look like the following:
App.kt
will contain the code that defines all stacks, their environments, and respective account information. Replace the account id value with your AWS account id and the region with your desired region. Once all stacks are declared we call app.synth()
to have the CDK generate the final cloud formation templates.
Next we need to add a cdk.json
file to the root of the project that contains metadata and additional information about our cloud stack. This is normally added by initializing a project with the cdk command but we are choosing a more manual route.
None of these values (necessarily) matter and these are copied from the default cdk
initializer. The only one that we care about is the app
value at the top level which is changed to point to our gradle run command.
Putting it all together
We can now start using the cdk
cli to deploy our infrastructure. If you haven’t already, set up your terminal environment to have aws configured and authenticated. You will also need to bootstrap your environment if this is the first time setting up a resource like lambda. See bootstrapping for more information. To start create the fat jar that holds the lambda function by running the synth command from the root of the project.
$ ./gradlew shadowJar$ cdk synth
You might have noticed the last line in the output stating that cdk.out/manifest.json
was missing. For all of these commands we are going to be operating from the root directory. By default the CLI expects cdk.out
to be present from the directory where the commands are being run. By inspecting the project tree we can see that cdk.out was actually created under the stack
module.
For consistency with normal gradle workflows with a wrapper at the root we are going to remain in the root directory. I feel this is easier to reason about and we can simply copy the commands we run later as we set up our CI/CD pipeline. All commands from here on that use cdk
will be appended with --app 'stack/cdk.out'
. Now that we have synthesized our template we are ready to deploy.
$ cdk deploy --app stack/cdk.out
The output of the deploy command will give us the ARN of the newly created function and we can also see it from our list of functions if we head to the lambda section of the AWS console.
Test the function by sending some JSON in a test event.
Our function works as expected and we have successfully deployed a lambda with CDK and Kotlin.
Taking it a step futher with API Gateway
Having a lambda is cool and all but ideally we are going to want a way to access this lambda from an external source, like a rest call. This is where API Gateway comes in and we can configure a POST endpoint directly from our exisintg CDK stack. Creating an api endpoint, making it point to our lambda, and connecting it to our stack can all be done inside our stack file. Make the following changes toStack.kt
:
Modifications are also needed to our lambda function to be able to work with this integration system. Any request we send is going to be transformed as a part of API Gateway -> Lambda proxy interactions. The new request to the lambda will be a map containing fields with information about the rest call (headers, encoding). The response should be a APIGatewayProxyResponseEvent
to conform to the specification outlined by API Gateway. The body lives under the “body” key in this new map. Update LambdaHandler.kt
:
Rebuild the shadowJar, synthesize the projcet, and deploy using the previous commands. Head to the console for API gateway and you can now test the newly created function.
Since we haven’t configured any sort of authentication and our integration is visible to the public net we can also test with curl or your API tester of choice
Destroy the stack when finished to free up any resources.
$ cdk destroy --app 'stack/cdk.out'
Summary
That is the basics of how to setup a scalable, multi module gradle project using AWS CDK to build your cloud infrastructure. Kotlin and gradle are not supported yet with the cdk
toolchain but hopefully that will change soon. Some final notes on using the cdk:
- The lambda method we are using here is blocking. It is trivial to wrap the internals of
handleRequest
withrunBlocking {...}
to enter a blocking coroutine context and have our business logic perform async operations. - If you didn’t change the lamda function to return
APIGatewayProxyResponseEvent
then you will see 502 responses being returned from API gateway. This is because your function’s responses aren’t confirming the required format that gateway expects. To make sure your data is being returned in a format acceptable by the proxy use the provided builder and return the event object. - You can and should combine all of the CLI calls into a script that runs them sequentially during your CD process. You can append a
--profile
argument to each call to generate templates for specific accounts. You should also be updating the account id in each Stack object you synthesize to make sure you are deploying to accounts explicitly. - You might see compilation issues related to java 6 vs 8 compatibility. Add the following to the
common-conventions
kotlin plugin created by gradle during initialization to compile successfully. If you didn’t use the gradle cli to make your project add this to your rootbuild.gradle.kts
or each module’sbuild.gradle.kts
as necessary.
tasks {
withType<KotlinCompile> { kotlinOptions { jvmTarget = "1.8" } }
}