Creating BTP Service Instances on SAP BTP Kyma Runtime with the Service Operator
When you deploy a SAP CAP application to Kyma, it usually needs backing services such as a SAP HANA Cloud database and an XSUAA instance for authentication. On Cloud Foundry you would create these with cf create-service and cf bind-service. On Kubernetes and Kyma, the equivalent is done through the SAP BTP Service Operator, using plain kubectl and a couple of custom resources.
In this article we'll look at what the SAP BTP Service Operator is, how its reconciliation loop turns YAML into real BTP services, and how to create a service instance and a service binding from the CLI. If you're new to the platform, the Intro to SAP BTP post is a good starting point, and the Kubernetes for Developers series covers the underlying Kubernetes primitives.
Series — SAP BTP Kyma Runtime:
- Preparing Your Environment For SAP BTP Development on Kyma Runtime
- Deploying Your First CAP Application on SAP BTP Kyma Runtime
- Creating BTP Service Instances on SAP BTP Kyma Runtime with the Service Operator (this post)
- Binding BTP Service Instances to your CAP Application with Kyma Runtime
- The Chart Templates Behind Your CAP Kyma Deployment
- Creating roles for your CAP Application on SAP BTP Kyma Runtime
- Using Istio Gateway as Entry Point in SAP BTP Kyma Runtime
What is the SAP BTP Service Operator?
The SAP BTP Service Operator is a Kubernetes operator — a controller that runs inside your Kyma cluster (typically in the kyma-system namespace) and bridges two worlds:
- The Kubernetes world: custom resources you create with
kubectl—ServiceInstanceandServiceBinding. - The SAP BTP world: the Service Manager, which actually provisions real services (HANA Cloud databases, XSUAA app registrations, Object Store buckets, etc.) in your BTP subaccount.
You declare what you want in Kubernetes YAML, and the operator makes it real in BTP and syncs the result back into the cluster. It is the Kubernetes-native replacement for the old Cloud Foundry cf create-service / cf bind-service commands.
┌──────────────────────────────────────────────────────────────────┐
│ Kyma Cluster │
│ │
│ ServiceInstance ─────┐ │
│ ServiceBinding ─────┤ │
│ (your YAML) │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ SAP BTP Service │ │
│ │ Operator (controller)│ │
│ └───────────┬───────────┘ │
│ │ reconcile │
└──────────────────────────┼──────────────────────────────────────┘
│
▼
┌──────────────────────┐ ┌──────────────────┐
│ SAP Service Manager │ ─────▶ │ BTP Subaccount │
│ │ │ (HANA, XSUAA…) │
└──────────────────────┘ └──────────────────┘The Two Custom Resources
Everything the operator does revolves around two custom resource definitions (CRDs) in the services.cloud.sap.com API group.
ServiceInstance — "provision me a service"
This is the equivalent of cf create-service. You choose what service you want (a service offering from the BTP marketplace) and which plan/tier of it.
apiVersion: services.cloud.sap.com/v1
kind: ServiceInstance
metadata:
name: my-hana
spec:
serviceOfferingName: hana # WHAT service
servicePlanName: hdi-shared # WHICH plan
parameters: # service-specific configuration
database_id: <optional-hana-cloud-id>ServiceBinding — "give me credentials to use it"
This is the equivalent of cf bind-service. A binding requests credentials for an instance and writes them into a Kubernetes Secret.
apiVersion: services.cloud.sap.com/v1
kind: ServiceBinding
metadata:
name: my-hana-binding
spec:
serviceInstanceName: my-hana # bind to which instance
secretName: my-hana-secret # write credentials into this SecretHow the Reconciliation Loop Works
The heart of any operator is its reconciliation loop: it continuously compares the desired state (your YAML) with the actual state (what exists in BTP) and drives them together. Here is what happens when you apply a ServiceInstance:
You K8s API BTP Operator Service Manager BTP Backend
│ │ │ │ │
│ kubectl apply ────▶│ │ │ │
│ ServiceInstance │ (stored in etcd) │ │ │
│ │──── watch ──────▶│ │ │
│ │ │── provision ────────▶│ │
│ │ │ │── create service ──▶ │
│ │ │◀── in progress ──────│ │ (takes
│ │◀─ status update ─│ │ │ minutes)
│ │ Ready=False │ │ │
│ │ │ ⟳ polls periodically │
│ │ │── check status ─────▶│◀── done ──────────── │
│ │◀─ status update ─│ │ │
│ │ Ready=True │ │ │This explains behavior you'll observe in practice:
- After
kubectl apply, the instance first showsSTATUS: NotReady/InProgress, then flips toCreated/READY: Trueonce BTP finishes provisioning — often a minute or two later. - If BTP-side provisioning is slow, the operator keeps polling on its own. You don't re-run anything.
- If you
kubectl deletetheServiceInstance, the operator calls BTP to deprovision the real service.
From Binding to Running Application
The ServiceBinding is what connects a provisioned service to your CAP pod. The chain looks like this:
- You create a
ServiceBindingthat references aServiceInstance. - The operator asks Service Manager for real credentials (HANA host, user, password, certificate; or XSUAA clientid/secret).
- The operator writes those credentials into a Kubernetes Secret (the
secretNameyou specified). - Your CAP pod mounts that Secret as a volume, with the environment variable
SERVICE_BINDING_ROOTpointing at the mount directory (for example/bindings). @sap/cdsreads the mounted binding at startup and connects to the service.
ServiceInstance ──▶ ServiceBinding ──▶ Secret ──▶ mounted volume ──▶ CAP reads credentials
(/bindings/db, /bindings/auth)If you deploy CAP with cds up --to k8s, the generated Helm chart creates all of these resources for you — the service-instance and service-binding subcharts render exactly the YAML shown above.
Creating a Service Instance with kubectl
The most direct and portable way is a declarative manifest. Let's create an XSUAA instance and a binding for it.
kubectl apply -n my-namespace -f - <<'EOF'
apiVersion: services.cloud.sap.com/v1
kind: ServiceInstance
metadata:
name: my-xsuaa
spec:
serviceOfferingName: xsuaa
servicePlanName: application
parameters:
xsappname: my-app
tenant-mode: dedicated
EOFThen create the binding that materializes the credentials into a Secret:
kubectl apply -n my-namespace -f - <<'EOF'
apiVersion: services.cloud.sap.com/v1
kind: ServiceBinding
metadata:
name: my-xsuaa-binding
spec:
serviceInstanceName: my-xsuaa
secretName: my-xsuaa-secret
EOFWatch provisioning happen in real time:
kubectl get serviceinstances.services.cloud.sap.com -n my-namespace -wYou'll see the status move from NotReady to Created once BTP has provisioned the service.
Note: The service offering and plan names (
serviceOfferingName,servicePlanName) must be entitled in your BTP subaccount. If they aren't, the instance will stay in aFailedstate with a message from Service Manager. Check the SAP BTP Cockpit → Entitlements if a plan isn't available.
Inspecting Status and Health
To understand whether an instance is healthy, read its status.conditions:
kubectl get serviceinstance my-xsuaa -n my-namespace -o yamlUseful fields:
status.conditions[Ready]—Truewhen the service is provisioned and usable.status.conditions[Succeeded]/Failed— the outcome of the last operation, with a human-readable message.status.instanceID— the real BTP-side identifier of the provisioned service.
The STATUS column you see in kubectl get serviceinstances (Created, InProgress, Failed) is just a summary of these conditions.
Deleting Instances and the Role of Finalizers
Both ServiceInstance and ServiceBinding carry finalizers. A finalizer blocks Kubernetes from removing the resource until the operator confirms the BTP-side service has actually been deprovisioned. This has two practical consequences:
kubectl delete serviceinstance my-xsuaa -n my-namespace- The command won't return instantly — it waits for BTP to tear down the real service.
- If the operator is down or BTP is unreachable, the delete hangs and the resource sits in
Terminating. That's the finalizer doing its job, not a bug — the fix is to restore the operator or connectivity rather than force-removing the finalizer, which would orphan the real BTP service.
Note: Always delete
ServiceBindingresources before deleting theServiceInstancethey depend on. Deleting an instance that still has active bindings can leave dangling credentials.
Where the Operator Gets Its Own Credentials
One prerequisite you can't express in a manifest: the operator authenticates to your BTP subaccount using a secret (typically sap-btp-service-operator in kyma-system) that contains a Service Manager binding — clientid, clientsecret, URL and token URL. This is set up when Kyma is enabled or when the operator is installed. Without it, no provisioning works at all, so it's the first thing to check if every instance fails immediately.
Conclusion
The SAP BTP Service Operator brings BTP service provisioning into the Kubernetes-native world. Instead of imperative cf create-service commands, you declare ServiceInstance and ServiceBinding custom resources, and the operator's reconciliation loop provisions the real services in your subaccount, syncs their status back, and materializes credentials into Kubernetes Secrets that your CAP pods mount at startup.
Using the kubectl CLI, the underlying mechanism is the same: two CRDs, a controller that reconciles them against SAP Service Manager, and finalizers that keep the Kubernetes and BTP sides consistent when you clean up. Understanding this loop makes deployments with cds up --to k8s far less of a black box — the Helm chart is simply generating the same resources you can create by hand.
