Introduction
In the part 1 we introduced our sample application. We basically used AWS Lambda Functions like GetProductByIdHandler where we extended com.amazonaws.services.lambda.runtime.RequestHandler and injected DynamoProductDao and other services by using Jakarta EE jakarta.inject.Inject annotation. While this is a valid approach, sometimes you have existing Quarkus REST application which runs on containers or servers, and you'd like to port it to Serverless with as little effort as possible. As we use here DynamoDB we're locked-in, so it's more about making our application portable between AWS services like EC2, ECS (also with Fargate), EKS (also with Fargate) and Lambda. Of course, you can replace DynamoDB repository layer with RDS, Aurora, Aurora Serverless or newly released Aurora DSQL which will make business logic at least more portable to other cloud provider or datacenter. In this article, we'll show how to develop this type of application.
Sample REST API application with the Quarkus framework on AWS Lambda
I created a sample application which does exactly this. Instead of using Quarkus version 3.18, I updated the version to 3.24 which is the newest one at the time of writing.
Let's focus on the main difference. Instead of Lambda functions we now have Product Controller which source code is shown below:
@Path("/products")
public class ProductController {
@Inject
private ProductDao productDao;
@PUT
@Path("/")
@Consumes("application/json")
public Response createProduct(@Valid @NotNull Product product) {
productDao.putProduct(product);
return Response.status(Response.Status.CREATED).entity(product).build();
}
@GET
@Path("/{id}")
public Response getProductById(@RestPath String id) {
Optional<Product> optionalProduct = productDao.getProduct(id);
if (optionalProduct.isPresent()) {
return Response.ok(optionalProduct.get()).build();
}
else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
For it we use JSON REST Services which we added as a dependency in the pom.xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
The Product Controller has 2 methods: createProduct and getProductById. It uses familiar REST annotations like Consumes, GET, PUT, Path and PathParam.
The main difference in how the Controller and its methods are resolved. This happens by adding the following dependency in the pom.xml:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda-rest</artifactId>
</dependency>
Instead of having the dependency to quarkus-amazon-lambda which we used in the initial sample application from part 1, we now have the dependency quarkus-amazon-lambda-rest as we use API Gateway REST API (v1). In case you'd like to use API Gateway HTTP API (v2) we must replace dependency quarkus-amazon-lambda-rest with quarkus-amazon-lambda-http.
The mapping itself happens in the SAM template.
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
CodeUri: target/function.zip
.....
Generic Lambda function handler io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler defined in the Globals section of the Lambda functions maps the incoming request to the appropriate controller (we have only one ProductController) and its method. The mapping itself, for example for the GetProductByIdFunction Lambda function, works this way:
GetProductByIdFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: GetProductByIdWithWithQuarkus324Rest
Events:
GetRequestById:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /products/{id}
Method: get
The path and method defined in the Events section will be mapped to the appropriate path and method in the Controller. In our case the getProductById method of the ProductConroller has exact GET HTTP method annotation and the Path annotation "products/{id}" (if you consider the global Path annotation on the controller itself). PutProductFunction Lambda function mapping defined in the SAM template to the createProduct method of the ProductController works the same way.
In case you use do not use AWS cloud native services (storage, databases, queues and so on) you can use for example the quarkus-azure-functions to deploy the business logic to Azure functions or quarkus-google-cloud-functions extension to deploy the business logic to Google functions.
Now we build the application with mvn clean package (function.zip is created and stored in the subdirectory named target) and deploy it with sam deploy -g. We will see our customized Amazon API Gateway URL in the output. We can use it to create products and retrieve them by ID. The interface is secured with the API key. We must send the following as HTTP header: "X-API-Key: a6ZbcDefQW12BN56WEV324Rest", see MyApiKey definition in template.yaml. To create the product with ID=1, we can use the following curl query:
curl -m PUT -d '{ "id": 1, "name": "Print 10x13", "price": 0.15 }' -H "X-API-Key: a6ZbcDefQW12BN56WEV324Rest" https://{$API_GATEWAY_URL}/prod/products
For example, to query the existing product with ID=1, we can use the following curl query:
curl -H "X-API-Key: a6ZbcDefQW12BN56WEV324Rest" https://{$API_GATEWAY_URL}/prod/products/1
In both cases, we need to replace the {$API_GATEWAY_URL} with the individual Amazon API Gateway URL that is returned by the sam deploy -g command. We can also search for this URL when navigating to our API in the Amazon API Gateway service in the AWS console.
Conclusion
In this article, we learned how to develop a pure Quarkus REST application and deploy it on AWS Lambda. What I also observed is that by adding quarkus-rest-jackson and quarkus-amazon-lambda-rest (instead of quarkus-amazon-lambda) dependencies to the pom.xml, the deployment artifact size of our application increased from 14 to 26 MBs. It's also because a lot of additional dependencies to netty and vertx were used. I need to dig deeper whether I can exclude some of these dependencies for my application, as I expect significantly lower Lambda performance (especially higher cold start times), especially without enabling Lambda SnapStart and using advanced priming techniques.
If I'll find time in the future, I'll do the same Lambda function performance measurements as I did in the previous article:
- without Lambda enabling SnapStart
- with enabling SnapStart but without any priming
- with enabling SnapStart and with additional priming techniques
- by deploying our application as a GraalVM Native Image on the Lambda Custom Runtime.
Top comments (0)