Featured image for: Using Quarkus with the Service Binding Operator.

In the seven years since Kubernetes was released, there have been various efforts to simplify the process of consuming and binding to services from Kubernetes clusters. While discovering a service isn't much of an issue if you employ a well-known set of conventions, getting the credentials and other details required to access that service is sometimes trickier.

The Kubernetes Service Catalog was an attempt to simplify provisioning and binding to services, but it seems to have lost momentum. The lack of uniformity between providers, differences in how each service communicates binding information, and the fact that developers tend to favor operators for provisioning services all made the Service Catalog hard to use in practice.

The Service Binding Operator for Kubernetes and Red Hat OpenShift is a more recent initiative. It stays out of the way of service provisioning, leaving that to operators. Instead, it focuses on how to best communicate binding information to the application. An interesting part of the specification is the workload projection, which defines a directory structure that will be mounted to the application container when binding occurs in order to pass all the required binding information: type, URI, and credentials

Other parts of the specification are related to the ServiceBinding resource, which controls which services are bound to which application, and how.

Quarkus, which already supports the workload-projection part of the Service Binding specification, recently received enhancements for service binding. In this article, you'll learn how to automatically generate a ServiceBinding resource, then go through the whole process from installing operators to configuring and deploying an application.

A note about the example

In the example, you will use kind to install the Service Binding Operator and the Postgres Operator from Crunchy Data. After that, you will create a PostgreSQL cluster, and finally create a simple todo application, deploying it and binding it to the provisioned cluster. Before you begin, you may want to take a look at the Service Binding Operator Quick Start Guide.

Set up your clusters

Begin by creating a new cluster with kind. (If you've already created one, or don't use kind at all, you can skip this step.)

kind create cluster

You'll install both of the operators used in our example through the OperatorHub.

Install the Operator Lifecycle Manager

The first step is to install the Operator Lifecycle Manager:

curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.19.1/install.sh | bash -s v0.19.1

Install the Service Binding Operator

Next, you'll install the Service Binding Operator:

kubectl create -f https://operatorhub.io/install/service-binding-operator.yaml

Before moving to the next step, verify the installation with the following command:

kubectl get csv -n operators -w

When the phase of the Service Binding Operator is Succeeded, you can proceed.

Install the Postgres Operator

Use the following command to install the Postgres Operator from Crunchy Data:

kubectl create -f https://operatorhub.io/install/postgresql.yaml 

As you did before, you'll want to verify the installation:

kubectl get csv -n operators -w

When the phase of the operator is Succeeded, you can move to the next stage.

Create a PostgreSQL cluster

To begin this process, create a new namespace where you'll install your cluster and application:

kubectl create ns demo kubectl config set-context --current --namespace=demo

To create the cluster, you need to apply the following custom resource:


apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: pg-cluster
  namespace: demo
spec:
  image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres-ha:centos8-13.4-0
  postgresVersion: 13
  instances:
    - name: instance1
      dataVolumeClaimSpec:
        accessModes:
        - "ReadWriteOnce"
        resources:
          requests:
            storage: 1Gi
  backups:
    pgbackrest:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:centos8-2.33-2
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes:
            - "ReadWriteOnce"
            resources:
              requests:
                storage: 1Gi
      - name: repo2
        volume:
          volumeClaimSpec:
            accessModes:
            - "ReadWriteOnce"
            resources:
              requests:
                storage: 1Gi
  proxy:
    pgBouncer:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:centos8-1.15-2

This resource has been borrowed from the Service Binding Operator Quick Start Guide. Save that file as pg-cluster.yml and apply it using kubectl:

kubectl apply -f ~/pg-cluster.yml

Now check the pods to verify the installation:

kubectl get pods -n demo

Create the Quarkus application

Next, you'll create a simple Quarkus todo application that will connect to PostgreSQL via Hibernate and Panache. The todo application is a simple rest API for creating, reading, and deleting todo entries in a PostgreSQL database. It is heavily inspired by Clement Escoffier's Quarkus todo app but focuses less on presentation and more on the binding aspect.

Generate the application using the following Maven command:

mvn io.quarkus.platform:quarkus-maven-plugin:2.5.0.Final:create -DprojectGroupId=org.acme -DprojectArtifactId=todo-example -DclassName="org.acme.TodoResource" -Dpath="/todo"

Next, add all of the required extensions for connecting to PostgreSQL, generate the required Kubernetes resources, and build a container image for the application using Docker:

./mvnw quarkus:add-extension -Dextensions="resteasy-jackson,jdbc-postgresql,hibernate-orm-panache,kubernetes,kubernetes-service-binding,container-image-docker"

At this point, you need to create a simple entity:

package org.acme;

import javax.persistence.Column;
import javax.persistence.Entity;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Todo extends PanacheEntity {

    @Column(length = 40, unique = true)
    public String title;

    public boolean completed;

    public Todo() {
    }

    public Todo(String title, Boolean completed) {
        this.title = title;
    }
}

Then, expose it via REST:

package org.acme;

import javax.transaction.Transactional;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.util.List;

@Path("/todo")
public class TodoResource {

    @GET
    @Path("/")
    public List<Todo> getAll() {
      return Todo.listAll();
    }

    @GET
    @Path("/{id}")
    public Todo get(@PathParam("id") Long id) {
        Todo entity = Todo.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND);
        }
        return entity;
    }

    @POST
    @Path("/")
    @Transactional
    public Response create(Todo item) {
        item.persist();
        return Response.status(Status.CREATED).entity(item).build();
    }

    @GET
    @Path("/{id}/complete")
    @Transactional
    public Response complete(@PathParam("id") Long id) {
        Todo entity = Todo.findById(id);
        entity.id = id;
        entity.completed = true;
        return Response.ok(entity).build();
    }


    @DELETE
    @Transactional
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
        Todo entity = Todo.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND);
        }
        entity.delete();
        return Response.noContent().build();
    }
}

Bind to the target cluster

In order to bind the PostgreSQL service to the application, you must either provide a ServiceBinding resource or have it generated. To have the binding generated for you, you need to provide the following service coordinates:

  • apiVersion: postgres-operator.crunchydata.com/v1beta1
  • Kind: PostgresCluster
  • Name: pg-cluster, prefixed with quarkus.kubernetes-service-binding.services.<id>

You can see these coordinates in the following listing:

quarkus.kubernetes-service-binding.services.my-db.api-version=postgres-operator.crunchydata.com/v1beta1
quarkus.kubernetes-service-binding.services.my-db.kind=PostgresCluster
quarkus.kubernetes-service-binding.services.my-db.name=pg-cluster 

Note that the id is just used to group properties together and can be any text.

You also need to configure the datasource:

quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql

This sample application will not push the image to a registry, but just loads it to the cluster, so use IfNotPresent as the image pull policy:

quarkus.kubernetes.image-pull-policy=IfNotPresent

The application properties file should look like this:

quarkus.kubernetes-service-binding.services.my-db.api-version=postgres-operator.crunchydata.com/v1beta1
quarkus.kubernetes-service-binding.services.my-db.kind=PostgresCluster
quarkus.kubernetes-service-binding.services.my-db.name=pg-cluster

quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql

quarkus.kubernetes.image-pull-policy=IfNotPresent

Prepare for deployment

Before you deploy, you need to perform a container image build, load the image to the cluster, and generate the resource.

First, build the container image:

mvn clean install -Dquarkus.container-image.build=true -DskipTests

Note that this instruction assumes that you have Docker up and running.

Next, you'll load the Docker image to the cluster:

kind load docker-image iocanel/todo-example:1.0.0-SNAPSHOT

If you're using minikube instead of Docker, execute the following to rebuild the image:

eval $(minikube docker-env)

When using tools like kind or minikube, it is generally a good idea to change the image pull policy to IfNotPresent as you did in this example. Doing this avoids unnecessary pulls, because most of the time the image will be loaded from the local Docker daemon.

Deploy the application

Next, generate the deployment manifest, including the ServiceBinding, and apply them on Kubernetes:

mvn clean install -Dquarkus.kubernetes.deploy=true -DskipTests

Now, verify that everything is up and running:

kubectl get pods -n demo -w

Verify the installation

The simplest way to verify that everything works as expected is to port forward to a local HTTP port and access the /todo endpoint:

kubectl port-forward service/todo-example 8080:80

Point your browser to http://localhost:8080/todo and enjoy!

A look ahead

I am very excited about recent progress on the service binding front. In the near future, we may be able to reduce the amount of configuration necessary with the use of smart conventions (such as assuming that the custom resource name is the same as the database name unless explicitly specified otherwise) and a reasonable set of defaults (such as assuming that for PostgreSQL the default operator is Crunchy Data's Postgres Operator). In such a scenario, you could bind to services with no configuration without sacrificing flexibility or customizability. I hope you are as excited as I am by this prospect!

See the following resources to learn more about service binding and the Service Binding Operator:

Last updated: September 20, 2023