DEV Community

Philippe MARTIN
Philippe MARTIN

Posted on

Binding a service to a micro-service using odo

In this article, we will see how to bind a service to a micro-service, using the odo development tool.

We will first see how the Service Binding Operator can be used to bind to a PostgreSQL instance started from a standalone Deployment, then to a PostgreSQL instance started from an Operator.

Next, we will see how to start the Operator backed PostgreSQL service from odo, and how to bind our micro-service to this PostgreSQL service using odo.

The Service Binding Operator

The Service Binding Operator is an operator that helps the developer get the values of the different parameters exposed by a service.

For example, when you have a PostgreSQL service deployed, you need to know its host, database name, user and password to work with the service. If the service provides which information are to be exposed, the Service Binding Operator is able to get the exposed values and provide them to your micro-service.

Installing the Service Binding Operator

You can install this operator with the help of the Operator Lifecycle Manager. For this, first install, if necessary, this manager with the command:

$ curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.18.3/install.sh | bash -s v0.18.3 
Enter fullscreen mode Exit fullscreen mode

Then, install the Service Binding Operator with the command:

$ kubectl create -f https://operatorhub.io/install/service-binding-operator.yaml 
Enter fullscreen mode Exit fullscreen mode

Deploying a standalone PostgreSQL instance

Let's deploy a single PostgreSQL instance using the official postgres image, available at https://hub.docker.com/_/postgres.

We can deploy an instance by defining our own database name, user and password with the following manifest:

apiVersion: apps/v1 kind: Deployment metadata: labels: app: postgres name: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - image: postgres name: postgres env: - name: POSTGRES_PASSWORD value: a-super-secret - name: POSTGRES_USER value: user1 - name: POSTGRES_DB value: db1 volumeMounts: - mountPath: /var/lib/postgresql/data name: pgdata volumes: - name: pgdata emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: postgres-svc spec: selector: app: postgres ports: - port: 5432 targetPort: 5432 
Enter fullscreen mode Exit fullscreen mode

When the service is running, you can run another container that will be ready to connect to it using psql, with the manifest:

apiVersion: apps/v1 kind: Deployment metadata: name: client spec: selector: matchLabels: app: client template: metadata: labels: app: client spec: containers: - name: client image: postgres command: ["bash", "-c", "sleep $((10**10))"] env: - name: PGPASSWORD value: a-super-secret - name: PGHOST value: postgres-svc - name: PGDATABASE value: db1 - name: PGUSER value: user1 
Enter fullscreen mode Exit fullscreen mode

Note that we have defined the environment variables supported by psql to define the host, database name, username and password.

You finally can connect to the database with the command:

kubectl exec -it deployment/client psql 
Enter fullscreen mode Exit fullscreen mode

The environment variables defined into the manifest for the container will be used by psql to connect to the desired service.

But with this method, the developer needs to know the values for these different parameters, when running the client. Let's see how these variables can be automatically injected into the pod, using the Service Binding Operator.

Annotating the service resource

The first step is to annotate the postgres Deployment, to indicate which parameters should be exposed:

apiVersion: apps/v1 kind: Deployment metadata: annotations: service.binding/pguser: path={.spec.template.spec.containers[0].env[?(@.name=="POSTGRES_USER")].value} service.binding/pgpassword: path={.spec.template.spec.containers[0].env[?(@.name=="POSTGRES_PASSWORD")].value} service.binding/pgdatabase: path={.spec.template.spec.containers[0].env[?(@.name=="POSTGRES_DB")].value} labels: app: postgres name: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - image: postgres name: postgres env: - name: POSTGRES_PASSWORD value: a-super-secret - name: POSTGRES_USER value: user1 - name: POSTGRES_DB value: db1 volumeMounts: - mountPath: /var/lib/postgresql/data name: pgdata volumes: - name: pgdata emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: postgres-svc spec: selector: app: postgres ports: - port: 5432 targetPort: 5432 
Enter fullscreen mode Exit fullscreen mode

Linking to the annotated service

Then, you can create a client2 deployment, which does not define the database name, user and password:

apiVersion: apps/v1 kind: Deployment metadata: name: client2 spec: selector: matchLabels: app: client2 template: metadata: labels: app: client2 spec: containers: - name: client2 image: postgres command: ["bash", "-c", "sleep $((10**10))"] env: - name: PGHOST value: postgres-svc 
Enter fullscreen mode Exit fullscreen mode

Finally, you can create a servicebinding resource, to define a link between the Postgres service and the client2 deployment:

apiVersion: binding.operators.coreos.com/v1alpha1 kind: ServiceBinding metadata: name: binding-request spec: bindAsFiles: false namingStrategy: '{{ .name | upper }}' application: name: client2 group: apps version: v1 resource: deployments services: - group: apps version: v1 kind: Deployment name: postgres 
Enter fullscreen mode Exit fullscreen mode

By applying this manifest, the client2 pod will be restarted, and environment variables will be injected through a secretRef, by the Service Binding Operator:

$ kubectl get deployment client2 -o yaml [...] spec: containers: - command: - bash - -c - sleep $((10**10)) env: - name: PGHOST value: postgres-svc envFrom: - secretRef: name: binding-request-703eda68 $ kubectl get secret binding-request-703eda68 -o yaml apiVersion: v1 kind: Secret metadata: name: binding-request-703eda68 data: PGDATABASE: ZGIx PGPASSWORD: YS1zdXBlci1zZWNyZXQ= PGUSER: dXNlcjE= type: Opaque 
Enter fullscreen mode Exit fullscreen mode

Now, you can connect to the service from the client2 deployment, without the need to know the credentials.

Deploying an Operator backed PostgreSQL service

Operator backed services are services managed by a Kubernetes operator.

You can find such Operator backed services on https://operatorhub.io/. For this example, we won't use operatorhub, but deploy manually the operator found at https://github.com/operator-backing-service-samples/postgresql-operator.

To deploy the PostgreSQL operator using the OLM framework, we first need to declare the catalog containing the Operator, then subscribe to this specific Operator:

kubectl apply -f - << EOD -------- apiVersion: operators.coreos.com/v1alpha1 kind: CatalogSource metadata: name: sample-db-operators namespace: olm spec: sourceType: grpc image: quay.io/redhat-developer/sample-db-operators-olm:v1 displayName: Sample DB Operators -------- apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: pg namespace: operators spec: channel: stable name: db-operators source: sample-db-operators sourceNamespace: olm installPlanApproval: Automatic EOD 
Enter fullscreen mode Exit fullscreen mode

Examining the annotations on the Database custom resource

The PostgreSQL operator comes with a custom resource definition (CRD): Database. Let's examine the definition of this resource with the command:

$ kubectl get crd databases.postgresql.baiju.dev -o yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: databases.postgresql.baiju.dev annotations: service.binding/db.host: path={.status.dbConfigMap},objectType=ConfigMap service.binding/db.name: path={.status.dbConfigMap},objectType=ConfigMap service.binding/db.password: path={.status.dbConfigMap},objectType=ConfigMap service.binding/db.port: path={.status.dbConfigMap},objectType=ConfigMap service.binding/db.user: path={.status.dbConfigMap},objectType=ConfigMap service.binding/dbConnectionIP: path={.status.dbConnectionIP} service.binding/dbConnectionPort: path={.status.dbConnectionPort} service.binding/dbName: path={.spec.dbName} service.binding/password: path={.status.dbCredentials},objectType=Secret service.binding/user: path={.status.dbCredentials},objectType=Secret [...] 
Enter fullscreen mode Exit fullscreen mode

We can find that the CRD get a list of service.binding annotations, that will be used by the Service Binding Operator to bind instances of this resource to micro-services.

Note that, earlier in this article, annotations were placed in the Deployment instance itself; annotations are now placed in the definition of the Database resource (if you are an object-oriented developer, think about Classes and Instances in place of Definition and Instance).

Creating a Database instance with odo

The odo tool can create instances of Operator backend services.

You can get the list of available Operators on your cluster with the command:

$ odo catalog list services Services available through Operators NAME CRDs postgresql-operator.v0.0.9 Database service-binding-operator.v0.9.1 ServiceBinding, ServiceBinding 
Enter fullscreen mode Exit fullscreen mode

Note that this command displays the list of ClusterServiceVersions resources contained in the current namespace in the Succeeded phase. The Succeeded phase can take a few minutes to appear, please be patient; you can check with the command:

$ kubectl get csv NAME DISPLAY VERSION REPLACES PHASE postgresql-operator.v0.0.9 PostgreSQL Database 0.0.9 Succeeded service-binding-operator.v0.9.1 Service Binding Operator 0.9.1 service-binding-operator.v0.9.0 Succeeded 
Enter fullscreen mode Exit fullscreen mode

With odo, services are attached to components; before to deploy a service, you need to create a component. You can follow this article to create your component, and get an example of sources in the repository https://github.com/feloy/nest-odo-example.

Now, you can create an instance of a PostgreSQL database with:

$ odo service create postgresql-operator.v0.0.9/Database db1 Successfully added service to the configuration; do 'odo push' to create service on the cluster 
Enter fullscreen mode Exit fullscreen mode

This will create an instance with the default parameters. If you want to use a specific configuration, you can get the possible parameters with the command:

$ odo catalog describe service postgresql-operator.v0.0.9/Database Kind: Database Version: v1alpha1 Description: Describes how an application component is built and deployed. Parameters: PATH DISPLAYNAME DESCRIPTION image PostgreSQL database image PostgreSQL database image imageName PostgreSQL database image name PostgreSQL database image name dbName DB name Desired database name. If not provided, default value 'postgres' will be used. 
Enter fullscreen mode Exit fullscreen mode

You can now create an instance with a specific database name with the command:

$ odo service create postgresql-operator.v0.0.9/Database db1 -p dbName=mydb -p imageName=postgresql -p image=postgres 
Enter fullscreen mode Exit fullscreen mode

Finally, you can deploy the service into the Kubernetes cluster:

odo push 
Enter fullscreen mode Exit fullscreen mode

Binding the service with odo

This odo command will create the Service Binding resource necessary to bind the PostgreSQL service to your micro-service:

$ odo link Database/db1 --name dblink ✓ Successfully created link between component "nodejs-prj1-api-hjtd" and service "Database/db1" $ odo push $ kubectl get sbr dblink -o yaml kind: ServiceBinding metadata: name: dblink [...] spec: application: group: apps name: nodejs-prj1-api-hjtd-app resource: deployments version: v1 bindAsFiles: false detectBindingResources: true services: - group: postgresql.baiju.dev kind: Database name: db1 version: v1alpha1 [...] 
Enter fullscreen mode Exit fullscreen mode

You can examine that the environment variables have been defined into your micro-service:

$ odo exec -- bash -c export | grep DATABASE_ declare -x DATABASE_CLUSTERIP="10.107.65.179" declare -x DATABASE_DBCONNECTIONIP="10.107.65.179" declare -x DATABASE_DBCONNECTIONPORT="5432" declare -x DATABASE_DBNAME="mydb" declare -x DATABASE_PASSWORD="password" declare -x DATABASE_USER="postgres" 
Enter fullscreen mode Exit fullscreen mode

The developer can now use these variables into the code of the micro-service, and does not need to declare the values for these variables when deploying the micro-service, as the injection of the environment variables will be managed by the Service Binding Operator.

Top comments (0)