49

Write an AWS Lambda Function with Kotlin and Micronaut [FREE]

 4 years ago
source link: https://www.tuicool.com/articles/qMzUzeY
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Serverless solutions such as AWS Lambda allow developers to run code without thinking about servers. Moreover, they offer you the ability to pay only for the compute time you consume instead of a fixed monthly rate. They are becoming especially suited for scenarios such as mobile phone application back-ends.

In this tutorial, you’ll take the following steps to create an AWS Lambda function using Kotlin:

  • Create a Kotlin project using Gradle.
  • Add a “Talk like a pirate” translator to the project.
  • Build an AWS Lambda RequestHandler.
  • Deploy your function to AWS.
  • Add an API-Gateway Trigger.
  • Re-do the project using the Micronaut framework, and re-deploy.

Prerequisites

In order to follow along with this tutorial step-by-step, you’ll need the following:

  • Basic knowledge of Kotlin. If you are new to Kotlin, check out ourKotlin tutorials or the bookKotlin Apprentice.
  • An Amazon Web Services (AWS) account.
  • JDK 8 installed in your computer.
  • Gradle 5.6.2 or greater . The easiest way to install Gradle in your computer is to use SDKMan.io . Once you install SDKMan you can open a Terminal and run sdk install gradle . You can also use Homebrew and the command brew install gradle .

You’ll be creating the tutorial project from scratch, and you can download the final project using the Download Materials button at the top or bottom of the tutorial.

Writing the Function

Time to get started creating your AWS Lambda function with Kotlin!

Create a Kotlin App with Gradle

Open a Terminal , make a new directory named pirate-translator , cd into the new empty directory, and type gradle init . You’ll see the following output:

$ gradle init
Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4]

Select application, type 2 , and press return .

Then, gradle will prompt you to choose an implementation language:

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Swift

Select Kotlin, type 4 , and press return .

Then, you’ll be prompted to choose a Build script DSL:

Select build script DSL:
  1: Groovy
  2: Kotlin
  Enter selection (default: Kotlin) [1..2] 2

Select Kotlin DSL, type 2 , and press return .

As project name enter pirate-translator

As source package enter com.raywenderlich .

Congratulations! You have created a Kotlin app which builds using Gradle.

Code a Pirate Translator

In this section, you are going to create Kotlin classes. You can use any text editor, but I recommend you use IntelliJ IDEA 2019.2 or later (Community or Ultimate) by JetBrains – the company behind Kotlin.

If you’re using IntelliJ IDEA, click File ➤ Open and open the pirate-translator directory you made before.

Create a new Kotlin file named PirateTranslator.kt and place in the directory src/main/kotlin/com/raywenderlich . Add an interface as contract for the Talk like a pirate translator.

// src/main/kotlin/com/raywenderlich/PirateTranslator.kt
package com.raywenderlich

interface PirateTranslator {
    // 1
    fun translate(message: String): String
}

In the interface, you specify a translate() method which takes a String and returns a String .

Next you’ll write a Pirate translator brain :]

It should have the following behavior:

  • For input Yes , it returns Aye .
  • For input Hello , it returns Ahoy .
  • For input Yes, Captain! , it returns Aye Aye! .

For example, for input,

“Hello, I am Captain Jack Sparrow”

the pirate translator returns:

“Ahoy!, I am Captain Jack Sparrow”

For your implementation of PirateTranslator , create a new file named DefaultPirateTranslator.kt and place it under src/main/kotlin/com/raywenderlich . Add the class DefaultPirateTranslator :

// src/main/kotlin/com/raywenderlich/DefaultPirateTranslator.kt
package com.raywenderlich

class DefaultPirateTranslator : PirateTranslator {

    // 1
    val replacements = mapOf("Hello" to "Ahoy!", "Yes" to "Aye!", "Yes, Captain!" to "Aye Aye!")
    
    override fun translate(message: String): String {
        var result = message
        // 2
        replacements.forEach { k, v -> result =    result.replace(k, v) }
        return result
    }
}

In this code:

Map

Test the PirateTranslator

Next you’ll add a test for the PirateTranslator . Create a file under src/test/kotlin/com/raywenderlich and name it PirateTranslatorTest.kt .

Add your test class:

//src/test/kotlin/com/raywenderlich/PirateTranslatorTest.kt
package com.raywenderlich

import kotlin.test.Test
import kotlin.test.assertEquals

class PirateTranslatorTest {
    // 1
    @Test fun testPirateTranslator() {
        // 2     
        val classUnderTest : PirateTranslator = DefaultPirateTranslator()
        
        // 3
        assertEquals("Ahoy!, I am Captain Jack Sparrow", classUnderTest.translate("Hello, I am Captain Jack Sparrow"))
        // 3
        assertEquals("Aye!", classUnderTest.translate("Yes"))
    }
}

Here you:

@Test

To run the test, in Terminal in the project root directory, run the following command:

$ ./gradlew test --tests=com.raywenderlich.PirateTranslatorTest

You should see a BUILD SUCCESSFUL message if the test passes. If the test failed, you’d see an exception. You can also run the test in IntelliJ IDEA, by pressing the green run button next to the test method.

Screen-Shot-2019-10-15-at-3.10.12-PM-650x359.png

Create an AWS Lambda RequestHandler

Your project needs to accept input, pass it to the pirate translator, and respond with the translated output. You’ll do this with an AWS Lambda RequestHandler .

Create a class named HandlerInput under src/main/kotlin/com/raywenderlich . This class will encapsulate the message you are going to input into your Talk like a pirate serverless function. Set up the class like this:

// src/main/kotlin/com/raywenderlich/HandlerInput.kt
package com.raywenderlich

class HandlerInput {
    var message: String = ""
}

Note that you are not using a Kotlin data class for the input, as that would cause issues when using it to bind JSON in the AWS Lambda RequestHandler .

Now create a Kotlin data class named HandlerOutput under src/main/kotlin/com/raywenderlich . This class will encapsulate the translation you will receive from the Talk like a pirate serverless function. The data class should look like this:

// src/main/kotlin/com/raywenderlich/HandlerOutput.kt
package com.raywenderlich

data class HandlerOutput(val message: String, val pirateMessage: String)

Now you need to add the AWS Lambda dependency. Modify the file build.gradle.kts , by adding the following dependency in the dependencies block:

implementation("com.amazonaws:aws-lambda-java-core:1.2.0")

The aws-lambda-java-core dependency is a minimal set of interface definitions for Java support in AWS Lambda.

Open the Gradle tab in IntelliJ IDEA and hit the sync button to synchronize the dependencies.

Screen-Shot-2019-10-15-at-3.19.25-PM-650x295.png

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

//src/main/kotlin/com/raywenderlich/App.kt
package com.raywenderlich

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler

// 1
class App : RequestHandler<HandlerInput, HandlerOutput> {
    // 2
    val translator : PirateTranslator =   DefaultPirateTranslator()

    // 3
    override fun handleRequest(input: HandlerInput?, context: Context?): HandlerOutput {
        input?.let {
            // 4
            return HandlerOutput(it.message, translator.translate(it.message))
        }
        return HandlerOutput("", "");
    }
}

Here you do the following:

  1. Create a RequestHandler . Lambda stream request handlers implement AWS Lambda Function application logic using input and output streams.
  2. Instantiate a PirateTranslator .
  3. Override handleRequest which takes a HandlerInput and responds with a HandlerOutput .
  4. Use the translator to populate pirateMessage , the second argument of the HandlerOutput constructor.

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

//src/test/kotlin/com/raywenderlich/AppTest.kt
package com.raywenderlich

import kotlin.test.Test
import kotlin.test.assertEquals

class AppTest {
    @Test fun testAppHasAGreeting() {
        val classUnderTest = App()
        val input = HandlerInput()
        input.message = "Hello"
        val expected = HandlerOutput("Hello", "Ahoy!")
        var output = classUnderTest.handleRequest(input, null)
        assertEquals(expected, output)
    }
}

The above test verifies that given a HandlerInput with a message of value “Hello”, the response (the HandlerOutput ) contains a pirateMessage with value “Ahoy!”.

Go ahead and run the test the same way as before, either from a Terminal using gradlew , or right in IntelliJ IDEA.

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-create-function-1.png

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

aws-create-function-2.png

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 :

aws-create-function-3.png

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:

aws-create-function-4.png

That will show the Configure test event dialog:

aws-create-function-5.png

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:

aws-create-function-6.png

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:

aws-create-function-7.png

You’ll be taken to the Add trigger screen:

aws-create-function-8.png

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:

aws-create-function-9.png

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.

aws-create-function-10.png

Open the Actions dropdown and select Create Method :

aws-create-function-11.png

Select POST and click the checkmark:

aws-create-function-12.png

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.

aws-create-function-13.png

Then click Test :

aws-create-function-14.png

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!

aws-create-function-15.png

Now open the Actions dropdown again and choose Deploy API .

aws-create-function-16.png

Select default as the Deployment stage , and click Deploy :

aws-create-function-17.png

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 aReactiveX 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 .

aws-create-function-18.png

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.

Where to Go From Here?

Congratulations again on deploying two versions of an AWS Lambda function written in Kotlin, and making the serverless function available in an API! :]

You can download both final projects using the Download Materials button at the top or bottom of the tutorial.

For further reading:

You have just scratched the surface of what is possible with AWS Lambda and serverless functions, but I hope it opens a world of possibilities for your mobile app API back-ends.

If you have any questions or comments, please don’t hesitate to join the forum discussion below.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK