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 withquarkus.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