Deploying native Quarkus REST API's in AWS Lambda
Tags: webdev, quarkus, aws, java
Published: 2024-07-09 19:55:04

In recent days or weeks, as someone relatively new to [AWS services](https://aws.amazon.com/products/?aws-products-all.sort-by=item.additionalFields.productNameLowercase&aws-products-all.sort-order=asc&awsf.re%3AInvent=*all&awsf.Free%20Tier%20Type=*all&awsf.tech-category=*all), I faced challenges running a [Quarkus](https://quarkus.io/) 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:

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

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

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

```log
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]
```

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

```bash
> curl localhost:8080/hello
Hello from Quarkus REST%
```

## 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](https://quarkus.io/guides/aws-lambda-http#deploying-a-native-executable), I encountered issues because I was developing on an ARM64 processor.

The part about [Deploying a native executable](https://quarkus.io/guides/aws-lambda-http#deploying-a-native-executable) didn't work with the `sam local start-api` or `sam deploy` commands.

Here's a [GitHub issue](https://github.com/quarkusio/quarkus/issues/38715) 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
```bash
> quarkus build --native --no-tests -Dquarkus.native.container-build=true
```
### 2. Login into AWS ECR
```bash
> aws ecr get-login-password --region <AWS_REGION> | docker login --username AWS --password-stdin <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com
```
### 3. Create repository on ECR
```bash
> aws ecr create-repository --repository-name quarkus-lambda
# you'll get a repository tag as a response which is used in the next steps
```
### 4. Build a container which runs the app in native mode.
```bash
> docker build -f src/main/docker/Dockerfile.native -t <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/quarkus-lambda .
```
### 5. Push the built container to ECR
```bash
> docker push <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/quarkus-lambda
```

## 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](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) or [Terraform](https://www.terraform.io).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufjqto24o37ztg7zumcy.png)

- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwrl79jf01jg0tm7chbo.png)

2. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nrypecf3jswv5sf6zodo.png)

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

Our trigger is now created:

![Lambda function trigger created](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orpc3ewcn3ma921bqnrd.png)

Let's open it:

![Lambda function trigger details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kk8dax77gdmbtzy2rcpa.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m6mjhseiu0eatey3swm.png)

Next let's create a **_Proxy resource_** for our `/hello` path and call it `{my-api+}`:

![Resources with created proxy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qexkccyjrz851si02ky9.png)

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

![Resource without integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69ajmr7xrp88fxucb6nj.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icgox57lqfe65kyuw6kp.png)

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

![Resource integration details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cwv5ssa2vz8fbevynk5.png)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7czc447p16lr0exoklpm.png)


## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/917ea7mkux4qx4ej2i46.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ghrfeg9he3py6nzbzwg.png)


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


![CloudWatch log output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9uzgx27fodzq6631mlo.png)

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.