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";
}
}
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]
We can also confirm it works by doing an HTTP GET request on the running server:
> curl localhost:8080/hello
Hello from Quarkus REST%
- 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
- 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
- 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
- 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 .
- Push the built container to ECR
> docker push <AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/quarkus-lambda
- 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:
- Login into AWS, go to Lambda and click on Create 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:
- 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.
We'll create a new REST API, and let's keep it open for simplicity.
Our trigger is now created:
Let's open it:
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:
Next let's create a Proxy resource for our /hello
path and call it {my-api+}
:
We now have the new resource definition but no Integration setup, meaning it's not connected to anything in particular.
Let's make it invoke our Quarkus app inside the lambda function we've created!
- click on ANY below the
/{my-api+}
resource
- Click on Edit integration, select Lambda function, and choose our Lambda
IMPORTANT! make sure to select the Lambda proxy integration checkbox - otherwise you'll probably get a NullPointerException when invoking your Quarkus app
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:
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:
Let's also check the Lambda's log output in CloudWatch:
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)