With the Kyma Runtime, you can use services offered by the SAP Business Technology Platform (SAP BTP). Previously service bindings were limited to the SAP BTP Cloud Foundry environment. Learn how to consume them in your application running in the SAP BTP Kyma environment.
Introduction
SAP BTP services run outside of your Kyma cluster, just like services you can consume from a Hyperscaler. Today it’s not possible to integrate them seamlessly in your service mesh. Even if it would be possible, other service-specific information is required in the application. Therefore, a mechanism is required to provide your application with the required information — referred to as “service credentials”.
Credentials are stored in a Kubernetes secret and mapped to a bunch of environment variables. However, this is not suitable if frameworks are used to access credentials, such as the SAP Cloud Application Programming Model (CAP) or Spring Boot.
The Cloud Foundry Approach
Cloud Foundry has a thing called “service bindings” that bind service instances to applications. The data of all services bound to an application is provided as a JSON object via an environment variable (VCAP_SERVICES
). Applications just need to parse the value of that environment variable.
Service bindings consist of metadata and credentials. Metadata are, for example, the type of the service offering (label
), the service plan, a list of tags, and the name of the service instance.
servicebinding.io Service Bindings
Kubernetes doesn’t have such a mechanism. And here comes servicebinding.io into the play: It’s a community lead effort to standardize how services are bound to Kubernetes applications, or more precisely: Kubernetes workloads.
Basically, that means:
A container must have an environment variable SERVICE_BINDING_ROOT
that points to the root directory for all service bindings.
For example, you set in your container spec:
...
env:
- name: SERVICE_BINDING_ROOT
value: /bindings
Bindings have to be put in a sub directory of that directory. The sub directory has a file for each property of the binding. Usually, it’s mounted to a volume created from a Kubernetes secret.
There are some conventions for binding properties:
- There must be a property
type
that contains the type of the service. You can compare this to thelabel
property in Cloud Foundry. - Optionally, there is a property
provider
to indicate the provider of this service. - There are rules for a few well-known properties.
- Any additional properties can have arbitrary content.
With that you can use type
and provider
to look up the desired service. For example, if you’re looking for the hana
service, you can search for a binding with type hana
.
Let’s have a look at an example directory structure for an SAP HANA binding:
/bindings/
my-hana-binding/
type: hana
url: http://hana.ondemand.com
user: me
password: secret
another-binding/
...
...
This works for SAP BTP services as well. Compared to Cloud Foundry, the label
property containing the service type must be renamed to type
.
But there are a few shortcomings that I wanted to mention:
The Metadata Extension
SAP BTP services have additional metadata, such as the service plan or tags, that you might want to use as a search criterion. For example, if your application supports different databases you could use the tag db
to search for a database service.
Bindings of SAP BTP services do not just contain strings, they can be structured using sub objects and arrays, and they can have boolean or numeric properties. Kubernetes secrets do not support typing and thus you need service-specific knowledge to deserialize the credentials according to its type.
Well, that’s not a problem if you write the code for specific service bindings. But it’s a problem for frameworks and libraries that read the bindings and expose them via an API. If your application was implemented for Cloud Foundry and you want to port it to Kyma, you don’t want to change the code to read the bindings. You expect the libraries to do the job for you.
To support that, SAP has implemented an extension to servicebinding.io. It just adds another property .metadata
that contains a JSON string explaining the kind and the formatting of each property. A property either belongs to the metadata, like type
or plan
, or to the credentials, like user
or password
. It can be formatted as a string or as a JSON value. String type properties are always stored with the string
format. The JSON value formatting allows to store booleans, numbers, sub objects, and arrays in a binding value.
For example:
/bindings/
my-funny-binding/
.metadata: /* see below */
type: funny-service
tags: ["funny", "somewhat", "useful"]
user: me
a_number: 3
a_boolean: true
an_object: { "property1": "value 1", "property2": true }
.metadata
File Content:
{
"metaDataProperties": [
{ "name": "type", "format": "text" },
{ "name": "tags", "format": "json" }
],
"credentialProperties": [
{ "name": "user", "format": "text" },
{ "name": "a_number", "format": "json" },
{ "name": "a_boolean", "format": "json" },
{ "name": "an_object", "format": "json" }
]
}
This helps libraries like @sap/xsenv
and CAP
to parse the bindings and provide them through their APIs in a compatible manner.
The good news is that it’s already implemented, and you can use it right now.
In the Kyma Runtime, secrets of SAP BTP service bindings are already created with the .metadata
property and contain all metadata you know from Cloud Foundry. This was introduced with the SAP BTP Service Operator version v0.2.3.
Library Support
You can consume bindings in your Node.js, Java, and CAP Node.js applications:
Language | Library | Min Version |
---|---|---|
Node.js | CAP | 6.0.1 |
Node.js | @sap/xsenv |
3.3.2 |
Node.js | @sap/hdi-deploy |
4.4.1 |
Node.js | @sap/html5-app-deployer |
4.1.2 |
Node.js | @sap/sbf |
6.5.2 |
Java | BTP Env Variable Access | 0.4.1 |
These libraries allow you to consume servicebinding.io bindings without the .metadata
property extension as well. In that case, they treat all properties as strings. The properties type
and provider
are handled as metadata and all other properties as strings.
End-to-End Example
If you want to see how this works end-to-end, you can follow the next steps to create a service and see how it can be consumed with @sap/xsenv
in your Kyma Runtime cluster.
Create a Service Instance
The first step is to create a service instance.
- Create a file
xsuaa-instance.yaml
:apiVersion: services.cloud.sap.com/v1 kind: ServiceInstance metadata: name: xsuaa-instance spec: serviceOfferingName: xsuaa servicePlanName: application
- Apply it to your cluster:
kubectl apply -f xsuaa-instance.yaml
- Check the state of your service instance:
kubectl get serviceinstance
- After short time, it should look like this:
NAME OFFERING PLAN STATUS READY AGE xsuaa-instance xsuaa application Created True 28s
Create a Service Binding
Next up, create a service binding for your service instance. This automatically creates a Kubernetes secret with the binding metadata and credentials.
- For the binding, create a file
xsuaa-binding.yaml
:apiVersion: services.cloud.sap.com/v1 kind: ServiceBinding metadata: name: xsuaa-binding spec: serviceInstanceName: xsuaa-instance secretName: xsuaa-secret
- Apply it to your cluster:
kubectl apply -f xsuaa-binding.yaml
- After short time, it should look like this:
NAME INSTANCE STATUS READY AGE xsuaa-binding xsuaa-instance Created True 15s
Inspecting the Binding’s Secret
Now, you can inspect the Kubernetes secreted that was created by the service binding.
- List the created secret:
kubectl get secret xsuaa-secret
NAME TYPE DATA AGE xsuaa-secret Opaque 22 62s
- You can view the secret’s base64 encoded values (I hid the sensitive data in my example):
kubectl get secret xsuaa-secret -o yaml
apiVersion: v1 kind: Secret type: Opaque metadata: name: xsuaa-secret ... data: .metadata: eyJjc...XX0= apiurl: <...hidden...> clientid: <...hidden...> clientsecret: <...hidden...> credential-type: YmluZGluZy1zZWNyZXQ= identityzone: <...hidden...> identityzoneid: <...hidden...> instance_guid: <...hidden...> instance_name: eHN1YWEtaW5zdGFuY2U= label: eHN1YWE= plan: YXBwbGljYXRpb24= sburl: <...hidden...> subaccountid: <...hidden...> tags: WyJ4c3VhYSJd tenantid: <...hidden...> tenantmode: c2hhcmVk type: eHN1YWE= uaadomain: <...hidden...> url: <...hidden...> verificationkey: <...hidden...> xsappname: <...hidden...> zoneid: <...hidden...>
- You can check what’s in the
.metadata
property with this small piece of shell magic:kubectl get secret xsuaa-secret -o json | jq -r '.data[".metadata"]' | base64 -d | jq .
Try a Service Binding API
Finally, you can create a pod using this binding to explore it, with the required configuration to consume the service binding.
- Create a file
test-pod.yaml
:apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: test env: - name: SERVICE_BINDING_ROOT value: /bindings image: node:alpine volumeMounts: - mountPath: /bindings/auth name: xsuaa-volume readOnly: true command: [ 'sleep', "10000" ] volumes: - name: xsuaa-volume secret: defaultMode: 420 secretName: xsuaa-secret
You can find the three things here that are important to access the service binding:
- The environment variable
SERVICE_BINDING_ROOT
is provided. - There is a volume with the service binding secret properties.
- The volume is mounted in a sub directory of the directory provided in
SERVICE_BINDING_ROOT
.
- The environment variable
- Apply the pod to your cluster:
kubectl apply -f test-pod.yaml
- Wait for the pod to turn into status
Running
:kubectl get pods test-pod
NAME READY STATUS RESTARTS AGE test-pod 2/2 Running 0 10s
After having the preparation completed, you can use the pod to explore the files created for the binding:
- Enter a shell on the pod:
kubectl exec test-pod -it -- sh
Now, you can execute commands on the pod.
- List all the service binding files, for example:
find /bindings
You will observe an unexpected, yet a bit weird, output: All properties exist two times!
The files in the expected location (for example:
/bindings/auth/clientid
) are symbolic links to files in a sub directory starting with..
.
You can also try out what the @sap/xsenv
library returns:
- Install
@sap/xsenv
on the pod:npm i -g @sap/xsenv
- Read all services:
node -e 'console.log(require("/usr/local/lib/node_modules/@sap/xsenv").readServices())'
- The output looks like this:
{ auth: { instance_name: 'xsuaa-instance', instance_guid: '<...hidden...>', plan: 'application', label: 'xsuaa', type: 'xsuaa', tags: [ 'xsuaa' ], credentials: { sburl: '<...hidden...>', url: '<...hidden...>', verificationkey: '<...hidden...>', clientid: '<...hidden...>', clientsecret: '<...hidden...>', 'credential-type': 'binding-secret', uaadomain: '<...hidden...>', zoneid: '<...hidden...>', subaccountid: '<...hidden...>', tenantmode: 'shared', xsappname: '<...hidden...>', apiurl: '<...hidden...>', identityzone: '<...hidden...>', identityzoneid: '<...hidden...>', tenantid: '<...hidden...>' }, name: 'auth' } }
Metadata and credential data are separated, and the tags
array is correctly deserialized as expected. The API returns the same binding data like on Cloud Foundry.
Credentials can be also looked up by its metadata properties.
- Query the service using its
label
:node -e 'console.log(require("/usr/local/lib/node_modules/@sap/xsenv").getServices({auth: { label: "xsuaa"}}))'
- You should get the same output as the previous call.
If you like, you can create a second service instance and bind it and continue doing experiments.
Once you’ve finished, enter exit
to leave the pod.
You can delete the created Kubernetes resources using:
kubectl delete -f xsuaa-instance.yaml,xsuaa-binding.yaml,test-pod.yaml
Remark: The value of the name
Property
Note, that the name
property is filled with the directory name (auth
in the example above) of your binding instead of the instance name (xsuaa-instance
in the example). On Cloud Foundry, you usually get the instance name.
name → The
binding_name
, if it exists. Otherwise, theinstance_name
Accordingly, we decided to always fill it with the binding’s directory name that you can set depending on your needs. For compatibility with Cloud Foundry, you either use the instance’s name as binding directory name or change your code to use the instance_name
property instead of the name
property.
Summary
Wrapping it up, the adoption of servicebinding.io increases the interoperability between SAP and non-SAP technology.
Further, it enables usage of things that may be created around that spec. For example, you could use the service binding controller to inject bindings into your pods.
With the .metadata
extension, all the metadata of SAP BTP service bindings can be accessed and credentials are accessible with their correct data type. This lets you stay platform independent and migrate applications from Cloud Foundry to the Kyma Runtime without touching the binding related code.
Additionally, any applications that are capable to read servicebinding.io bindings can also read bindings to SAP BTP services (ignoring the .metadata
file).
I’d like to recommend the new tutorials showing how to Deploy Your CAP Application on SAP BTP Kyma Runtime as part of the SAP BTP end-to-end tutorial series. In the tutorials you use CAP’s tooling to add a Helm chart to an application and deploy it to a Kyma Runtime cluster. The Helm chart makes use of the new service binding mechanism.