|
| 1 | +### Introduction |
| 2 | + |
| 3 | +Simple Java library to build native AWS Lambda functions that limit cold start issues for Java runtime. It uses Java 11 and adds support for AWS SDK V2, Jackson for JSON serialization and Guice as JSR-330 Dependency Injection provider. |
| 4 | + |
| 5 | +Please note that this is still proof of concept. |
| 6 | + |
| 7 | +### Usage |
| 8 | + |
| 9 | +rapid-lambda still requires you to implement `RequestHandler` interface from `aws-lambda-java-core` that contains your function logic. You still should use event classes from `aws-lambda-java-events` if you consumes events from other AWS services. All events have reflection configuration already prepared to be used by `native-image` plugin. |
| 10 | + |
| 11 | +Example bootstrap code: |
| 12 | +```java |
| 13 | +public class Function { |
| 14 | + |
| 15 | + public static void main(String[] args) { |
| 16 | + FunctionBootstrap.build(new MeasurementHandler(), APIGatewayProxyRequestEvent.class, FunctionConfiguration.newConfiguration(new Configuration())).bootstrap(); |
| 17 | + } |
| 18 | + |
| 19 | +} |
| 20 | +``` |
| 21 | + |
| 22 | +Please see `example` directory for more details. |
| 23 | + |
| 24 | +`native-image` requires reflection configuration for your classes that will be deserialized via ObjectMapper or injected via JSR-330 annotations. |
| 25 | + |
| 26 | +In order to build executable you need to run the following command (`JAVA_HOME` points to GraalVM location): |
| 27 | + |
| 28 | +```shell script |
| 29 | +mvn clean package |
| 30 | +$JAVA_HOME/bin/native-image -H:ReflectionConfigurationFiles=reflection-config.json -jar target/native-function-jar-with-dependencies.jar function.o |
| 31 | +``` |
| 32 | + |
| 33 | +### Deployment on AWS Lambda |
| 34 | + |
| 35 | +AWS Lambda custom runtime uses Amazon Linux 2 so in order to build native binary it has to be done on that OS e.g. on EC2 instance. |
| 36 | + |
| 37 | +### AWS SDK V2 |
| 38 | + |
| 39 | +Supported clients |
| 40 | +* DynamoDB |
| 41 | +* Lambda |
| 42 | +* S3 |
| 43 | +* SQS |
| 44 | +* SNS |
| 45 | +* SSM |
| 46 | +* EC2 |
| 47 | + |
| 48 | +In order to start using AWS Clients you need just to add maven dependency |
| 49 | + |
| 50 | +```xml |
| 51 | +<dependency> |
| 52 | + <groupId>software.amazon.awssdk</groupId> |
| 53 | + <artifactId>dynamodb</artifactId> |
| 54 | + <version>2.10.76</version> |
| 55 | +</dependency> |
| 56 | +``` |
| 57 | + |
| 58 | +and the just inject client in your service classes. |
| 59 | + |
| 60 | +```java |
| 61 | +@Inject |
| 62 | +private DynamoDbClient dynamoDbClient; |
| 63 | +``` |
| 64 | + |
| 65 | +### Dependency Injection |
| 66 | + |
| 67 | +rapid-lambda partially supports JSR-330 Dependency Injection provided by Google implementation named Guice. |
| 68 | + |
| 69 | +#### Available to inject |
| 70 | + |
| 71 | +* HttpClient from Java 11 |
| 72 | +* ObjectMapper |
| 73 | +* AWS SDK clients (if added as dependency) |
| 74 | + |
| 75 | +#### Configuration: |
| 76 | + |
| 77 | +Create binding configuration |
| 78 | + |
| 79 | +```java |
| 80 | +public class GlobalModule extends AbstractModule { |
| 81 | + |
| 82 | + @Override |
| 83 | + protected void configure() { |
| 84 | + bind(HttpClient.class).toInstance(HttpClient.newHttpClient()); |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +then HttpClient can be injected via @Inject annotation |
| 90 | + |
| 91 | +```java |
| 92 | +@Inject |
| 93 | +private HttpClient httpClient; |
| 94 | +``` |
| 95 | + |
| 96 | +Injecting multiple instances of single interface: |
| 97 | +```java |
| 98 | +public class Configuration extends AbstractModule { |
| 99 | + @Override |
| 100 | + protected void configure() { |
| 101 | + Multibinder<NotificationService> multibinder = Multibinder.newSetBinder(binder(), NotificationService.class); |
| 102 | + multibinder.addBinding().toInstance(new SnsNotificationService()); |
| 103 | + multibinder.addBinding().toInstance(new LambdaNotificationService()); |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +injection via |
| 109 | +```java |
| 110 | +@Inject |
| 111 | +private Set<NotificationService> notificationServices; |
| 112 | +``` |
| 113 | + |
| 114 | +##### Not supported: |
| 115 | + |
| 116 | +binding interfaces to implementation classes e.g. |
| 117 | +```java |
| 118 | +bind(Service.class).to(ServiceImpl.class); |
| 119 | +``` |
| 120 | + |
| 121 | +It requires creating proxies via CGLIB that is not supported by native-image plugin. As alternative you might use direct binding to new class instances e.g. |
| 122 | +```java |
| 123 | +bind(Service.class).annotatedWith(Names.named("service1")).toInstance(new Service1()); |
| 124 | +bind(Service.class).annotatedWith(Names.named("service2")).toInstance(new Service2()); |
| 125 | +``` |
| 126 | + |
| 127 | +then it can be autowired with @Named annotation |
| 128 | +```java |
| 129 | +@Inject |
| 130 | +@Named("service1") |
| 131 | +private Service service; |
| 132 | +``` |
| 133 | + |
| 134 | +As an alternative to Guice you might want to try Dagger 2 https://github.com/google/dagger as compile-time dependency injection library. |
| 135 | + |
| 136 | +#### Native-image |
| 137 | +Most of AWS services require HTTPS communication with their endpoints. In order to make it work with native binares generated by native-image libsunec.so and cacerts are added to custom runtime archive. More details https://quarkus.io/guides/native-and-ssl#the-sunec-library-and-friends |
| 138 | + |
| 139 | +#### Debuging |
| 140 | + |
| 141 | +native-image plugin is still a new tool in GraalVM portfolio and there is still room to improve. Many well-known libraries and frameworks from Java world are not supported. However there are some tricks that you may want to try if you face any issues with your code. You need to remember that your application is no longer pure AWS Lambda function but standalone Java application. GraalVM authors provide great tool to record all required configuration to build native binary called native-image-agent. You can launch your application as normal Java application with following configuration and it will create configuration files (Lambda endpoints simulator required, look above): |
| 142 | +```shell script |
| 143 | +java -agentlib:native-image-agent=config-output-dir=/tmp/native-image-config -jar target/native-function-jar-with-dependencies.jar |
| 144 | +``` |
| 145 | + |
| 146 | +#### Simulating Lambda endpoints |
| 147 | + |
| 148 | +Please read the following documentation https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html |
| 149 | + |
| 150 | +The quick way to write such simulator is to use SparkJava (http://sparkjava.com/) or Javalin (https://javalin.io/). |
| 151 | + |
| 152 | +Example code using Javalin: |
| 153 | + |
| 154 | +Maven dependency |
| 155 | +```xml |
| 156 | +<dependency> |
| 157 | + <groupId>io.javalin</groupId> |
| 158 | + <artifactId>javalin</artifactId> |
| 159 | + <version>3.8.0</version> |
| 160 | +</dependency> |
| 161 | +<dependency> |
| 162 | + <groupId>org.slf4j</groupId> |
| 163 | + <artifactId>slf4j-simple</artifactId> |
| 164 | + <version>1.7.28</version> |
| 165 | +</dependency> |
| 166 | +``` |
| 167 | + |
| 168 | +LambdaRuntimeSimulator.java |
| 169 | +```java |
| 170 | +public class LambdaRuntimeSimulator { |
| 171 | + |
| 172 | + private static final BlockingQueue<String> events = new LinkedBlockingQueue<>(); |
| 173 | + |
| 174 | + public static void main(String[] args) { |
| 175 | + Javalin app = Javalin.create().start(7000); |
| 176 | + app.get("/2018-06-01/runtime/init/error", LambdaRuntimeSimulator::logResponse); |
| 177 | + app.get("/2018-06-01/runtime/invocation/next", LambdaRuntimeSimulator::nextInvocation); |
| 178 | + app.post("/2018-06-01/runtime/invocation/:invocationId/response", LambdaRuntimeSimulator::logResponse); |
| 179 | + app.post("/2018-06-01/runtime/invocation/:invocationId/error", LambdaRuntimeSimulator::logResponse); |
| 180 | + app.post("/events", LambdaRuntimeSimulator::newEvent); |
| 181 | + } |
| 182 | + |
| 183 | + private static void nextInvocation(Context ctx) throws InterruptedException { |
| 184 | + ctx.header("Lambda-Runtime-Aws-Request-Id", UUID.randomUUID().toString()); |
| 185 | + ctx.result(events.take()); |
| 186 | + } |
| 187 | + |
| 188 | + private static void logResponse(Context ctx) { |
| 189 | + System.out.println(String.format("Response from %s, body: %s", ctx.fullUrl(), ctx.body())); |
| 190 | + } |
| 191 | + |
| 192 | + private static void newEvent(Context ctx) { |
| 193 | + final String body = ctx.body(); |
| 194 | + if (nonNull(body) && !body.isBlank()) { |
| 195 | + events.add(body); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +This code exposes `/events` endpoint that accepts next Lambda event. |
0 commit comments