Binding a service to a micro-service using odo
Tags: kubernetes, development, odo, binding
Published: 2021-09-05 16:02:31

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
```

Then, install the Service Binding Operator with the command:
```
$ kubectl create -f https://operatorhub.io/install/service-binding-operator.yaml
```

## 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
```

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
```

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
```

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
```

### 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
```

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
```

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
```

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
```

### 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
[...]
```

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
```

> 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
```

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
```

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

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
```

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

```
odo push
```

### 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
[...]
```

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"
```

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.