Micronaut AWS

Provides integration between Micronaut and Amazon Web Services (AWS)

Version: 1.1.0.M2

1 Introduction

This project provides various extensions to Micronaut for Amazon Web Services (AWS). The primary focus initially is on AWS Lambda, however other integrations may be included in this project in the future.

A base AWSClientConfiguration is provided which can be used as a base configuration class for any configuration that needs to configure an AWS SDK client.

Some features described in this release depend on Micronaut 1.1 which is at the development / milestone stage.

2 AWS Lambda Support

Regular Micronaut functions created via mn create-function can be deployed to Lambda directly. However if you need further integration with what AWS Lambda has to offer then this project includes a number of parent classes and utilities to simplify working with the native Lambda APIs.

2.1 Micronaut Request Handlers

The micronaut-function-aws module includes two parent classes you can subclass to implement native AWS Lambda functionality. To get started first add the micronaut-function-aws dependency to your build:

compile 'io.micronaut:micronaut-function-aws'
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-function-aws</artifactId>
</dependency>

You can then use either MicronautRequestHandler or MicronautRequestStreamHandler as your parent class to implement your Lambda handler. For example:

Implementing MicronautRequestHandler
package example;

import io.micronaut.context.env.Environment;
import javax.inject.Inject;

public class RoundHandler extends MicronautRequestHandler<Float, Integer> { (1)

    @Inject
    MathService mathService; (2)

    @Inject
    Environment env;

    @Override
    public Integer execute(Float input) {
        return mathService.round(input); (3)
    }
}
1 The class extends MicronautRequestHandler and must have a default constructor.
2 You can use field injection to inject dependencies
3 The function implementation

With MicronautRequestHandler it is expected that you supply generic types with the input and the output types.

If you wish to work with raw streams then subclass MicronautRequestStreamHandler instead.

You should then configure the class name as the Lambda handler when deploying to AWS. For example with SAM:

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: example.RoundHandler::handleRequest

2.2 AWS API Gateway Support

An alternative approach to using Micronaut with AWS is to use Micronaut support for the AWS Serverless Java Container project.

In Micronaut 1.1 or above you can create an API proxy ready application with mn create-app myapp --features aws-api-gateway

In this arrangement you build your application as a regular REST application, for example:

Example Controller
package example;

import io.micronaut.http.annotation.*;

@Controller("/ping")
public class PingController {

    @Get("/")
    public String index() {
        return "{\"pong\":true}";
    }
}

You then need to add the micronaut-function-aws-api-proxy dependency:

compile 'io.micronaut.aws:micronaut-function-aws-api-proxy'
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-function-aws-api-proxy</artifactId>
</dependency>

You can then implement a RequestHandler that handles the API proxy request:

Example Controller
package example;

import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.model.*;
import com.amazonaws.services.lambda.runtime.*;
import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler;
import java.io.*;

public class StreamLambdaHandler implements RequestStreamHandler {
    private static MicronautLambdaContainerHandler handler; (1)
    static {
        try {
            handler = MicronautLambdaContainerHandler.getAwsProxyHandler();
        } catch (ContainerInitializationException e) {
            // if we fail here. We re-throw the exception to force another cold start
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Micronaut", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context); (2)
    }
}
1 Micronaut is initialized in the static initializer
2 The API Proxy Request is handled

The MicronautLambdaContainerHandler class is used to initialization Micronaut and handle the serverless requests.

The getAwsProxyHandler() method also accepts a ApplicationContextBuilder instance which you can use to further customize application bootstrap.

See the Example Application and associated README for instructions on how to deploy locally using AWS SAM Local and AWS Cloud Formation for production.

2.3 AWS Lambda Custom Runtimes

You may wish to implement a custom runtime, for example if you wish to use a different JVM than the one AWS provides.

The MicronautLambdaRuntime class provides an implementation that you can use to execute a custom runtime. You need to add the micronaut-function-aws-custom-runtime dependency to your build:

compile 'io.micronaut.aws:micronaut-function-aws-custom-runtime'
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-function-aws-custom-runtime</artifactId>
</dependency>

The application should be built with using the API Gateway approach.

Custom Java Runtimes

Then create a bootstrap bash script. The following is an example bootstrap

Example bootstrap
#!/bin/sh
set -euo pipefail
java -XX:TieredStopAtLevel=1 -noverify -cp server.jar io.micronaut.function.aws.runtime.MicronautLambdaRuntime

You then need to create a zip bundle including the bootstrap and your application JAR filed (in the above case named server.jar) for deployment to AWS and specify a runtime of provided. For example:

aws lambda create-function --function-name my-function \
--zip-file fileb://function.zip --handler function.handler --runtime provided \
--role arn:aws:iam::123456789:role/lambda_basic_execution
See Publishing a Custom Runtime for more information.

Custom GraalVM Native Runtimes

To achieve the absolutely best cold startup time you can create an API Gateway application that is compiled into a native image and then run using a custom AWS Lambda Runtime. The quickest way to get started is with the Micronaut 1.1 or above CLI:

Using the CLI
$ mn create-app my-app --features aws-api-gateway-graal

Then create a controller, for example:

Example Controller
package example;

import io.micronaut.http.annotation.*;

@Controller("/ping")
public class PingController {

    @Get("/")
    public String index() {
        return "{\"pong\":true}";
    }
}

You can then use Docker to build a function ZIP file ready for deployment to AWS Lambda:

Building the Function ZIP file
$ docker build . -t my-app
$ mkdir build
$ docker run --rm --entrypoint cat my-app  /home/application/function.zip > build/function.zip

The application can then be deployed to Lambda either via the AWS console or the CLI:

Deploying the Function with the AWS CLI
$ aws lambda create-function --function-name my-app \
--zip-file fileb://build/function.zip --handler function.handler --runtime provided \
--role arn:aws:iam::881337894647:role/lambda_basic_execution
See the Example application on Github

Then you can invoke the function:

Invoking the Function with the AWS CLI
$ aws lambda invoke --function-name my-app --payload '{"resource": "/{proxy+}", "path": "/ping", "httpMethod": "GET"}' build/response.txt
$ cat build/response.txt

3 Alexa Skill Support

The micronaut-function-aws-alexa module includes support for building Alexa Skills with Micronaut:

compile 'io.micronaut.aws:micronaut-function-aws-alexa'
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-function-aws-alexa</artifactId>
</dependency>

In Micronaut 1.1 or above you can create an Alexa Skills application with mn create-function hello-alexa --provider alexa

When using Micronaut’s Alexa support you set your Lambda handler name to AlexaFunction class or a sublcass of this class if you plan to customize it.

The AlexaFunction will wire up your Alexa application and supports dependency injection of the following types:

  • com.amazon.ask.dispatcher.request.handler.RequestHandler

  • com.amazon.ask.dispatcher.request.interceptor.RequestInterceptor

  • com.amazon.ask.dispatcher.exception.ExceptionHandler

  • com.amazon.ask.builder.SkillBuilder

Simply declaring these types as beans will automatically configure the Alexa Skill appropriately.

You can find sample applications in Java, Kotlin and Groovy in the Examples directory of the repository.

3.1 The IntentHandler Annotation

To simplify the programming model Micronaut AWS includes a @IntentHandler annotation that can be used on any bean method to make the method an intent handler.

The method must accept a single value of type com.amazon.ask.dispatcher.request.handler.HandlerInput and return a value of type Optional<com.amazon.ask.model.Response> otherwise a compilation error will occur.

A typical Alexa application written in Micronaut looks like:

import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.model.Response;
import io.micronaut.function.aws.alexa.AlexaIntents;
import io.micronaut.function.aws.alexa.annotation.IntentHandler;

import javax.inject.Singleton;
import java.util.Optional;

@Singleton (1)
public class AlexaApplication {

    public static final String INTENT_NAME = "HelloWorldIntent";

    private final MessageService messageService;

    public AlexaApplication(MessageService messageService) { (2)
        this.messageService = messageService;
    }

    @IntentHandler(INTENT_NAME) (3)
    public Optional<Response> greet(HandlerInput input) { (4)
        String speechText = messageService.sayHello();
        return input.getResponseBuilder()
                .withSpeech(speechText)
                .withSimpleCard("HelloWorld", speechText)
                .build();
    }
import com.amazon.ask.dispatcher.request.handler.HandlerInput
import com.amazon.ask.model.Response
import groovy.transform.CompileStatic
import io.micronaut.function.aws.alexa.AlexaIntents
import io.micronaut.function.aws.alexa.annotation.IntentHandler
import javax.inject.Singleton

@Singleton (1)
@CompileStatic
class AlexaApplication {

    private final MessageService messageService

    AlexaApplication(MessageService messageService) { (2)
        this.messageService = messageService
    }

    @IntentHandler("HelloWorldIntent") (3)
    Optional<Response> greet(HandlerInput input) { (4)
        String speechText = messageService.sayHello()
        return input.getResponseBuilder()
                .withSpeech(speechText)
                .withSimpleCard("HelloWorld", speechText)
                .build()
    }
import com.amazon.ask.dispatcher.request.handler.HandlerInput
import com.amazon.ask.model.Response
import io.micronaut.function.aws.alexa.AlexaIntents
import io.micronaut.function.aws.alexa.annotation.IntentHandler

import javax.inject.Singleton
import java.util.Optional

@Singleton (1)
class AlexaApplication(val messageService: MessageService) { (2)

    companion object {
        const val INTENT_NAME = "HelloWorldIntent"
    }

    @IntentHandler(INTENT_NAME) (3)
    fun greet(input : HandlerInput) : Optional<Response> { (4)
        val speechText = messageService.sayHello()
        return input.responseBuilder
                .withSpeech(speechText)
                .withSimpleCard("HelloWorld", speechText)
                .build()
    }
1 The javax.inject.Singleton annotation is used to define AlexApplication as a bean
2 Other services can be dependency injected into the constructor
3 The @IntentHandler is used to indicate which methods are intent handlers
4 The method receives a HandlerInput and returns a Response