-
Binding Metadata in Annotations
- Requirements for specifying binding information in a backing service CRD / Kubernetes resource
- Data model : Building blocks for expressing binding information
- A Sample CR : The Kubernetes resource that the application would bind to
- Scenarios
A Service Binding involves connecting an application to one or more backing services using a binding secret generated for the purpose of storing information to be consumed by the application.
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: binding-request
namespace: service-binding-demo
spec:
## Workload where binding information is
## injected.
application:
name: java-app
group: apps
version: v1
resource: deployments
## One or more Service resources from which
## binding information is collected
services:
- group: database.example.com
version: v1alpha1
kind: DBInstance
name: db
- group: database.example.com
version: v1alpha1
kind: DBCredentials
name: db
- group: "route.openshift.io"
version: v1
kind: Route
name: auth-service
The application workload expects binding metadata to be present on the Kubernetes Resources representing the backing service.
As shown above, you may also directly use a ConfigMap
or a Secret
itself as a service resource that would be used as a source of binding information.
If the backing service author has provided binding metadata in the corresponding CRD, then Service Binding acknowledges it and automatically creates a binding secret with the information relevant for binding.
The backing service may provide binding information as
- Metadata in the CRD as annotations
- Metadata in the OLM bunde manifest file as Descriptors
- Secret or ConfigMap
If the backing service provides binding metadata, you may use the resource as is
to express an intent to bind your workload with one or more service resources, by creating a ServiceBinding
.
If the backing service hasn't provided any binding metadata, the application author may annotate the Kubernetes resource representing the backing service such that the managed binding secret generated has the necessary binding information.
As an application author, you have a couple of options to extract binding information from the backing service:
- Decorate the backing service resource using annotations.
- Define custom binding variables.
In the following section, details of the above methods to make a backing service consumable for your application workload, is explained.
The application author may consider specific elements of the backing service resource interesting for binding
- A specific attribute in the
spec
section of the Kubernetes resource. - A specific attribute in the
status
section of the Kubernetes resource. - A specific attribute in the
data
section of the Kubernetes resource. - A specific attribute in a
Secret
referenced in the Kubernetes resource. - A specific attribute in a
ConfigMap
referenced in the Kubernetes resource.
As an example, if the Cockroachdb authors do not provide any binding metadata in the CRD, you, as an application author may annotate the CR/kubernetes resource that manages the backing service ( cockroach DB ).
The backing service could be represented as any one of the following:
- Custom Resources.
- Kubernetes Resources, such as
Ingress
,ConfigMap
andSecret
. - OpenShift Resources, such as
Routes
.
If the backing service doesn't expose binding metadata or the values exposed are not easily consumable, then an application author may compose custom binding variables using attributes in the Kubernetes resource representing the backing service.
The custom binding variables feature enables application authors to request customized binding secrets using a combination of Go and jsonpath templating.
Example, the backing service CR may expose the host, port and database user in separate variables, but the application may need to consume this information as a connection string.
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: multi-service-binding
namespace: service-binding-demo
spec:
application:
name: java-app
group: apps
version: v1
resource: deployments
services:
- group: postgresql.baiju.dev
version: v1alpha1
kind: Database
name: db-demo <--- Database service
id: postgresDB <--- Optional "id" field
- group: ibmcloud.ibm.com
version: v1alpha1
kind: Binding
name: mytranslator-binding <--- Translation service
id: translationService
mappings:
## From the database service
- name: JDBC_URL
value: 'jdbc:postgresql://{{ .postgresDB.status.dbConnectionIP }}:{{ .postgresDB.status.dbConnectionPort }}/{{ .postgresDB.status.dbName }}'
- name: DB_USER
value: '{{ .postgresDB.status.dbCredentials.user }}'
## From the translator service
- name: LANGUAGE_TRANSLATOR_URL
value: '{{ index translationService.status.secretName "url" }}'
- name: LANGUAGE_TRANSLATOR_IAM_APIKEY
value: '{{ index translationService.status.secretName "apikey" }}'
## From both the services!
- name: EXAMPLE_VARIABLE
value: '{{ .postgresDB.status.dbName }}{{ translationService.status.secretName}}'
## Generate JSON.
- name: DB_JSON
value: {{ json .postgresDB.status }}
This has been adopted in IBM CodeEngine.
In future releases, the above would be supported as volume mounts too.
The Service Binding Operator binds all information 'dependent' to the backing service CR by populating the binding secret with information from Routes, Services, ConfigMaps, and Secrets owned by the backing service CR if you express an intent to extract the same in case the backing service isn't annotated with the binding metadata.
This is how owner and dependent relationships are set in Kubernetes.
The binding is initiated by the setting this detectBindingResources: true
in the ServiceBinding
CR's spec
.
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: etcdbinding
namespace: service-binding-demo
spec:
detectBindingResources: true
application:
name: java-app
group: apps
version: v1
resource: deployments
services:
- group: etcd.database.coreos.com
version: v1beta2
kind: EtcdCluster
name: etcd-cluster-example
When this API option is set to true, the Service Binding Operator automatically detects Routes, Services, ConfigMaps, and Secrets owned by the backing service CR and generates a binding secret out of it.
The binding secret generated for the ServiceBinding
may be associated with the workload as environment variables or volume mounts ("files").
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: binding-request
namespace: service-binding-demo
spec:
application:
name: java-app
group: apps
version: v1
resource: deployments
services:
- group: charts.helm.k8s.io
version: v1alpha1
kind: Cockroachdb
name: db-demo
id: db_1
The generated binding secret would look like this:
kind: Secret
apiVersion: v1
metadata:
name: example-servicebindingrequest
namespace: pgo
data:
COCKROACHDB_CLUSTERIP: MTcyLjMwLjEwMS4zNA==
COCKROACHDB_CONF_PORT: MjYyNTc=
type: Opaque
This would generate a binding secret and inject it into the workload as an environment variable or a file based on bindAsFile
flag in the ServiceBinding
resource.
Here's how the environment variables look like:
$ env | grep COCKROACHDB
COCKROACHDB_CLUSTERIP=172.10.2.3
COCKROACHDB_CONF_PORT=8090
Here's how the mount paths look like:
bindings
├── <Service-binding-name>
│ ├── COCKROACHDB_CLUSTERIP
│ ├── COCKROACHDB_CONF_PORT
Instead of /bindings
, you can specify a custom binding root path by specifying the same in spec.mountPath
, example,
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: binding-request
namespace: service-binding-demo
spec:
bindAsFiles: true
application:
name: java-app
group: apps
version: v1
resource: deployments
services:
- group: charts.helm.k8s.io
version: v1alpha1
kind: Cockroachdb
name: db-demo
id: db_1
mounthPath: '/bindings/accounts-db' # User configurable binding root
Here's how the mount paths would look like, where applicable:
bindings
├── accounts-db
│ ├── COCKROACHDB_CLUSTERIP
│ ├── COCKROACHDB_CONF_PORT
Setting spec.bindAsFiles
to false
(default: true
) enables injecting gathered bindings as env variables into the application/workload.
For determining the folder where bindings should be injected, we can specify the destination using spec.mountPath
or we can use SERVICE_BINDING_ROOT
environment variable. If both are set then the SERVICE_BINDING_ROOT
environment variable takes the higher precedence.
The following table summarizes how the final bind path is computed:
spec.mountPath | SERVICE_BINDING_ROOT | Final Bind Path |
---|---|---|
nil | non-existent | /bindings/ServiceBinding_Name |
nil | /some/path/root | /some/path/root/ServiceBinding_Name |
/home/foo | non-existent | /home/foo |
/home/foo | /some/path/root | /some/path/root/ServiceBinding_Name |
Binding names declared through annotations or CSV descriptors are processed before injected into the application according to the following strategy
- names are upper-cased
- service resource kind is upper-cased and prepended to the name
example:
DATABASE_HOST: example.com
DATABASE
is backend service Kind
and HOST
is the binding name.
With custom naming strategy/templates we can build custom binding names.
Naming strategy defines a way to prepare binding names through ServiceBinding request.
- We have a nodejs application which connects to database.
- Application mostly requires host address and exposed port information.
- Application access this information through binding names.
###How ?
Following fields are part of ServiceBinding
request.
- Application
application:
name: nodejs-app
group: apps
version: v1
resource: deployments
- Backend/Database Service
namingStrategy: 'POSTGRES_{{ .service.kind | upper }}_{{ .name | upper }}_ENV'
services:
- group: postgresql.baiju.dev
version: v1alpha1
kind: Database
name: db-demo
Considering following are the fields exposed by above service to use for binding
- host
- port
We have applied POSTGRES_{{ .service.kind | upper }}_{{ .name | upper }}_ENV
naming strategy
.name
refers to the binding name specified in the crd annotation or descriptor..service
refer to the services in theServiceBinding
request.upper
is the string function used to postprocess the string while compiling the template string.POSTGRES
is the prefix used.ENV
is the suffix used.
Following would be list of binding names prepared by above ServiceBinding
POSTGRES_DATABASE_HOST_ENV: example.com
POSTGRES_DATABASE_PORT_ENV: 8080
We can define how that key should be prepared defining string template in namingStrategy
There are few naming strategies predefine.
none
- When this is applied, we get binding names in following form -{{ .name }}
host: example.com
port: 8080
uppercase
- This is by uppercase set when nonamingStrategy
is defined andbindAsFiles
set to false -{{ .service.kind | upper}}_{{ .name | upper }}
DATABASE_HOST: example.com
DATABASE_PORT: 8080
lowercase
- This is by default set whenbindAsFiles
set to true -{{ .name | lower }}
host: example.com
port: 8080
upper
- Capatalize all letterslower
- Lowercase all letterstitle
- Title case all letters.
If your application is to be deployed as a non-podSPec-based workload such that the containers path should bind at a custom location, the ServiceBinding
API provides an API to achieve that.
apiVersion: binding.operators.coreos.com/v1alpha1
kind: ServiceBinding
metadata:
name: binding-request
namespace: service-binding-demo
spec:
application:
resourceRef: example-appconfig
group: stable.example.com
version: v1
resource: appconfigs
bindingPath:
secretPath: spec.secret # custom path to secret reference
containersPath: spec.containers # custom path to containers reference
...
...
A detailed documentation could be found here.
If your application is using a custom resource and containers path should bind at a custom location, SBO provides an API to achieve that. Here is an example CR with containers in a custom location:
apiVersion: "stable.example.com/v1"
kind: AppConfig
metadata:
name: example-appconfig
spec:
containers:
- name: hello-world
image: yusufkaratoprak/kubernetes-gosample:latest
ports:
- containerPort: 8090
In the above CR, the containers path is at spec.containers
. You can specify
this path in the ServiceBindingRequest
config at
spec.application.bindingPath.containersPath
:
apiVersion: apps.openshift.io/v1alpha1
kind: ServiceBindingRequest
metadata:
name: binding-request
spec:
namePrefix: qiye111
application:
name: example-appconfig
group: stable.example.com
version: v1
resource: appconfigs
bindingPath:
containersPath: spec.containers
services:
- group: postgresql.baiju.dev
version: v1alpha1
kind: Database
name: example-db
id: zzz
namePrefix: qiye
After reconciliation, the spec.containers
is going to be updated with
envFrom
and secretRef
like this:
apiVersion: stable.example.com/v1
kind: AppConfig
metadata:
name: example-appconfig
spec:
containers:
- env:
- name: ServiceBindingOperatorChangeTriggerEnvVar
value: "31793"
envFrom:
- secretRef:
name: binding-request
image: yusufkaratoprak/kubernetes-gosample:latest
name: hello-world
ports:
- containerPort: 8090
resources: {}
If your application is using a custom resource and secret path should bind at a custom location, SBO provides an API to achieve that. Here is an example CR with secret in a custom location:
apiVersion: "stable.example.com/v1"
kind: AppConfig
metadata:
name: example-appconfig
spec:
secret: some-value-72ddc0c540ab3a290e138726940591debf14c581
In the above CR, the secret path is at spec.secret
. You can specify
this path in the ServiceBindingRequest
config at
spec.application.bindingPath.secretPath
:
apiVersion: apps.openshift.io/v1alpha1
kind: ServiceBindingRequest
metadata:
name: binding-request
spec:
namePrefix: qiye111
application:
name: example-appconfig
group: stable.example.com
version: v1
resource: appconfigs
bindingPath:
secretPath: spec.secret
services:
- group: postgresql.baiju.dev
version: v1alpha1
kind: Database
name: example-db
id: zzz
namePrefix: qiye
After reconciliation, the spec.secret
is going to be updated with
binding-request
as the value:
apiVersion: "stable.example.com/v1"
kind: AppConfig
metadata:
name: example-appconfig
spec:
secret: binding-request-72ddc0c540ab3a290e138726940591debf14c581
During a binding operation, annotations from relevant Kubernetes resources are extracted to gather information about what is interesting for binding. This information is eventually used to bind the application with the backing service by populating the binding Secret.
- Extract a string from the Kubernetes resource.
- Extract a string from the Kubernetes resource, and map it to custom name in the binding Secret.
- Extract an entire configmap/Secret from the Kubernetes resource.
- Extract a specific field from the configmap/Secret from the Kubernetes resource, and bind it as an environment variable.
- Extract a specific field from the configmap/Secret from the Kubernetes resource and and bind it as a volume mount.
- Extract a specific field from the configmap/Secret from the Kubernetes resource and map it to different name in the binding Secret.
- Extract a “slice of maps” from the Kubernetes resource and generate multiple fields in the binding Secret.
- Extract a "slice of strings" from a Kubernetes resource and indicate the content in a specific index in the slice to be relevant for binding.
-
path
: A template representation of the path to the element in the Kubernetes resource. The value ofpath
could be specified in either JSONPath or GO templates -
elementType
: Specifies if the value of the element referenced inpath
is of typestring
/sliceOfStrings
/sliceOfMaps
/map
. Defaults tostring
if omitted. -
objectType
: Specifies if the value of the element indicated inpath
refers to aConfigMap
,Secret
or a plain string in the current namespace! Defaults toSecret
if omitted andelementType
is a non-string
. -
bindAs
: Specifies if the element is to be bound as an environment variable or a volume mount using the keywordsenvVar
andvolume
, respectively. Defaults toenvVar
if omitted. -
sourceKey
: Specifies the key in the configmap/Secret that is be added to the binding Secret. When used in conjunction withelementType
=sliceOfMaps
,sourceKey
specifies the key in the slice of maps whose value would be used as a key in the binding Secret. This optional field is the operator author intends to express that only when a specific field in the referencedSecret
/ConfigMap
is bindable. -
sourceValue
: Specifies the key in the slice of maps whose value would be used as the value, corresponding to the value of thesourceKey
which is added as the key, in the binding Secret. Mandatory only ifelementType
issliceOfMaps
.
apiVersion: apps.kube.io/v1beta1
kind: Database
metadata:
name: my-cluster
spec:
...
status:
bootstrap:
- type: plain
url: myhost2.example.com
name: hostGroup1
- type: tls
url: myhost1.example.com:9092
name: hostGroup2
data:
dbConfiguration: database-config # configmap
dbCredentials: database-cred-Secret # Secret
url: db.stage.ibm.com
-
Requirement : Extract an entire configmap/Secret from the Kubernetes resource
Annotation:
“service.binding/dbcredentials”:”path={.status.data.dbcredentials},objectType=Secret”
Descriptor:
- path: data.dbcredentials x-descriptors: - urn:alm:descriptor:io.kubernetes:Secret - service.binding
-
Requirement : Extract an entire configmap/Secret from the Kubernetes resource
Annotation
“service.binding/dbConfiguration”: "path={.status.data.dbConfiguration},objectType=ConfigMap”
Descriptor
- path: data.dbConfiguration x-descriptors: - urn:alm:descriptor:io.kubernetes:ConfigMap - service.binding
-
Requirement : Extract a specific field from the configmap/Secret from the Kubernetes resource and use it as an environment variable.
Annotation
“service.binding/certificate”: "path={.status.data.dbConfiguration},objectType=ConfigMap"
Descriptor
- path: data.dbConfiguration x-descriptors: - urn:alm:descriptor:io.kubernetes:ConfigMap - service.binding:certificate:bindAs=envVar
-
Requirement : Extract a specific field from the configmap/Secret from the Kubernetes resource and use it as a volume mount.
Annotation
“service.binding/certificate”: "path={.status.data.dbConfiguration},bindAs=volume,objectType=ConfigMap"
Descriptor
- path: data.dbConfiguration x-descriptors: - urn:alm:descriptor:io.kubernetes:ConfigMap - service.binding:certificate:bindAs=volume
-
Use “db_timeout” from the ConfigMap “status.data.dbConfiguration” as “timeout” in the binding Secret.
Requirement: Extract a specific field from the configmap/Secret from the Kubernetes resource and map it to different name in the binding Secret
Annotation
“service.binding/timeout”: “path={.status.data.dbConfiguration},objectType=ConfigMap,sourceKey=db_timeout”
Descriptor
- path: data.dbConfiguration x-descriptors: - urn:alm:descriptor:io.kubernetes:ConfigMap - service.binding:timeout:sourceKey=db_timeout
-
Requirement: Extract a string from the Kubernetes resource.
Annotation
“service.binding/url”:"path={.status.data.url}"
Descriptor
- path: data.url x-descriptors: - service.binding
-
Requirement: Extract a string from the Kubernetes resource, and map it to custom name in the binding Secret.
Annotation
“service.binding/uri: "path={.status.data.connectionURL}”
Descriptor
- path: data.connectionURL x-descriptors: - service.binding:uri
-
Use specific elements from the CR’s “status.bootstrap” to produce key/value pairs in the binding Secret
Requirement: Extract a “slice of maps” from the Kubernetes resource and generate multiple fields in the binding Secret.
Annotation
“service.binding/endpoints”: "path={.status.bootstrap},elementType=sliceOfMaps,sourceKey=type,sourceValue=url"
Descriptor
- path: bootstrap x-descriptors: - service.binding:endpoints:elementType=sliceOfMaps:sourceKey=type:sourceValue=url
-
Requirement: Extract binding information from the Kubernetes resource using Go templates and generate multiple fields in the binding Secret.
A sample Kafka CR:
apiVersion: kafka.strimzi.io/v1alpha1 kind: Kafka metadata: name: my-cluster ... status: listeners: - type: plain addresses: - host: my-cluster-kafka-bootstrap.service-binding-demo.svc port: 9092 - host: my-cluster-kafka-bootstrap.service-binding-demo.svc port: 9093 - type: tls addresses: - host: my-cluster-kafka-bootstrap.service-binding-demo.svc port: 9094
Go Template:
{{- range $idx1, $lis := .status.listeners -}} {{- range $idx2, $adr := $el1.addresses -}} {{ $lis.type }}_{{ $idx2 }}={{ printf "%s:%s\n" "$adr.host" "$adr.port" | b64enc | quote }} {{- end -}} {{- end -}}
The above Go template produces the following string when executed on the sample Kafka CR:
plain_0="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9092>" plain_1="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9093>" tls_0="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9094>"
The string can then be parsed into key-value pairs to be added into the final binding secret. The Go template above can be written as one-liner and added as
{{GO TEMPLATE}}
in the annotation and descriptor below.Annotation
“service.binding: "path={.status.listeners},elementType=template,source={{GO TEMPLATE}}"
Descriptor
- path: listeners x-descriptors: - service.binding:elementType=template:source={{GO TEMPLATE}}
Requirement: Extract a “map” from the Kubernetes resource and generate multiple fields in the binding Secret.
Annotation
service.binding: path={.data},elementType=map