Serverless Development with Kotlin, AWS Lambda, and MongoDB Atlas
Rate this tutorial
As seen in a previous tutorial, creating a serverless function for AWS Lambda with Java and MongoDB isn't too complicated of a task. In fact, you can get it done with around 35 lines of code!
However, maybe your stack doesn't consist of Java, but instead Kotlin. What needs to be done to use Kotlin for AWS Lambda and MongoDB development? The good news is not much will be different!
In this tutorial, we'll see how to create a simple AWS Lambda function. It will use Kotlin as the programming language and it will use the MongoDB Kotlin driver for interacting with MongoDB.
There are a few prerequisites that must be met in order to be successful with this particular tutorial:
- Must have a Kotlin development environment installed and configured on your local computer.
- Must have an Amazon Web Services (AWS) account.
The easiest way to develop with Kotlin is through IntelliJ, but it is a matter of preference. The requirement is that you can build Kotlin applications with Gradle.
For the purpose of this tutorial, any MongoDB Atlas instance will be sufficient whether it be the M0 free tier, the serverless pay-per-use tier, or something else. However, you will need to have the instance properly configured with user rules and network access rules. If you need help, use our MongoDB Atlas tutorial as a starting point.
Assuming you have a project created using your tooling of choice, we need to properly configure the build.gradle.kts file with the correct dependencies for AWS Lambda with MongoDB.
In the build.gradle.kts file, include the following:
1 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 3 plugins { 4 kotlin("jvm") version "1.9.0" 5 application 6 id("com.github.johnrengelman.shadow") version "7.1.2" 7 } 8 9 application { 10 mainClass.set("example.Handler") 11 } 12 13 group = "org.example" 14 version = "1.0-SNAPSHOT" 15 16 repositories { 17 mavenCentral() 18 } 19 20 dependencies { 21 testImplementation(kotlin("test")) 22 implementation("com.amazonaws:aws-lambda-java-core:1.2.2") 23 implementation("com.amazonaws:aws-lambda-java-events:3.11.1") 24 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") 25 implementation("org.mongodb:bson:4.10.2") 26 implementation("org.mongodb:mongodb-driver-kotlin-sync:4.10.2") 27 } 28 29 tasks.test { 30 useJUnitPlatform() 31 } 32 33 tasks.withType<KotlinCompile> { 34 kotlinOptions.jvmTarget = "1.8" 35 }
There are a few noteworthy items in the above configuration.
1 plugins { 2 kotlin("jvm") version "1.9.0" 3 application 4 id("com.github.johnrengelman.shadow") version "7.1.2" 5 }
AWS Lambda expects a ZIP or a JAR. By using the Shadow plugin, we can use Gradle to build a "fat" JAR, which includes both the application and all required dependencies. When using Shadow, the main class must be defined.
To define the main class, we have the following:
1 application { 2 mainClass.set("example.Handler") 3 }
The above assumes that all our code will exist in a
Handler
class in an example
package. Yours does not need to match, but note that this particular class and package will be referenced throughout the tutorial. You should swap names wherever necessary.The next item to note is the
dependencies
block:1 dependencies { 2 testImplementation(kotlin("test")) 3 implementation("com.amazonaws:aws-lambda-java-core:1.2.2") 4 implementation("com.amazonaws:aws-lambda-java-events:3.11.1") 5 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") 6 implementation("org.mongodb:bson:4.10.2") 7 implementation("org.mongodb:mongodb-driver-kotlin-sync:4.10.2") 8 }
In the above block, we are including the various AWS Lambda SDK packages as well as the MongoDB Kotlin driver. These dependencies will allow us to use MongoDB with Kotlin and AWS Lambda.
If you wanted to, you could run the following command:
1 ./gradlew shadowJar
As long as the main class exists, it should build a JAR file for you.
With the configuration items out of the way, we can focus on the development of our serverless function. Open the project's src/main/kotlin/example/Handler.kt file and include the following boilerplate code:
1 package example 2 3 import com.amazonaws.services.lambda.runtime.Context 4 import com.amazonaws.services.lambda.runtime.RequestHandler 5 import com.mongodb.client.model.Filters 6 import com.mongodb.kotlin.client.MongoClient 7 import com.mongodb.kotlin.client.MongoCollection 8 import com.mongodb.kotlin.client.MongoDatabase 9 import org.bson.Document 10 import org.bson.conversions.Bson 11 import org.bson.BsonDocument 12 13 class Handler : RequestHandler<Map<String, String>, Void> { 14 15 override fun handleRequest(input: Map<String, String>, context: Context): void { 16 17 return null; 18 19 } 20 }
The above code won't do much of anything if you tried to execute it on AWS Lambda, but it is a starting point. Let's start by establishing a connection to MongoDB.
Within the
Handler
class, add the following:1 class Handler : RequestHandler<Map<String, String>, Void> { 2 3 private val mongoClient: MongoClient = MongoClient.create(System.getenv("MONGODB_ATLAS_URI")) 4 5 override fun handleRequest(input: Map<String, String>, context: Context): void { 6 7 val database: MongoDatabase = mongoClient.getDatabase("sample_mflix") 8 val collection: MongoCollection<Document> = database.getCollection("movies") 9 10 return null; 11 12 } 13 }
First, you'll notice that we are creating a
mongoClient
variable to hold the information about our connection. This client will be created using a MongoDB Atlas URI that we plan to store as an environment variable. It is strongly recommended that you use environment variables to store this information so your credentials don't get added to your version control.In case you're unsure what the MongoDB Atlas URI looks like, it looks like the following:
1 mongodb+srv://<username>:<password>@<clustername>.dmhrr.mongodb.net/?retryWrites=true&w=majority
You can find your exact connection string using the MongoDB Atlas CLI or through the MongoDB Atlas dashboard.
Within the
handleRequest
function, we get a reference to the database and collection that we want to use:1 val database: MongoDatabase = mongoClient.getDatabase("sample_mflix") 2 val collection: MongoCollection<Document> = database.getCollection("movies")
For this particular example, we are using the
sample_mflix
database and the movies
collection, both of which are part of the optional MongoDB Atlas sample dataset. Feel free to use a database and collection that you already have.Now we can focus on interactions with MongoDB. Make a few changes to the
Handler
class so it looks like this:1 package example 2 3 import com.amazonaws.services.lambda.runtime.Context 4 import com.amazonaws.services.lambda.runtime.RequestHandler 5 import com.mongodb.client.model.Filters 6 import com.mongodb.kotlin.client.MongoClient 7 import com.mongodb.kotlin.client.MongoCollection 8 import com.mongodb.kotlin.client.MongoDatabase 9 import org.bson.Document 10 import org.bson.conversions.Bson 11 import org.bson.BsonDocument 12 13 class Handler : RequestHandler<Map<String, String>, List<Document>> { 14 15 private val mongoClient: MongoClient = MongoClient.create(System.getenv("MONGODB_ATLAS_URI")) 16 17 override fun handleRequest(input: Map<String, String>, context: Context): List<Document> { 18 19 val database: MongoDatabase = mongoClient.getDatabase("sample_mflix") 20 val collection: MongoCollection<Document> = database.getCollection("movies") 21 22 var filter: Bson = BsonDocument() 23 24 if(input.containsKey("title") && !input.get("title").isNullOrEmpty()) { 25 filter = Filters.eq("title", input.get("title")) 26 } 27 28 val results: List<Document> = collection.find(filter).limit(5).toList() 29 30 return results; 31 32 } 33 }
Instead of using
Void
in the RequestHandler
and void
as the return type for the handleRequest
function, we are now using List<Document>
because we plan to return an array of documents to the requesting client.This brings us to the following:
1 var filter: Bson = BsonDocument() 2 3 if(input.containsKey("title") && !input.get("title").isNullOrEmpty()) { 4 filter = Filters.eq("title", input.get("title")) 5 } 6 7 val results: List<Document> = collection.find(filter).limit(5).toList() 8 9 return results;
Instead of executing a fixed query when the function is invoked, we are accepting input from the user. If the user provides a
title
field with the invocation, we construct a filter for it. In other words, we will be looking for movies with a title that matches the user input. If no title
is provided, we just query for all documents in the collection.For the actual
find
operation, rather than risking the return of more than a thousand documents, we are limiting the result set to five and are converting the response from a cursor to a list.At this point in time, our simple AWS Lambda function is complete. We can focus on the building and deployment of the function now.
Before we worry about AWS Lambda, let's build the project using Shadow. From the command line, IntelliJ, or with whatever tool you're using, execute the following:
1 ./gradlew shadowJar
Find the JAR file, which is probably in the build/libs directory unless you specified otherwise.
Everything we do next will be done in the AWS portal. There are three main items that we want to take care of during this process:
- Add the environment variable with the MongoDB Atlas URI to the Lambda function.
- Rename the "Handler" information in Lambda to reflect the actual project.
- Upload the JAR file to AWS Lambda.
Within the AWS Lambda dashboard for your function, click the "Configuration" tab followed by the "Environment Variables" navigation item. Add
MONGODB_ATLAS_URI
along with the appropriate connection string when prompted. Make sure the connection string reflects your instance with the proper username and password.You can now upload the JAR file from the "Code" tab of the AWS Lambda dashboard. When this is done, we need to tell AWS Lambda what the main class is and the function that should be executed.
In the "Code" tab, look for "Runtime Settings" and choose to edit it. In our example, we had example as the package and Handler as the class. We also had our function logic in the handleRequest function.
With all this in mind, change the "Handler" within AWS Lambda to example.Handler::handleRequest or whatever makes sense for your project.
At this point, you should be able to test your function.
On the "Test" tab of the AWS Lambda dashboard, choose to run a test as is. You should get a maximum of five results back. Next, try using the following input criteria:
1 { 2 "title": "The Terminator" 3 }
Your response will now look different because of the filter.
Congratulations! You created your first AWS Lambda function in Kotlin and that function supports communication with MongoDB!
While this example was intended to be short and simple, you could add significantly more logic to your functions that engage with other functionality of MongoDB, such as aggregations and more.
If you'd like to see how to use Java to accomplish the same thing, check out my previous tutorial on the subject titled Serverless Development with AWS Lambda and MongoDB Atlas Using Java.