Write an AWS Lambda Function with Kotlin and Micronaut

In this Kotlin tutorial, you’ll learn how to create a “Talk like a pirate” translator and deploy it to AWS Lambda as a function. By Sergio del Amo.

4.3 (9) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Gradle Shadow Plugin

Before deploying to AWS, apply the Gradle Shadow Plugin – a Gradle plugin for collapsing all dependencies and project code into a single Jar file.

Modify the file build.gradle.kts again, this time to add to the plugins block:

id("com.github.johnrengelman.shadow") version "5.1.0"

Go ahead and sync the project Gradle files one more time for fun! :]

Deploying to AWS Lambda

Now that you have your Kotlin pirate translator project set up, it’s time to deploy it to AWS Lambda! :]

Login to your AWS Console and go to https://console.aws.amazon.com/lambda.

Create a Function

You’ll be taken to the Functions screen:

AWS Lambda Functions screen

Click on Create function, and you’ll see the Create function screen:

Create function screen

On this screen:

  • Select Author from scratch.
  • As Function name enter pirate-translator.
  • For Runtime, select Java 8.
  • Select Create a new Role with Basic Lambda permissions
  • Click on Create Function.

You’ll be taken to your new AWS Lambda function pirate-translator:

Pirate translator screen

Locally in a Terminal, from the project root folder run ./gradlew shadowJar.

That generates a JAR file build/libs/pirate-translator-all.jar.

Back in AWS, on the pirate-translator screen:

  • Upload the JAR file as function package.
  • For the Handler, enter: com.raywenderlich.App::handleRequest.
  • Click Save in the upper right.

Test Your Function

Next you’ll create a test event for your function. Select Configure test events from the dropdown:

Configure test event dropdown

That will show the Configure test event dialog:

Configure test event dialog

On the dialog:

  • For Event name enter PirateHello
  • As content enter: {"message":"Hello"}
  • Click Create.

Now make sure the PirateHello event is selected and click Test. You should then see a successful run of the test:

Test success

Congratulations! Your Kotlin Pirate Translator works with AWS Lambda!

Add an API-Gateway Trigger

The next step is to create an AWS API Gateway Trigger. Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.

You will create an API in front of your AWS Lambda making it transparent to the consumers of the API which technology you used to power your pirate translator.

In the AWS Lambda console, on your pirate-translator screen, click the Add trigger button:

Add a trigger

You’ll be taken to the Add trigger screen:

Add trigger screen

On this screen:

  • Choose API Gateway from the first dropdown.
  • Select Create a new API.
  • Select Open for Security.
  • For the API name, leave pirate-translator-API.
  • For Deployment stage, leave default.
  • Leave the rest with the default values as well and click Add.

You’ll be taken back to the screen for your function:

Back to your function screen

Click the API name to go to the Amazon API Gateway UI. From here, select the method ANY and in the Actions dropdown select Delete Method. Then confirm the delete.

Select Delete Method Action

Open the Actions dropdown and select Create Method:

Select Create Method Action

Select POST and click the checkmark:

Select POST method

Choose Integration type Lambda Function. Enter pirate-translator for the function name. Type p and you should be able to select using auto-completion. Then click Save in the lower right, and confirm on the dialog that appears.

Setting up the POSE

Then click Test:

Test the POST method

Enter the Headers:

Accept:application/json
Content-Type:application/json

And for the Request Body use:

{"message": "hello"}

Then click Test and you will see the pirate translation as the Response Body. Aye!

Running the test

Now open the Actions dropdown again and choose Deploy API.

Select Deploy API

Select default as the Deployment stage, and click Deploy:

Deploying the API

Once, it is deployed you will get a Invoke URL which you can exercise with a cURL command in Terminal:

$ curl -X "POST" "https://cd9z9e3dgi.execute-api.us-east-1.amazonaws.com/default/pirate-translator" -H 'Accept: application/json' -H 'Content-Type: application/json' -d $'{"message": "Hello"}'

(Be sure to substitute in your Invoke URL.)

You will get a response like this:

{"message":"Hello","pirateMessage":"Ahoy!"}

Congratulations! You have created an API in front of an AWS Lambda function written with Kotlin!

Writing the Function with Micronaut

Micronaut is a a modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications. In this section, you’ll see how to create the same pirate translator using Micronaut.

The easiest way to install Micronaut is via SDKMan:

$ sdk install micronaut

Or instead by using Homebrew:

$ brew install micronaut

Create a new directory named pirate-translator-mn. In that directory run:

$ mn create-function com.raywenderlich.app --inplace --lang=kotlin --test=spek

Like before, you can open the project in IntelliJ IDEA 2019.2 or later.

Copy the files PirateTranslator.kt, DefaultPirateTranslator.kt and HandlerOutput.kt, HandlerInput.kt from the previous project to src/main/kotlin/com/raywenderlich in the Micronaut project.

For the Micronaut version of the project, the HandlerInput can be a data class. Replace the content of HandlerInput.kt with:

package com.raywenderlich

data class HandlerInput(val message: String)

Replace the contents of src/main/kotlin/com/raywenderlich/AppFunction.kt with :

package com.raywenderlich;

import io.micronaut.function.executor.FunctionInitializer
import io.micronaut.function.FunctionBean;
import java.util.function.Function;

@FunctionBean("app")
 // 1
class AppFunction : FunctionInitializer(), Function<HandlerInput, HandlerOutput> {
     val translator : PirateTranslator =  DefaultPirateTranslator()

     override fun apply(input: HandlerInput): HandlerOutput {
         return HandlerOutput(input.message, translator.translate(input.message))
     }
}

/**
 * This main method allows running the function as a CLI application using: echo '{}' | java -jar function.jar 
  * where the argument to echo is the JSON to be parsed.
 */
fun main(args : Array<String>) { 
    val function = AppFunction()
    function.run(args, { context -> function.apply(context.get(HandlerInput::class.java))})
} 

The code is almost identical to the previous project, but instead of implementing com.amazonaws.services.lambda.runtime.RequestHandler, you implement java.util.function.Function. That will allow you to swap a serverless provider such as AWS Lambda with another serverless provider easily.

Delete src/main/kotlin/com/raywenderlich/App.kt from the project, since you use HandlerInput and HandlerOutput.

Replace the contents of src/main/test/kotlin/com/raywenderlich/AppClient.kt with:

package com.raywenderlich

import io.micronaut.function.client.FunctionClient
import io.micronaut.http.annotation.Body
import io.reactivex.Single
import javax.inject.Named

// 1
@FunctionClient
interface AppClient {

    // 2
    @Named("app")
    // 3 
    fun apply(@Body body : HandlerInput): Single<HandlerOutput>

}

This code does the following:

  1. Applying the @FunctionClient annotation means that methods defined by the interface become invokers of remote or local functions configured by the application.
  2. The value of @Named must match the value of the @FunctionBean annotation in the AppFunction class.
  3. You define a method returning a ReactiveX Single type. Micronaut implements the method for you.

Replace the contents of src/main/test/kotlin/com/raywenderlich/AppFunctionTest.kt with:

package com.raywenderlich

import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.it
import org.junit.jupiter.api.Assertions.assertEquals

// 1
class AppFunctionTest: Spek({

    describe("app function") {
       // 2
        val server = ApplicationContext.run(EmbeddedServer::class.java)
        
        // 3
        val client = server.applicationContext.getBean(AppClient::class.java)
    
        it("should return 'Ahoy!'") {
            val body = HandlerInput("Hello")
            // 4
            assertEquals("Ahoy!", client.apply(body).blockingGet().pirateMessage
            )
        }
    
        afterGroup {
            server.stop()
        }  
    }
})

Here you do the following:

  1. Test using Spek– a specification framework that allows you to easily define specifications in a clear, understandable, human readable way.
  2. Start a Netty embedded server inside your test.
  3. Retrieve the declarative client to consume the function.
  4. Since the client returns a blocking type, you call blockingGet to obtain an answer.

Generate another shadow JAR with ./gradlew shadowJar from the pirate-translator-mn root.

Now you need to update your AWS Lambda function to use the Micronaut version of the project. Go back to the AWS Lambda console, and on the screen for your pirate-translator function:

  • Upload the JAR
  • Change the Handler to io.micronaut.function.aws.MicronautRequestStreamHandler.
  • Click Save.

Uploading the Micronaut JAR

Go ahead and run the same cURL command you ran before as a test.

Congratulations! You have deployed your first Micronaut function written with Kotlin to AWS Lambda.

Micronaut offers a lot of features such as dependency injection, declarative HTTP clients, bean validation, etc., which you have not used in this tutorial but which can greatly improve your performance and code quality when developing real-world serverless functions.