Developing A REST Service with Quarkus and Deploying to Minikube
We, as software developers, are now designing more microservices than monolith applications. We also need to implement microservices more productively and reliably than before, with the technology stacks we are most familiar with.
In the Java world, to develop and deploy microservices, we often utilize existing frameworks that share environments such as application servers, non-optimized Java runtimes, dynamic and reflection and so on. These result in high RAM utilization and slow startup time, further reduce developers’ productivity and microservices’ stability.
Quarkus is a Java runtime that targets the needs of the Java microservices developers. It is designed for developers to be as productive as other technology stacks such as Node.js and can be optimized to utilize as few resources as possible.
Quarkus offers features like Live Coding, annotations, plugins and patterns to increase developers’ productivity, compilation to native binary for fast startup time and low memory usage, and straightforward deployment to various Kubernetes container orchestration platforms.
We also need to make sure that we can consistently apply to microservices the capabilities such as configurations, health checking, resilience, security, reactive and streaming, metrics and tracing , and so on. Quarkus implements Eclipse MicroProfile Platform specifications for microservices and offers those features for us to improve our productivity and microservices runtime efficiency.
This article will serve as our starting point to explore Quarkus’s features by creating a simple restful service and will cover the followings:
- Creating the project with the Quarkus Maven plugin and dependencies.
- Developing a simple REST service endpoint with Quarkus and Live Coding mechanism.
- Writing tests for the service.
- Deploying the service to Kubernetes (Minikube) and see it running.
The sample can be found at https://github.com/genekuo/customer-service.git .
Creating the project with the Quarkus Maven plugin and dependencies
First, we will use the Quarkus Maven plugin to create our project, which is a service implementing CRUD functionalities in memory and exposing REST APIs.
mvn io.quarkus:quarkus-maven-plugin:1.13.1.Final:create \
-DprojectGroupId=demo.quarkus.service.customer \
-DprojectArtifactId=customer-service \
-DclassName="demo.quarkus.service.customer.CustomerEndpoint" \
-Dpath="/customers"
The following directory structure will be generated and major files be described below:
- A project object model (
pom.xml
) that describes the project configuration. - A JAX-RS resource endpoint named
CustomerEndpoint.java
and a test class namedCustomerEndpointTest.java
, as well asNativeCustomerEndpointIT.java
for testing against the native executable. - A file named
application.properties
for application configuration. - An
index.html
file to serve as the initial static content. - A couple of
Dockerfiles
that allow us to create containers for our application.
We can see that there is a dependency, quarkus-resteasy
, in the pom.xml
, which allows us to develop and expose REST APIs. It is an implementation of the JAX-RS specification that is included by default when we create our project. We can use it to provide a representation of our service through the standard HTTP methods. We also manually include the quarkus-jsonb
dependency in the pom.xml
in order to produce JSON content for our REST endpoints and to create JSON objects in the test class.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
You can check the various goals of the Quarkus Maven plugin with the following command.
mvn -Dplugin=io.quarkus:quarkus-maven-plugin help:describe
We are ready to run our application with the following command to see its default behavior.
mvn compile quarkus:dev
The application will be compiled and executed in a few seconds. This also starts a debugger on port 5005. Now, we can request the endpoint with a tool such as curl.
curl http://localhost:8080/customers
Developing a simple REST service endpoint with Quarkus and Live Coding mechanism
While keeping our application running, we can start to develop the Customer service with Live Coding of Quarkus. Live Coding enables Java source, resources, and configuration of a running application to be updated, recompiled, and redeployed automatically, as soon as the application receives a new request. For example, refreshing the browser or issuing a new request.
Next, we will create a model class that represents a customer’s information. This is a Plain Old Java Object that will be stored in memory by the CustomerRepository
class shown next.
public class Customer {
private Integer id;
private String name;
private String surname;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
} @Override
public String toString() {
return "Customer{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", surname='" + surname + '\'' +
'}';
}
}
The CustomerRepository
here represents the core CRUD (Create, Retrieve, Update and Delete) functionalities which manage our model.
@ApplicationScoped
public class CustomerRepository {
List<Customer> customerList = new ArrayList();
int counter;
public int getNextCustomerId() {
return counter++;
}
public List<Customer> findAll() {
return customerList;
}
public Customer findCustomerById(Integer id) {
for (Customer c:customerList) {
if (c.getId().equals(id)) {
return c;
}
}
throw new CustomerException("Customer not found!");
}
public void updateCustomer(Customer customer) {
Customer customerToUpdate = findCustomerById(customer.getId());
customerToUpdate.setName(customer.getName());
customerToUpdate.setSurname(customer.getSurname());
}
public void createCustomer(Customer customer) {
customer.setId(getNextCustomerId());
findAll().add(customer);
}
public void deleteCustomer(Integer customerId) {
Customer c = findCustomerById(customerId);
findAll().remove(c);
}
}
Finally, we will create a REST endpoint over the CustomerRepository
class and define a method for each CRUD functionality. Each method in CustomerEndpoint
class is mapped to an appropriate HTTP method and exposes to clients to communicate with.
@Path("customers")
@ApplicationScoped
public class CustomerEndpoint {
@Inject
CustomerRepository customerRepository;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Customer> getAll() {
return customerRepository.findAll();
}
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Customer findById(@PathParam("id") Integer id) {
return customerRepository.findCustomerById(id);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(Customer customer) {
customerRepository.createCustomer(customer);
return Response.status(201).build();
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response update(Customer customer) {
customerRepository.updateCustomer(customer);
return Response.status(204).build();
}
@DELETE
public Response delete(@QueryParam("id") Integer customerId) {
customerRepository.deleteCustomer(customerId);
return Response.status(204).build();
}
}
While our application is still running, we can start sending requests to the CustomerEndpoints
to experience Live Coding mechanism and see our application work.
// Initially, the command will return nothing
curl http://localhost:8080/customers // We add a customer to our application
curl -v -X POST -H "Content-Type: application/json" \
-d '{"name":"Bill","surname":"John"}' \
http://localhost:8080/customers// Now we have a customer record
curl http://localhost:8080/customers// We can also find the customer by id
curl http://localhost:8080/customers/0// Then we can update the customer details
curl -v -X PUT -H "Content-Type: application/json" \
-d '{"id":0,"name":"Tom","surname":"Chris"}' \
http://localhost:8080/customers// Finally we delete the customer record
curl -v -X DELETE http://localhost:8080/customers?id=0
Writing tests for the service
We will write a quick test for the CustomerEndpoint
to verify that our code actually works. However, this simple test has not considered the best practices about unit testing or integration testing, such as grouping tests, isolation of test methods, detailed assertions and so on, which can be elaborated further in order to obtain high quality of tests. Instead, I will focus on describing the supports that Quarkus offers for testing. As we can in the pom.xml
, the following dependencies has been added by default to support unit testing our service.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
We have used Json.createObjectBuilder
API to create two JSON objects and serialize to strings that are sent to our CustomerEndpoint
, and to verify its functionalities.
@QuarkusTest
public class CustomerEndpointTest {
@Test
public void testCustomerService() {
JsonObject obj = Json.createObjectBuilder()
.add("name", "John")
.add("surname", "Smith").build();
// Test POST
given()
.contentType("application/json")
.body(obj.toString())
.when()
.post("/customers")
.then()
.statusCode(201);
// Test GET
given()
.when().get("/customers")
.then()
.statusCode(200)
.body(containsString("John"),
containsString("Smith"));
obj = Json.createObjectBuilder()
.add("id", 0)
.add("name", "Tom")
.add("surname", "Chris").build();
// Test PUT
given()
.contentType("application/json")
.body(obj.toString())
.when()
.put("/customers")
.then()
.statusCode(204);
// Test DELETE
given()
.contentType("application/json")
.when()
.delete("/customers?id=0")
.then()
.statusCode(204);
}
}
After we have our test class in place, we can perform our tests with the following command.
mvn clean test
Packaging and deploying the service to Kubernetes (Minikube)
It is time to package and deploy our Customer Service to Kubernetes. In order to package an application for deployment to Kubernetes, we will use Jib dependency.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-jib</artifactId>
</dependency>
Next, we will need two more dependencies to create a Kubernetes manifest YAML specific for Minikube development cluster for out application.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kubernetes</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-minikube</artifactId>
</dependency>
We can modify application.properties
to specify the Docker image name and Kubernetes service name when we run mvn clean install
command. The .yml
and .json
will be generated in /target/kubernetes
as the result.
quarkus.container-image.group=quarkus-mp
quarkus.container-image.name=customer-service
quarkus.kubernetes.name=customer-service
After the previous setup, we are ready to package our application and deploy to Minikube. We will start the Minkube cluster and make use of the Docker daemon inside Minikube with the following commands.
minikube starteval $(minikube -p minikube docker-env)
We can now use quarkus-container-image-jib
to create an image for JVM execution and check our image.
mvn clean package -Dquarkus.container-image.build=truedocker images // will show quarkus-mp/customer-service image created
Finally, we can deploy our application to Minikube and test it with the following command.
mvn clean package -Dquarkus.kubernetes.deploy=true
The above command will generate the necessary container and deploy to the Kubernetes cluster specified in the /[HOME]/.kube/config
.
We can now query our service URL exposed by Minikube and test it.
minikube service list
// Initially, the command will return nothing
curl http://[URL]/customers// We add a customer to our application
curl -v -X POST -H "Content-Type: application/json" \
-d '{"name":"Bill","surname":"John"}' \
http://[URL]/customers// Now we have a customer record
curl http://[URL]/customers// We can also find the customer by id
curl http://[URL]/customers/0// Then we can update the customer details
curl -v -X PUT -H "Content-Type: application/json" \
-d '{"id":0,"name":"Tom","surname":"Chris"}' \
http://[URL]/customers// Finally we delete the customer record
curl -v -X DELETE http://[URL]/customers?id=0
Summary
In this article, we went through our first Quarkus application, implemented a REST service, tested it and deployed to a single node Kubernetes cluster. We learned how to create a JAX-RS resource, use CDI Scope annotation @ApplicationScoped
on the JAX-RS resource class and the Repository class. We know how to use @Inject
annotation to inject the dependent CDI bean.
We also know that rest-assured
and the Quarkus JUnit extension can be used to create quality tests before packaging an application and deployment.
We demonstrated how to develop an application with Quarkus Live Coding, packaged our application with the quarkus-container-image-jib
dependency, generated manifest YAML files for Kubernetes cluster and for Minikube cluster specifically. Using these Quarkus extensions, we can go from application development, to testing, and to packaging and deployment, without any other tooling. These all make our application development much more productive.
Thanks for reading.