This post picks up where the Python microservice post left off. If you haven’t read that one, the short version is: SAP has withdrawn Kyma runtime from BTP trial, so we built a local Kyma environment on k3d instead. That gave us a working cluster with Istio, API Gateway, and a deployed Python Flask app accessible over HTTP and HTTPS.
This post does two things. First, it connects that local cluster to SAP BTP trial using the BTP Service Operator — so you can provision real BTP services from your local environment. Second, it deploys a Node.js microservice through the same stack, partly to show the pattern works across runtimes, and partly because doing it twice cements how the pieces fit together.
Part 1 — Connecting to SAP BTP Trial
Why Bother?
The local Kyma cluster works fine in isolation, but the point of Kyma in the SAP context is the integration with BTP services — Destination Service, XSUAA, Connectivity, and so on. The BTP Service Operator is what bridges the gap. It lets you create ServiceInstance and ServiceBinding resources on your local cluster that provision real services on BTP, the same way a managed Kyma runtime would.
Your BTP trial account still works — it’s just the managed Kyma runtime that’s been withdrawn. The service marketplace is still there.
Prerequisites
You’ll need:
The local Kyma cluster from the previous post (k3d, Istio, API Gateway, BTP Manager installed)BTP CLI (btp) installed and logged inHelm — brew install helm on macOS, or see helm.sh for other platformskubectl pointed at your local cluster
Step 1 — Create a Service Manager Instance
Set your BTP CLI target to your subaccount:
btp target –subaccount <your-subaccount-id>
Then create a Service Manager instance with the service-operator-access plan:
btp create services/instance
–subaccount <your-subaccount-id>
–offering-name service-manager
–plan-name service-operator-access
–name btp-operator-access
This is what gives the BTP Service Operator permission to talk to the BTP service marketplace on your behalf.
Step 2 — Create a Service Binding and Extract Credentials
Create a binding for the instance:
btp create services/binding
–subaccount <your-subaccount-id>
–instance-name btp-operator-access
–name btp-operator-key
Get the binding ID:
btp get services/binding
–subaccount <your-subaccount-id>
–name btp-operator-key
Extract the credentials:
btp get services/binding –id <binding-id>
You’re looking for four values: clientid, clientsecret, url, and tokenurl. You’ll need these in the next step.
Step 3 — Install cert-manager
The BTP Service Operator depends on cert-manager for webhook certificate management. Install it:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
kubectl wait –for=condition=Available deployment –all -n cert-manager –timeout=90s
Step 4 — Install the BTP Service Operator via Helm
Add the SAP BTP operator Helm repo:
helm repo add sap-btp-operator https://sap.github.io/sap-btp-service-operator
helm repo update
Install the operator, passing credentials directly as Helm values:
helm install sap-btp-operator sap-btp-operator/sap-btp-operator
–namespace operators
–set manager.secret.clientid=”<clientid>”
–set manager.secret.clientsecret=”<clientsecret>”
–set manager.secret.sm_url=”<url>”
–set manager.secret.tokenurl=”<tokenurl>”
A note on passing credentials directly via –set rather than creating the secret first: the alternative approach (running the setup script and then installing Helm) can cause field manager conflicts where Helm and kubectl both think they own the secret. Passing them as Helm values at install time avoids that.
Verify it’s running:
kubectl get pods -n operators
You’re looking for the BTP operator pod in a Running state.
Part 2 — The Node.js Microservice
Why Node.js?
The Python post covered the core pattern — Dockerfile, Deployment, Service, APIRule. This post uses Node.js to show the same approach works across runtimes. The Kubernetes side is identical; only the application code and Dockerfile change.
The Application
app.js
const express = require(‘express’);
const app = express();
const port = process.env.PORT || 8080;
app.get(‘/health’, (req, res) => {
res.json({ status: ‘ok’ });
});
app.get(‘/api/data’, (req, res) => {
res.json({
message: ‘Hello from Kyma!’,
environment: process.env.ENVIRONMENT || ‘dev’
});
});
app.listen(port, ‘0.0.0.0’, () => {
console.log(`Server running on port ${port}`);
});
Initialise the project and install dependencies:
mkdir -p ~/Documents/GitHub/kubernetes-course/kyma/btp-node-app
cd ~/Documents/GitHub/kubernetes-course/kyma/btp-node-app
npm init -y
npm install express
The Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json .
RUN npm install –production
COPY app.js .
EXPOSE 8080
CMD [“node”, “app.js”]
Build and Test Locally
docker build –no-cache -t btp-node-app:v1 .
The –no-cache flag matters here. If you’ve run npm install in the project folder before adding express to package.json, Docker may have a cached layer from that earlier run. When it rebuilds, it sees the npm install step hasn’t changed and uses the cache — which means express never gets installed in the image. The –no-cache flag forces every layer to rebuild from scratch.
This is the kind of thing that produces a perfectly healthy-looking container that crashes immediately on startup with a module not found error. Worth knowing.
Test it before touching Kubernetes:
docker run –rm -p 8081:8080 btp-node-app:v1
curl http://localhost:8081/health
curl http://localhost:8081/api/data
If both return JSON, the app is fine. Stop the container.
Import into k3d
k3d image import btp-node-app:v1 -c kyma
Kubernetes Manifests
Create a k8s/ folder in the project directory.
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: btp-node-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: btp-node-app
template:
metadata:
labels:
app: btp-node-app
spec:
containers:
– name: btp-node-app
image: btp-node-app:v1
imagePullPolicy: Never
ports:
– containerPort: 8080
env:
– name: ENVIRONMENT
value: “kyma-local”
resources:
requests:
memory: “64Mi”
cpu: “100m”
limits:
memory: “128Mi”
cpu: “200m”
k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: btp-node-app
namespace: default
spec:
selector:
app: btp-node-app
ports:
– port: 80
targetPort: 8080
k8s/apirule.yaml
apiVersion: gateway.kyma-project.io/v2
kind: APIRule
metadata:
name: btp-node-app
namespace: default
spec:
gateway: kyma-system/kyma-gateway
hosts:
– btp-node-app.local.kyma.dev
service:
name: btp-node-app
port: 80
rules:
– path: /*
methods: [“GET”, “POST”]
noAuth: true
Deploy
kubectl apply -f k8s/
Add the hosts entry:
echo “127.0.0.1 btp-node-app.local.kyma.dev” | sudo tee -a /etc/hosts
Check the pod and APIRule:
kubectl get pods -l app=btp-node-app
kubectl get apirule btp-node-app
Pod should be 2/2 (container plus Istio sidecar). APIRule should be Ready.
Test
HTTP:
curl -H “Host: btp-node-app.local.kyma.dev” http://127.0.0.1:30080/health
curl -H “Host: btp-node-app.local.kyma.dev” http://127.0.0.1:30080/api/data
HTTPS:
curl -k –resolve btp-node-app.local.kyma.dev:30443:127.0.0.1
-H “Host: btp-node-app.local.kyma.dev”
https://btp-node-app.local.kyma.dev:30443/health
curl -k –resolve btp-node-app.local.kyma.dev:30443:127.0.0.1
-H “Host: btp-node-app.local.kyma.dev”
https://btp-node-app.local.kyma.dev:30443/api/data
Expected:
{“status”: “ok”}
{“environment”: “kyma-local”, “message”: “Hello from Kyma!”}
At this point you have a local Kyma cluster that:
Runs containerised microservices in multiple runtimesExposes them externally via Istio and APIRule v2Is connected to SAP BTP trial via the BTP Service Operator
The next logical step is creating a ServiceInstance and ServiceBinding on the local cluster to provision a real BTP service — Destination Service is the obvious one — and consuming it from a microservice. That’s where the SAP-specific integration story starts to get interesting.
That’ll be the next post.
This post picks up where the Python microservice post left off. If you haven’t read that one, the short version is: SAP has withdrawn Kyma runtime from BTP trial, so we built a local Kyma environment on k3d instead. That gave us a working cluster with Istio, API Gateway, and a deployed Python Flask app accessible over HTTP and HTTPS.This post does two things. First, it connects that local cluster to SAP BTP trial using the BTP Service Operator — so you can provision real BTP services from your local environment. Second, it deploys a Node.js microservice through the same stack, partly to show the pattern works across runtimes, and partly because doing it twice cements how the pieces fit together.Part 1 — Connecting to SAP BTP TrialWhy Bother?The local Kyma cluster works fine in isolation, but the point of Kyma in the SAP context is the integration with BTP services — Destination Service, XSUAA, Connectivity, and so on. The BTP Service Operator is what bridges the gap. It lets you create ServiceInstance and ServiceBinding resources on your local cluster that provision real services on BTP, the same way a managed Kyma runtime would.Your BTP trial account still works — it’s just the managed Kyma runtime that’s been withdrawn. The service marketplace is still there.PrerequisitesYou’ll need:The local Kyma cluster from the previous post (k3d, Istio, API Gateway, BTP Manager installed)BTP CLI (btp) installed and logged inHelm — brew install helm on macOS, or see helm.sh for other platformskubectl pointed at your local clusterStep 1 — Create a Service Manager InstanceSet your BTP CLI target to your subaccount:btp target –subaccount <your-subaccount-id>Then create a Service Manager instance with the service-operator-access plan:btp create services/instance
–subaccount <your-subaccount-id>
–offering-name service-manager
–plan-name service-operator-access
–name btp-operator-accessThis is what gives the BTP Service Operator permission to talk to the BTP service marketplace on your behalf.Step 2 — Create a Service Binding and Extract CredentialsCreate a binding for the instance:btp create services/binding
–subaccount <your-subaccount-id>
–instance-name btp-operator-access
–name btp-operator-keyGet the binding ID:btp get services/binding
–subaccount <your-subaccount-id>
–name btp-operator-keyExtract the credentials:btp get services/binding –id <binding-id>You’re looking for four values: clientid, clientsecret, url, and tokenurl. You’ll need these in the next step.Step 3 — Install cert-managerThe BTP Service Operator depends on cert-manager for webhook certificate management. Install it:kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
kubectl wait –for=condition=Available deployment –all -n cert-manager –timeout=90sStep 4 — Install the BTP Service Operator via HelmAdd the SAP BTP operator Helm repo:helm repo add sap-btp-operator https://sap.github.io/sap-btp-service-operator
helm repo updateInstall the operator, passing credentials directly as Helm values:helm install sap-btp-operator sap-btp-operator/sap-btp-operator
–namespace operators
–set manager.secret.clientid=”<clientid>”
–set manager.secret.clientsecret=”<clientsecret>”
–set manager.secret.sm_url=”<url>”
–set manager.secret.tokenurl=”<tokenurl>”A note on passing credentials directly via –set rather than creating the secret first: the alternative approach (running the setup script and then installing Helm) can cause field manager conflicts where Helm and kubectl both think they own the secret. Passing them as Helm values at install time avoids that.Verify it’s running:kubectl get pods -n operatorsYou’re looking for the BTP operator pod in a Running state.Part 2 — The Node.js MicroserviceWhy Node.js?The Python post covered the core pattern — Dockerfile, Deployment, Service, APIRule. This post uses Node.js to show the same approach works across runtimes. The Kubernetes side is identical; only the application code and Dockerfile change.The Applicationapp.jsconst express = require(‘express’);
const app = express();
const port = process.env.PORT || 8080;
app.get(‘/health’, (req, res) => {
res.json({ status: ‘ok’ });
});
app.get(‘/api/data’, (req, res) => {
res.json({
message: ‘Hello from Kyma!’,
environment: process.env.ENVIRONMENT || ‘dev’
});
});
app.listen(port, ‘0.0.0.0’, () => {
console.log(`Server running on port ${port}`);
});Initialise the project and install dependencies:mkdir -p ~/Documents/GitHub/kubernetes-course/kyma/btp-node-app
cd ~/Documents/GitHub/kubernetes-course/kyma/btp-node-app
npm init -y
npm install expressThe DockerfileFROM node:20-slim
WORKDIR /app
COPY package*.json .
RUN npm install –production
COPY app.js .
EXPOSE 8080
CMD [“node”, “app.js”]Build and Test Locallydocker build –no-cache -t btp-node-app:v1 .The –no-cache flag matters here. If you’ve run npm install in the project folder before adding express to package.json, Docker may have a cached layer from that earlier run. When it rebuilds, it sees the npm install step hasn’t changed and uses the cache — which means express never gets installed in the image. The –no-cache flag forces every layer to rebuild from scratch.This is the kind of thing that produces a perfectly healthy-looking container that crashes immediately on startup with a module not found error. Worth knowing.Test it before touching Kubernetes:docker run –rm -p 8081:8080 btp-node-app:v1
curl http://localhost:8081/health
curl http://localhost:8081/api/dataIf both return JSON, the app is fine. Stop the container.Import into k3dk3d image import btp-node-app:v1 -c kymaKubernetes ManifestsCreate a k8s/ folder in the project directory.k8s/deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: btp-node-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: btp-node-app
template:
metadata:
labels:
app: btp-node-app
spec:
containers:
– name: btp-node-app
image: btp-node-app:v1
imagePullPolicy: Never
ports:
– containerPort: 8080
env:
– name: ENVIRONMENT
value: “kyma-local”
resources:
requests:
memory: “64Mi”
cpu: “100m”
limits:
memory: “128Mi”
cpu: “200m”k8s/service.yamlapiVersion: v1
kind: Service
metadata:
name: btp-node-app
namespace: default
spec:
selector:
app: btp-node-app
ports:
– port: 80
targetPort: 8080k8s/apirule.yamlapiVersion: gateway.kyma-project.io/v2
kind: APIRule
metadata:
name: btp-node-app
namespace: default
spec:
gateway: kyma-system/kyma-gateway
hosts:
– btp-node-app.local.kyma.dev
service:
name: btp-node-app
port: 80
rules:
– path: /*
methods: [“GET”, “POST”]
noAuth: trueDeploykubectl apply -f k8s/Add the hosts entry:echo “127.0.0.1 btp-node-app.local.kyma.dev” | sudo tee -a /etc/hostsCheck the pod and APIRule:kubectl get pods -l app=btp-node-app
kubectl get apirule btp-node-appPod should be 2/2 (container plus Istio sidecar). APIRule should be Ready.TestHTTP:curl -H “Host: btp-node-app.local.kyma.dev” http://127.0.0.1:30080/health
curl -H “Host: btp-node-app.local.kyma.dev” http://127.0.0.1:30080/api/dataHTTPS:curl -k –resolve btp-node-app.local.kyma.dev:30443:127.0.0.1
-H “Host: btp-node-app.local.kyma.dev”
https://btp-node-app.local.kyma.dev:30443/health
curl -k –resolve btp-node-app.local.kyma.dev:30443:127.0.0.1
-H “Host: btp-node-app.local.kyma.dev”
https://btp-node-app.local.kyma.dev:30443/api/dataExpected:{“status”: “ok”}
{“environment”: “kyma-local”, “message”: “Hello from Kyma!”} At this point you have a local Kyma cluster that:Runs containerised microservices in multiple runtimesExposes them externally via Istio and APIRule v2Is connected to SAP BTP trial via the BTP Service OperatorThe next logical step is creating a ServiceInstance and ServiceBinding on the local cluster to provision a real BTP service — Destination Service is the obvious one — and consuming it from a microservice. That’s where the SAP-specific integration story starts to get interesting.That’ll be the next post. Read More Technology Blog Posts by Members articles
#SAP
#SAPTechnologyblog