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:

  1. Preparing Your Environment For SAP BTP Development on Kyma Runtime
  2. Deploying Your First CAP Application on SAP BTP Kyma Runtime
  3. Creating BTP Service Instances on SAP BTP Kyma Runtime with the Service Operator (this post)
  4. Binding BTP Service Instances to your CAP Application with Kyma Runtime
  5. The Chart Templates Behind Your CAP Kyma Deployment
  6. Creating roles for your CAP Application on SAP BTP Kyma Runtime
  7. 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 kubectlServiceInstance and ServiceBinding.
  • 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.

Copy
┌──────────────────────────────────────────────────────────────────┐
│                         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.

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

Copy
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 Secret

How 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:

Copy
   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 shows STATUS: NotReady / InProgress, then flips to Created / READY: True once 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 delete the ServiceInstance, 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:

  1. You create a ServiceBinding that references a ServiceInstance.
  2. The operator asks Service Manager for real credentials (HANA host, user, password, certificate; or XSUAA clientid/secret).
  3. The operator writes those credentials into a Kubernetes Secret (the secretName you specified).
  4. Your CAP pod mounts that Secret as a volume, with the environment variable SERVICE_BINDING_ROOT pointing at the mount directory (for example /bindings).
  5. @sap/cds reads the mounted binding at startup and connects to the service.
Copy
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.

Copy
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
EOF

Then create the binding that materializes the credentials into a Secret:

Copy
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
EOF

Watch provisioning happen in real time:

Copy
kubectl get serviceinstances.services.cloud.sap.com -n my-namespace -w

You'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 a Failed state 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:

Copy
kubectl get serviceinstance my-xsuaa -n my-namespace -o yaml

Useful fields:

  • status.conditions[Ready]True when 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:

Copy
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 ServiceBinding resources before deleting the ServiceInstance they 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.