How to run Quarkus applications on Kubernetes

In this article we will learn how to deploy a Quarkus application on top of a Kubernetes cluster. We will start with a minimal REST application and then we will increase its complexity.

Hard requirements:

Before creating your Quarkus project, let’s start the Kubernetes environment:

minikube start
  minikube v1.24.0 on Fedora 29
     KUBECONFIG=/home/francesco/taskforce/install/auth/kubeconfig
. . . . .
  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Then, in order to build the Docker image using as target Minikube’s Docker instance, execute:

$ eval $(minikube docker-env)

Your Kubernetes environment is ready! Let’s bootstrap Quarkus.

Create the Quarkus Project

We will create a basic Quarkus project from the Command Line and we will progressively increase its complexity.

Firstly, let’s create a project “kubernetes-demo”:

mvn io.quarkus.platform:quarkus-maven-plugin:2.6.1.Final:create \
    -DprojectGroupId=com.sample \
    -DprojectArtifactId=kubernetes-demo \
    -DprojectVersion=1.0.0 \
    -DclassName="com.sample.RestDemo"

Next, we need to add Kubernetes and Container-Image extensions. If you are running a fully-fledged Kubernetes Cluster add the following extension:

$ mvn quarkus:add-extension -Dextensions="kubernetes, container-image-docker"

On the other hand, if you are running a Local Kubernetes Engine such as Minikube, the following extensions are tailored for your envornment:

$ mvn quarkus:add-extension -Dextensions="minikube, container-image-docker"

As we are using Minikube, we will go for the latter option.

In a nusthell, the kubernetes extension uses the "Always" image pull policy which forces pulling the Image each time. On the other hand, the minikube extension uses the "IfNotPresent" policy which will pull the image if the tag doesn’t already exist locally-  

The following additional dependencies are now in your project:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-minikube</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-container-image-jib</artifactId>
</dependency>

Your project includes, out of the box, the following REST Endpoint:

@Path("/hello")
public class GreetingResource {
	
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello RESTEasy";
    }
}

To build your application and deploy into Kubernetes execute:

$ mvn clean package -Dquarkus.kubernetes.deploy=true

Next, check from the build logs that the Deployment completed successfully:

[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Deploying to minikube server: https://192.168.49.2:8443/ in namespace: default.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: Service kubernetes-demo.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: Deployment kubernetes-demo.
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 7723ms

A Pod is now running in the default Kubernetes namespace:

$ kubectl get pod 
NAME                               READY   STATUS    RESTARTS   AGE
kubernetes-demo-85dfff747d-4xnjm   1/1     Running   0          9s

You can access the Pod logs using the “kubectl logs” command. You can also check your Pods from Kubernetes Dashboard:

$ minikube dashboard

Finally, in order to access your Pod from outside the Cluster, we will be using the Service kubernetes-demo:

$ kubectl get svc
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes        ClusterIP   10.96.0.1       <none>        443/TCP        47m
kubernetes-demo   NodePort    10.102.235.40   <none>        80:30962/TCP   83s

The simplest way to do that, is to configure a port forward towards the Kubernetes Service:

$ kubectl port-forward svc/kubernetes-demo 8080:80
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080

Now all requests to localhost:8080 will be forwarded to the the Service kubernetes-demo:

$ curl localhost:8080/hello
Hello RESTEasy

Customizing the application Configuration

As next step, we will show how to customize your Endpoint using a ConfigProperty which we will provide through a ConfigMap.

Firstly, modify your Endpoint to include a ConfigProperty:

@ConfigProperty(name = "greeting")
String greeting;
	
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {  	
      return greeting;
}

You can configure the property “greeting” in the application.properties file. However, in our example, we will show how to set that property using a ConfigMap.

For this purpose, create a file cm.yml to define your ConfigMap object:

apiVersion: v1
kind: ConfigMap
metadata:
  name: rest-demo
data:
  application.properties: |-
    greeting=HelloFromConfigMap

Next, add to your application.properties a reference to the “rest-demo” ConfigMap:

quarkus.kubernetes-config.enabled=true
quarkus.kubernetes-config.config-maps=rest-demo

Finally, include in your pom.xml the quarkus-kubernetes-config extension which allows developers to use Kubernetes ConfigMaps and Secrets as a configuration source:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-kubernetes-config</artifactId>
</dependency>

We are done. Before deploying the new application, create the ConfigMap:

$ kubectl create -f cm.yml

Deploy again the application and check that the Endpoint returns the value from the ConfigMap:

$ curl localhost:8080/hello
HelloFromConfigMap

Configuring Database Connectivity

The last enhancement to our application will be database connectivity. We will deploy a PostgreSQL Database in our Kubernetes namespace and connect it from our Quarkus application. To do that, here is our checklist:

  1. Deploy a PostgreSQL Service on Kubernetes
  2. Configure the Quarkus application to connect to the PostgreSQL service

Let’s start with Database set up!

Deploying PostgreSQL on Kubernetes

To deploy PostgreSQL on Kubernetes there are several valid strategies, such as Helm Charts. To keep our example as simple as possible, we will just create a YAML file for the Deployment, one for the Service and, finally, one to hold the credentials.

Let’s start from the credentials. Create a ConfigMap – postgres-configmap.yml – with the DB Credentials:

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRESQL_USER: quarkus_test
  POSTGRESQL_PASSWORD: quarkus_test
  POSTGRESQL_DATABASE: quarkus_test
When running Kubernetes project in a production system, you should use a Secret to store any sensitive information such as username/passwords

Next, create a Deployment with the file postgres-deployment.yml:

kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: centos/postgresql-96-centos7:latest
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 5432
          envFrom:
            - configMapRef:
                name: postgres-config

Finally, create a Service with the file postgres-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  type: NodePort
  ports:
   - port: 5432
  selector:
   app: postgres

Now, create all the above objects on your namespace:

$ kubectl create -f postgres-configmap.yml
configmap/postgres-config created
$ kubectl create -f postgres-service.yml 
service/postgres created
$ kubectl create -f postgres-deployment.yml 

Check the Pod status to verify that PostgreSQL is running:

$ kubectl get pods -w
NAME                       READY   STATUS    RESTARTS   AGE
postgres-cf987cb96-9r8vl   1/1     Running   0          58s

Set up Quarkus to connect to PostgreSQL

Firstly, we will configure our application to use Hibernate ORM and PostgreSQL JDBC Driver. We will therefore add the following dependencies:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

Next, most importantly, you need to include into the application.properties the JDBC Connection Properties matching with our PostgreSQL Database:

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus_test
%prod.quarkus.datasource.password=quarkus_test
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://postgres/quarkus_test
%prod.quarkus.datasource.jdbc.max-size=10
%prod.quarkus.datasource.jdbc.min-size=1

quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true

To verify Database Connectivity, we will create a minimal Entity and a Service to store a record in the PostgreSQL DB.

Here is our Greeting Entity:

@Entity
public class Greeting {
    private Long id;
    private String message;

    @Id
    @SequenceGenerator(name = "greetSeq", sequenceName = "greet_id_seq", allocationSize = 1, initialValue = 1)
    @GeneratedValue(generator = "greetSeq")
    public Long getId() {
        return id;
    }
    // Getters/Setters omitted for brevity

  
}

A minimalist Service to insert a Greeting into the DB follows here:

@ApplicationScoped
public class GreetingService {
	
	private static final Logger LOGGER = Logger.getLogger(GreetingService.class.getName());
	
	@Inject
	EntityManager em;

	@Transactional
	public void saveGreeting() {
		Greeting g = new Greeting();
		g.setMessage("An user greeting was sent at" +new Date());
		em.persist(g);
		LOGGER.info("The greeting was saved on DB");
		
	}
}

Finally, we will reach the Service every time there is a request for the hello Endpoint:

@Path("/hello")
public class GreetingResource {
    @ConfigProperty(name = "greeting")
    String greeting;
	
    @Inject
    GreetingService greetingService;
	
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
    	 greetingService.saveGreeting();
        return greeting;
    }
}

Our Quarkus application is ready. Deploy it on Kubernetes:

$ mvn clean package -Dquarkus.kubernetes.deploy=true

Next, wait until also the kubernetes-demo Pod is Running:

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
kubernetes-demo-7b49df995-p7ctd   1/1     Running   0          7s
postgres-cf987cb96-9r8vl          1/1     Running   0          5m54s

Access the Service again using the port-forward:

 curl localhost:8080/hello

In conclusion, if every step completed successfully, you will see from the Pod logs the Hibernate logs of your Insert:

2022-01-06 17:31:59,125 INFO  [com.sam.GreetingService] (executor-thread-0) The greeting was saved on DB
Hibernate: 
    insert 
    into
        Greeting
        (message, id) 
    values
        (?, ?)

Well done! you managed to deploy an Hibernate ORM Quarkus application on the top of a Kubernetes cluster!

If you want to learn how to deploy a Quarkus application on OpenShift, we recommend this article: Deploying Quarkus applications on OpenShift

Source code for this project: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/kubernetes-demo