DEV Community

Patryk Szczypień
Patryk Szczypień

Posted on • Updated on

Deploying native Quarkus REST API's in AWS Lambda

In recent days or weeks, as someone relatively new to AWS services, I faced challenges running a Quarkus application I've been working on, on AWS Lambda.

From the start I wanted to run it as a native image, but developing the project on a laptop with an Apple Silicon chip caused some issues that I needed to overcome first.

Once this was sorted that out, the next problem was in resolving some internal server errors that occurred when invoking the Lambda containing my app.

Overall, it was a frustrating experience at times, but it was rewarding in the end. The app now runs super-fast inside a Lambda (about 0.5 seconds or less to invoke), and I've gained valuable knowledge about some AWS services.

I'm writing this article for two reasons:

  • As a solution reminder in case I forget how to solve these problems but need to do it again someday.
  • As a hopefully useful resource for someone (you?) who is currently struggling with the same problems I faced recently.

Let's dive in!

1. Create a Quarkus app

I won't go into much detail about how to create a Quarkus app since there are plenty of resources that explain it.

For a starter project, let's create an app called quarkus-lambda with the following extensions:

  • quarkus-rest
  • quarkus-amazon-lambda-rest

For simplicity, let's create just one REST endpoint in the app:

@Path("/hello")
public class RestApi {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from Quarkus REST";
    }
}
Enter fullscreen mode Exit fullscreen mode

When running it via quarkus dev we should confirm it uses Lambda:

2024-07-09 17:19:24,938 INFO  [io.qua.ama.lam.run.MockEventServer] (build-51) Mock Lambda Event Server Started
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2024-07-09 17:19:25,583 INFO  [io.qua.ama.lam.run.AbstractLambdaPollLoop] (Lambda Thread (DEVELOPMENT)) Listening on: http://localhost:8080/_lambda_/2018-06-01/runtime/invocation/next

2024-07-09 17:19:25,604 INFO  [io.quarkus] (Quarkus Main Thread) quarkus-lambda 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.12.1) started in 1.345s. 
2024-07-09 17:19:25,605 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2024-07-09 17:19:25,606 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [amazon-lambda, cdi, rest, security, smallrye-context-propagation, vertx]
Enter fullscreen mode Exit fullscreen mode

We can also confirm it works by doing an HTTP GET request on the running server:

> curl localhost:8080/hello
Hello from Quarkus REST%
Enter fullscreen mode Exit fullscreen mode

2. Build a native image of the app

This is where my first problems started to arise. When following guides like AWS LAMBDA WITH QUARKUS REST, UNDERTOW, OR REACTIVE ROUTES, I encountered issues because I was developing on an ARM64 processor.

The part about Deploying a native executable didn't work with the sam local start-api or sam deploy commands.

Here's a GitHub issue describing my problem.

Instead of deploying the necessary AWS services via sam, I decided to create a Docker image instead, push it to AWS Elastic Container Registry (ECR), and create the Lambda from it:

1. Build native image inside a docker container

> quarkus build --native --no-tests -Dquarkus.native.container-build=true
Enter fullscreen mode Exit fullscreen mode

2. Login into AWS ECR

> aws ecr get-login-password --region <AWS_REGION> | docker login --username AWS --password-stdin <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

3. Create repository on ECR

> aws ecr create-repository --repository-name quarkus-lambda
# you'll get a repository tag as a response which is used in the next steps
Enter fullscreen mode Exit fullscreen mode

4. Build a container which runs the app in native mode.

> docker build -f src/main/docker/Dockerfile.native -t <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/quarkus-lambda .
Enter fullscreen mode Exit fullscreen mode

5. Push the built container to ECR

> docker push <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/quarkus-lambda
Enter fullscreen mode Exit fullscreen mode

3. Create the Lambda function on AWS

Now we need to create a Lambda function to run our application when invoked.

The best way to do it would probably be via an IaaS script using CloudFormation or Terraform.

Here we'll do it manually by clicking around in the AWS console:

  1. Login into AWS, go to Lambda and click on Create function

Create AWS Lambda function

  • Choose Container image to create the function from
  • Enter a function name
  • Browse images to find the image container we created and pushed to ECR
  • (important for Mac / Apple Silicon) choose arm64 as the Architecture
  • Click on Create function

You should see a result like this:

AWS Lambda function page

  1. Create a trigger for the Lambda function

We need to invoke the lambda somehow and want to use our /hello endpoint for this.
To do it we must create an API Gateway trigger and configure it:

  • Click on Create Trigger and select API Gateway as the source.

Create trigger

We'll create a new REST API, and let's keep it open for simplicity.

Our trigger is now created:

Lambda function trigger created

Let's open it:

Lambda function trigger details

The resources basically define paths of our application like v1/api/orders/list, /v1/api/recipes/edit/1, etc.

On this page we need to differentiate between "static" paths that don't change much, like /v1/api and "dynamic" ones like /orders/list and /recipes/edit/1.
For better differentiation in this case you would probably do /v1/api/orders and /v1/recipes as the static parts and /list and /edit/1 as dynamic.

But what matters here, is that we need to declare the dynamic parts as proxy resources.

In our app we only have one endpoint /hello and no /quarkus-lambda endpoint - so let's delete it:

Resources after deletion

Next let's create a Proxy resource for our /hello path and call it {my-api+}:

Resources with created proxy

We now have the new resource definition but no Integration setup, meaning it's not connected to anything in particular.

Resource without integration

Let's make it invoke our Quarkus app inside the lambda function we've created!

  • click on ANY below the /{my-api+} resource

Resource details view

  • Click on Edit integration, select Lambda function, and choose our Lambda

Resource integration details

IMPORTANT! make sure to select the Lambda proxy integration checkbox - otherwise you'll probably get a NullPointerException when invoking your Quarkus app

Proxy integration checkbox

4. Test your endpoint

With the integration to the Lambda function covered, we can now test our endpoint.
An easy way to do this, is to select ANY below our /{my-api+} proxy resource, and then select the Test tab:

Lambda test tab

We fill the {my-api+} value with our only endpoint in the application - hello, set the method type to GET and click the "Test" button.

When everything was setup properly, we should get the following response from our Lambda function:

API Gateway response

Let's also check the Lambda's log output in CloudWatch:

CloudWatch log output

As you can see, it takes about 0,5s to run our Quarkus app when invoking the lambda.

And that's it - I hope this is helpful to someone!

Feel free to comment or message me, if you have any questions.

I'll probably write another article soon about calling various AWS services like S3, DynamoDB, Cognito, etc., from a Quarkus app running inside a Lambda function.

Top comments (0)