export AWS_ACCESS_KEY_ID=XXXX
export AWS_SECRET_KEY=YYYY
Micronaut AWS
Provides integration between Micronaut and Amazon Web Services (AWS)
Version: 1.1.2
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 Credentials Provider
When working with AWS SDK, you may need to provide a com.amazonaws.auth.AWSCredentialsProvider
. To ease that this module provides a utility class: EnvironmentAWSCredentialsProvider.
For example the following snippet show how you may configure a S3 Client if you set two environment variables:
AmazonS3ClientBuilder amazonS3ClientBuilder = AmazonS3ClientBuilder.standard();
amazonS3ClientBuilder.setCredentials(new EnvironmentAWSCredentialsProvider(applicationContext.getEnvironment()));
AmazonS3 s3 = amazonS3ClientBuilder.build();
Read about externalized Configuration with property sources in Mironaut.
3 AWS Lambda Support
Regular Micronaut functions created via mn create-function
can be deployed to Lambda directly.
Using the CLI
$ mn create-function example |
For example, when using create-function hello
Micronaut 1.1 and above will create a function that looks like:
package example;
import io.micronaut.function.executor.FunctionInitializer;
import javax.inject.*;
@Singleton
public class EchoFunction extends FunctionInitializer { (1)
@Inject EchoService echoService; (2)
public Message echo(Message msg) { (3)
return greetingService.hello(msg);
}
public static void main(String...args) throws IOException { (4)
EchoFunction function = new EchoFunction();
function.run(args, (context)-> function.sayHello(context.get(Message.class)));
}
}
1 | The function shouuld be a simple class annotated with @Singleton that extends from FunctionInitializer and has no constructor arguments. |
2 | You can use dependency injection to inject fields. |
3 | The function should be defined as a public method, and accept a single argument that is a simple java.lang type or a POJO bean that can be serialized/deserialized to JSON. |
4 | You can optionally define a main method if you plan to deploy the function to other FaaS environments that use standard in/out if not you can remove the main method |
The FunctionInitiazer
super class will bootstrap Micronaut and perform injection on the instance prior to the echo
method being executed.
An appropriate SAM definition for this function would be:
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: example.EchoFunction::echo
The key part is the Handler
which should be set to example.EchoFunction::echo
which is the class name and method name combination. The main
method is optional and can be removed if it is not needed. The main
method allows the function to be executed from the cli, for example:
$ echo '{"name":"Fred"}' | java -jar function.jar
The above example pipes the JSON into system in which the functions reads and writes the result to system out. Certain FaaS systems such as OpenFaaS use system in/out for function inputs and outputs, hence with the main method in place you can deploy the function to such systems.
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.
3.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:
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
3.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.
Using the CLI with Micronaut 1.1 or above
$ mn create-app my-app --features aws-api-gateway |
In this arrangement you build your application as a regular REST application, for example:
package example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/ping")
public class PingController {
@Get("/")
public String ping() {
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:
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. |
3.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
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:
package example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/ping")
public class PingController {
@Get("/")
public String ping() {
return "{\"pong\":true}";
}
}
You can then use Docker to build a function ZIP file ready for deployment to AWS Lambda:
$ 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:
$ 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:
$ aws lambda invoke --function-name my-app --payload '{"resource": "/{proxy+}", "path": "/ping", "httpMethod": "GET"}' build/response.txt
$ cat build/response.txt
4 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.
To set the skill ID of your skill to enable skill verification, set the following property in Micronaut (application.yml or environment variable) under the property name 'alexa.skill.id'.
You can find sample applications in Java, Kotlin and Groovy in the Examples directory of the repository. |
4.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 |