BLOG

Consuming a REST service with Quarkus using a native build to speed up AWS Lambda execution

Valentin Rachev, Software Engineer

Preface: 

A language that relies on typing like Java is a good choice for implementing microservices but when we decide to go serverless, the initialization time and memory usage can be problematic. Although there are ways to optimize your program’s startup on  JVM, choosing a native build has good benefits over the well-known Java virtual machine. A native image is a self-contained executable binary that holds everything it needs to run. 

Native images are compiled ahead of time and don’t execute on a Java VM , but on a Substrate VM runtime. The main advantages are startup time and lower runtime memory footprint. Keep in mind the heap for program data is the same. No magic can reduce that. Native-image utility statically analyzes dependencies, classes, and method usages to include in the native executable. The idea is to make expensive computations before (i.e., AOT compiled vs JIT). For the Community version – only Serial GC is used for garbage collection. In Enterprise – you have more options. 

What this is? This is not a detailed guide to starting with AWS Lambdas or Quarkus. I encourage you to start with this official Quarkus guide – https://quarkus.io/guides/amazon-lambda (I’ll call it the official Quarkus guide below). But the following paragraphs will hopefully help you understand some key ideas behind a native build and give you some practical tips. I’ll show you how to implement a basic microservice in Quarkus that consumes a third-party REST service, debug it locally, package it as a native image, and deploy it to AWS Lambda. 

Our task: 

Let’s say you need to make a microservice that hits a third-party REST HTTP API and gets some data to process further. This API will be secured in some way and you will need to keep the credentials somewhere safe. For brevity, I will only use a token-secured third party so I need to store one piece of string to use in my microservice. A natural choice is AWS Secrets Manager. But if your customer wants to run on a budget, setting up AWS Parameter Store would trim the bill a bit and you still have the option to store a secure string safely. 

Source: https://github.com/helecloud/blogposts/tree/master/quarkus-demo-rest-consume  

Steps: 

1. Run archetype creation as in the official Quarkus guide:

-mvn archetype:generate \

 -DarchetypeGroupId=io.quarkus \ 

 -DarchetypeArtifactId=quarkus-amazon-lambda-archetype \ 

 -DarchetypeVersion=2.7.5.Final 

2. You have a couple of possible starting points depending on what is convenient for you, but let’s add a DemoLambda class: 

 @Named("demo") 

 public class DemoLambda implements RequestHandler<Map<String, String>, OutputObject>

Set in application.properties: quarkus.lambda.handler=demo 

3. Add dependencies to help you consume the third-party REST API in pom.xml: 

 <dependency> 

 <groupId>io.quarkus</groupId> 

 <artifactId>quarkus-rest-client</artifactId> 

 </dependency> 

<dependency> 

 <groupId>io.quarkus</groupId> 

 <artifactId>quarkus-rest-client-jackson</artifactId> 

 </dependency> 

4. Add the interface for the third-party REST API:  

 @RegisterRestClient @ApplicationScoped public interface FoobarApiService { 

@GET @Path("/foobar/search") FoobarResponse searchDemoResource(@HeaderParam("Authorization") String authorization, @QueryParam("term") String term); 

// … see some more methods in source 

 and set in application.properties: 

 quarkus.rest-client."lambda_native_demo.service.FoobarApiService".uri=<thirdpartyuri> 

The other way is @RegisterRestClient(baseUri="thirdpartyuri") 

5. Add DemoService and @Inject the REST Client interface in it: 

 @Inject @RestClient FoobarApiService foobarApiService; 

6. Run with live reloading:  

 mvn quarkus:dev 

Send an empty event and see what happens: 

curl -d “{}” -X POST http://localhost:8080 

If you have configured your application correctly, you will get an output like this: 

{“result”:”final result here”,”requestId”:”8b06d212-1e89-4fdb-b893-78f024a64ffe”} 

If not – you might have missed some configuration e.g., the foobar api uri, the paths that the REST service hits, some AWS config like region etc…, but do not worry – the error will be obvious enough to handle on your own. 

7. Create your AWS Lambda Role and add policy for Systems Manager > Parameter read access 

8. Now, let’s get to the fun part of packaging: 

 mvn clean package -Dnative 

If successful, you’ll find a /target/manage.sh  script which can help you with creating, updating, deleting, or invoking your lambda function.  

Go to function cmd_create() at the lines where aws cli is used to create a lambda function: 

 aws lambda create-function 

It is quite odd that the region parameter is missing. In fact, it is not even mentioned in this AWS web documentation:  https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html. You have to dig deeper and find it here:  https://docs.aws.amazon.com/lambda/latest/dg/lambda-dg.pdf

So,  go ahead and add it yourself: 

--region ${AWS_REGION} 

Now that you’ve set the region in the create function of the manage script, run it: 

./target/manage.sh native create 

 This makes the first deployment of your packaged program as AWS Lambda. 

How you develop in Quarkus: 

While developing your program in Quarkus, you should keep in mind the main philosophy behind it: 

“The container first philosophy” – https://quarkus.io/vision/container-first 

The most important thing you should take from it and use in practice is this: 

Minimize reflection usage: 

Classes that are not directly used will be trimmed during the native image build. So, to include them, you usually annotate them with @RegisterForReflection

Misc. and other tips: 

Quarkus will give you handy hints, if you accidentally inject a private accessed member, e.g.: 

 “Found unrecommended usage of private members (use package-private instead) in application beans” – so: simply stick to a default access modifier. 

If you build a native image after every small code change, you will waste too much time. But if you are new to this kind of development – do it once in a while – you do not want to implement a whole lot of logic and realize that you are stepping out of a Substrate VM’s capabilities. Your end goal, after all, is to prepare a native image. 

Static resources: 

By default, static resources will not be added to the native image, so if you need to add static resources to it, simply include them in the application.properties: 

 e.g., quarkus.native.resources.includes=*.json 

Debugging: 

 Use quarkus-maven-plugin to compile and start up: 

 mvn quarkus:dev 

You know that by doing so, you are not running native 🙂 

Notice the output says:

Listening for transport dt_socket at address: 5005

– so you can attach to the running application process for remote debugging. : 

All modern IDEs support this. If you are using IntelliJ (Community Edition is sufficient): 

Run > Attach to a process 

Consuming a REST service with Quarkus using a native build to speed up AWS Lambda execution

And now you are ready to put breakpoints and have fun debugging your lambda. 

Specifics of the REST: 

This is how you set the REST client’s logs’ verbosity. Under the hood the REST client deals with http traffic via the Apache HTTP client: 

Set the logging level in application.properties: 

quarkus.log.category."org.apache.http".level=DEBUG 

Important things to consider before deciding to make a native build Quarkus lambda: 

– Check for available Quarkus extensions (depending on your processing needs) and their state of stability.  

Conclusion: 

If you are looking to speed up your Java Lambda startup time, footprint, and in the end, the monthly bill, a native image is a good solution. But you have to be prepared to juggle some dependencies. Do not forget the limitations of the Graal Substrate VM and the inherent static analysis for AOT compilation of your code – reflection is not your friend here.