BTP Kyma Runtime based on a NodeJS application.
If you would like to know how to build a multi-tenant application in BTP Cloud Foundry Runtime, you can read this .
For more general descriptions of how many steps it takes to do from a normal application to a multitenant application, you can read this .
More information on this Series – Multitenancy:
Prerequisites
Business applications
-
If you come with a prepared applications project, please skip to the next item.
-
If you come with no project, you can use the application generator tool,
express-generator
, to quickly create an application skeleton by following this tutorial: . Or, you can git clone the skeleton project directly from: .npm install express --save npm install -g express-generator express --view=jade cloud-kyma-multitenant-saas-provisioning-sample
For more information on how to develop and run business applications on SAP Business Technology Platform (SAP BTP) using our cloud application programming model, APIs, services, tools, and capabilities, see .
BTP account
You can get a Free Account on SAP BTP Trial by following this , then enable a Kyma Environment in the account by following this . Besides, with your account in hand, determine your key values for yourself:
-
Subaccount subdomain: where your application is deployed, you can find it on the overview page of your subaccount in the Cockpit. In this post, for example,
trial-kyma-vnrmtio8
. -
Cluster domain: the full Kyma cluster domain. You can find the cluster name in the downloaded kubeconfig file or in the URL of the Kyma dashboard. In this post, for example,
e6803e4.kyma.shoot.live.k8s-hana.ondemand.com
.
-
Kyma namespace: in this post, for example:
multitenancy-ns
. If you would like to define with a customized name, you should modify the parts of the code that appearmultitenancy-ns
accordingly.
Scenario
Persona: SaaS Application Provider
Let’s assume you are a SaaS application provider, for example: Provider: TIA
. Provider: TIA
would like to provide an application that displays the logged in user’s name and customer’s tenant-related information, shown as below:
Final project with multitenancy can be found: .
Persona: Customer
A consumer can subscribe to the application through the SAP BTP Account Cockpit.
Steps
-
Create and Configure the Approuter Application
-
Create and Configure Authentication and Authorization with XSUAA
-
Implement Subscription callbacks API
-
Register the Multitenant Application to the SAP SaaS Provisioning Service
-
Deploy the Multitenant Application to the Provider Subaccount
-
Subscribe SaaS Application by a Consumer
Step 1: Create and Configure the Approuter Application
Each multitenant application has to deploy its own application router, and the application router handles requests of all tenants to the application. The application router is able to determine the tenant identifier out of the URL and then forwards the authentication request to the tenant User Account and Authentication (UAA) service and the related identity zone.
For general instructions, see .
Create a folder kyma-multitenant-approuter
under the root directory.
mkdir kyma-multitenant-approuter cd kyma-multitenant-approuter
Under the folder kyma-multitenant-approuter
, create a file package.json
with the following content:
{ "name": "kyma_multitenant_approuter", "dependencies": { "@sap/xsenv": "^3", "@sap/approuter": "^8" }, "scripts": { "start": "node node_modules/@sap/approuter/approuter.js" } }
Then we should configure the routes in the application router security descriptor file (xs-app.json) so that application requests are forwarded to the multitenant application destination.
Under the folder kyma-multitenant-approuter
, create file xs-app.json
with the following content:
{ "welcomeFile": "/ui/index.html", "authenticationMethod": "none", "routes": [{ "source": "/", "target": "/", "destination": "dest_kyma_multitenant_node" }] }
In order to provide destination to the approuter app, we should create a ConfigMap for reference later.
Create a new deployment YAML file named k8s-deployment-approuter.yaml
for the approuter app with the following content:
--- apiVersion: v1 kind: ConfigMap metadata: name: destinations-config data: destinations: | [ {"name":"dest_kyma_multitenant_node","url":"https://trial-kyma-vnrmtio8-node.e6803e4.kyma.shoot.live.k8s-hana.ondemand.com","forwardAuthToken" : true} ]
There are two alternatives to define the destination urls:
use (external) service url provided by Kyma APIRule (JWT enabled)
destinations: | [ {"name":"dest_kyma_multitenant_node","url":"https://trial-kyma-vnrmtio8-node.e6803e4.kyma.shoot.live.k8s-hana.ondemand.com","forwardAuthToken" : true} ]
use cluster internal service url: note that internal service naming follows http://<service-name>.<namespace>.svc.cluster.local:<service-port>, make sure “namespace” of the broker is adapted when deploying to different namespace
Define the Deployment resource for the approuter app into the k8s-deployment-approuter.yaml
file, and add config reference to the destination ConfigMap:
--- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: kyma-multitenant-approuter-multitenancy release: multitenancy name: kyma-multitenant-approuter-multitenancy spec: replicas: 1 selector: matchLabels: app: kyma-multitenant-approuter-multitenancy release: multitenancy strategy: {} template: metadata: creationTimestamp: null labels: app: kyma-multitenant-approuter-multitenancy release: multitenancy spec: automountServiceAccountToken: false imagePullSecrets: - name: registry-secret containers: - env: - name: destinations valueFrom: configMapKeyRef: name: destinations-config key: destinations - name: PORT value: "8080" - name: TMPDIR value: /tmp image: tiaxu/multitenant-approuter:v1 livenessProbe: exec: command: - nc - -z - localhost - "8080" failureThreshold: 1 initialDelaySeconds: 60 periodSeconds: 30 successThreshold: 1 timeoutSeconds: 60 name: kyma-multitenant-approuter-multitenancy ports: - containerPort: 8080 readinessProbe: exec: command: - nc - -z - localhost - "8080" failureThreshold: 1 initialDelaySeconds: 60 periodSeconds: 30 successThreshold: 1 timeoutSeconds: 60 resources: limits: ephemeral-storage: 256M memory: 256M requests: cpu: 100m ephemeral-storage: 256M memory: 256M securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL privileged: false readOnlyRootFilesystem: false volumeMounts: - mountPath: /tmp name: tmp securityContext: runAsNonRoot: true volumes: - emptyDir: {} name: tmp status: {}
We will define the Secret when deploying apps:
imagePullSecrets: – name: registry-secret
Now let’s create a Service and APIRule to make it accessible to the internet.
Define the Service resource for the approuter app into the k8s-deployment-approuter.yaml
file:
--- apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: kyma-multitenant-approuter-multitenancy release: multitenancy name: kyma-multitenant-approuter-multitenancy spec: type: ClusterIP ports: - port: 8080 protocol: TCP targetPort: 8080 selector: app: kyma-multitenant-approuter-multitenancy release: multitenancy
Define the APIRule resource for the approuter app into the k8s-deployment-approuter.yaml
file:
--- apiVersion: gateway.kyma-project.io/v1alpha1 kind: APIRule metadata: creationTimestamp: null labels: app: kyma-multitenant-approuter-multitenancy release: multitenancy name: kyma-multitenant-approuter-multitenancy spec: gateway: kyma-gateway.kyma-system.svc.cluster.local rules: - accessStrategies: - handler: allow methods: - GET - POST - PUT - PATCH - DELETE - HEAD path: /.* service: host: trial-kyma-vnrmtio8-approuter.e6803e4.kyma.shoot.live.k8s-hana.ondemand.com name: kyma-multitenant-approuter-multitenancy port: 8080
Note: Host name must start with your org subdomain so that your app can be redirected to the right authenticator.
Step 2: Create and Configure Authentication and Authorization with XSUAA
To use a multitenant application router, you must have a shared UAA service and the version of the application router has to be greater than 2.3.1:
-
Define the application provider tenant as a shared tenant
tenant-mode: shared
-
Provide access to the SAP SaaS Provisioning service (technical name: saas-registry) for calling callbacks and getting the dependencies API by granting scopes:
scopes: - name: $XSAPPNAME.Callback description: With this scope set, the callbacks for subscribe, unsubscribe and getDependencies can be called. grant-as-authority-to-apps: - $XSAPPNAME(application,sap-provisioning,tenant-onboarding)
In Kubernetes, you can create and bind to a service instance using the Service Catalog. Create a new deployment file k8s-deployment-services.yaml
and define resources for XSUAA instance and binding into the file:
################### XSUAA ################### --- apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: annotations: com.sap.cki/source-instance-name: uaa_kyma_multitenant creationTimestamp: null name: xsuaa-service spec: clusterServiceClassExternalName: xsuaa clusterServicePlanExternalName: application parameters: xsappname: multitenant-kyma-demo tenant-mode: shared description: Security profile of called application scopes: - name: $XSAPPNAME.Callback description: With this scope set, the callbacks for subscribe, unsubscribe and getDependencies can be called. grant-as-authority-to-apps: - $XSAPPNAME(application,sap-provisioning,tenant-onboarding) oauth2-configuration: redirect-uris: - https://*.e6803e4.kyma.shoot.live.k8s-hana.ondemand.com/** --- apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: creationTimestamp: null name: xsuaa-service-binding spec: externalID: "" instanceRef: name: xsuaa-service secretName: xsuaa-service-binding
Upon creation of the binding, the Service Catalog will create a Kubernetes secret (by default with the same name as the binding) containing credentials, configurations and certificates.
One thing to note is that SAP’s uses package internally to parse and load service keys and secrets bound to the application, this makes the process to load secrets easy.
Kubernetes offers several ways of handling application configurations for bound services and certificates. @sap/xsenv expects that such configurations are handled as Kubernetes Secrets and mounted as files to the pod at a specific path. This path can be provided by the application developer, but the default is /etc/secrets/sapcp
. From there, @sap/xsenv assumes that the directory structure is the following /etc/secrets/sapcp/<service-name>/<instance-name>
. Here <service-name>
and <instance-name>
are both directories and the latter contains the credentials/configurations for the service instance as files, where the file name is the name of the configuration/credential and the content is respectively the value.
For example, the following folder structure:
/etc/ /secrets/ /sapcp/ /hana/ | /hanaInst1/ | | /user1 | | /pass1 | /hanaInst2/ | /user2 | /pass2 /xsuaa/ /xsuaaInst/ /user /pass
resembles two instances of service
hana
–hanaInst1
andhanaInst2
each with their own credentials/configurations and one instance of servicexsuaa
calledxsuaaInst
with its credentials.
Now, we can mount the secret just generated to the pods of both approuter and node application as a volume in the k8s-deployment-backend.yaml
and k8s-deployment-approuter.yaml
:
volumeMounts: - name: xsuaa-volume mountPath: "/etc/secrets/sapcp/xsuaa/xsuaa-service" readOnly: true - mountPath: /tmp name: tmp securityContext: runAsNonRoot: true volumes: - emptyDir: {} name: tmp - name: xsuaa-volume secret: secretName: xsuaa-service-binding
For more details, please read .
Secrets can be found in the directory
/etc/secrets/sapcp/<service-name>/<instance-name>
:
Update the xs-app.json
file:
{ "welcomeFile": "/ui/index.html", "authenticationMethod": "route", "routes": [{ "source": "/", "target": "/", "destination": "dest_kyma_multitenant_node", "authenticationType": "xsuaa" }] }
Add libraries for enabling authentication in the kyma-multitenant-node/app.js
file:
//**************************** Libraries for enabling authentication ***************************** var passport = require('passport'); var xsenv = require('@sap/xsenv'); var JWTStrategy = require('@sap/xssec').JWTStrategy; //************************************************************************************************
Enabling authorization in the kyma-multitenant-node/app.js
file:
//*********************************** Enabling authorization *********************************** var services = xsenv.getServices({ uaa: { tag: 'xsuaa' } }); //Get the XSUAA service passport.use(new JWTStrategy(services.uaa)); app.use(passport.initialize()); app.use(passport.authenticate('JWT', { session: false })); //Authenticate using JWT strategy //************************************************************************************************
The application router must determine the tenant-specific subdomain for the UAA that in turn determines the identity zone, used for authentication. This determination is done by using a regular expression defined in the environment variable TENANT_HOST_PATTERN
.
More details: https://help.sap.com/products/BTP/65de2977205c403bbc107264b8eccf4b/5310fc31caad4707be9126377e144627.html?locale=en-US
Create a new Config to define your Kyma Cluster domain in the k8s-deployment-approuter.yaml
file:
--- apiVersion: v1 kind: ConfigMap metadata: name: cluster-domain data: cluster-domain: e6803e4.kyma.shoot.live.k8s-hana.ondemand.com ## adapt to your Kyma cluster
And add two environment variables to the Deployment resource in the k8s-deployment-approuter.yaml
file
containers: - env: ...... - name: CLUSTER_DOMAIN valueFrom: configMapKeyRef: key: cluster-domain name: cluster-domain - name: TENANT_HOST_PATTERN value: "^(.*)-approuter.$(CLUSTER_DOMAIN)"
Step 3: Implement Subscription callbacks API
Under the routes/index.js
file, implement the two APIs. Besides, the tenant-specific application URL is exposed through APIRule, which needs to be created dynamically through the onboarding/offboarding process using .
//******************************** API Callbacks for multitenancy ******************************** /** * Request Method Type - PUT * When a consumer subscribes to this application, SaaS Provisioning invokes this API. * We return the SaaS application url for the subscribing tenant. * This URL is unique per tenant and each tenant can access the application only through it's URL. */ router.put('/callback/v1.0/tenants/*', async function(req, res) { //1. create tenant unique URL var consumerSubdomain = req.body.subscribedSubdomain; var tenantAppURL = "https://" + consumerSubdomain + "-approuter." + "e6803e4.kyma.shoot.live.k8s-hana.ondemand.com"; //2. create apirules with subdomain, const kc = new k8s.KubeConfig(); kc.loadFromCluster(); const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi); const apiRuleTempl = createApiRule.createApiRule( EF_SERVICE_NAME, EF_SERVICE_PORT, consumerSubdomain + "-approuter", kyma_cluster); try { const result = await k8sApi.getNamespacedCustomObject(KYMA_APIRULE_GROUP, KYMA_APIRULE_VERSION, EF_APIRULE_DEFAULT_NAMESPACE, KYMA_APIRULE_PLURAL, apiRuleTempl.metadata.name); //console.log(result.response); if (result.response.statusCode == 200) { console.log(apiRuleTempl.metadata.name + ' already exists.'); res.status(200).send(tenantAppURL); } } catch (err) { //create apirule if non-exist console.warn(apiRuleTempl.metadata.name + ' does not exist, creating one...'); try { const createResult = await k8sApi.createNamespacedCustomObject(KYMA_APIRULE_GROUP, KYMA_APIRULE_VERSION, EF_APIRULE_DEFAULT_NAMESPACE, KYMA_APIRULE_PLURAL, apiRuleTempl); console.log(createResult.response); if (createResult.response.statusCode == 201) { console.log("API Rule created!"); res.status(200).send(tenantAppURL); } } catch (err) { console.log(err); console.error("Fail to create APIRule"); res.status(500).send("create APIRule error"); } } console.log("exiting onboarding..."); res.status(200).send(tenantAppURL) }); /** * Request Method Type - DELETE * When a consumer unsubscribes this application, SaaS Provisioning invokes this API. * We delete the consumer entry in the SaaS Provisioning service. */ router.delete('/callback/v1.0/tenants/*', async function(req, res) { console.log(req.body); var consumerSubdomain = req.body.subscribedSubdomain; //delete apirule with subdomain const kc = new k8s.KubeConfig(); kc.loadFromCluster(); const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi); const apiRuleTempl = createApiRule.createApiRule( EF_SERVICE_NAME, EF_SERVICE_PORT, consumerSubdomain + "-approuter", kyma_cluster); try { const result = await k8sApi.deleteNamespacedCustomObject( KYMA_APIRULE_GROUP, KYMA_APIRULE_VERSION, EF_APIRULE_DEFAULT_NAMESPACE, KYMA_APIRULE_PLURAL, apiRuleTempl.metadata.name); if (result.response.statusCode == 200) { console.log("API Rule deleted!"); } } catch (err) { console.error(err); console.error("API Rule deletion error"); } res.status(200).send("deleted"); }); //************************************************************************************************
To create such APIRule from a pod, proper RoleBinding should be granted through the following definition:
--- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: broker-rolebinding subjects: - kind: ServiceAccount name: default namespace: <kyma-namespace> roleRef: kind: ClusterRole name: kyma-namespace-admin apiGroup: rbac.authorization.k8s.io
Replace <kyma-namespace> with your own namespace name
Otherwise, you will get such errors as below:
Add const values and variables:
const EF_SERVICE_NAME = 'kyma-multitenant-approuter-multitenancy'; const EF_SERVICE_PORT = 8080; const EF_APIRULE_DEFAULT_NAMESPACE = <kyma-namespace>; const KYMA_APIRULE_GROUP = 'gateway.kyma-project.io'; const KYMA_APIRULE_VERSION = 'v1alpha1'; const KYMA_APIRULE_PLURAL = 'apirules'; const k8s = require('@kubernetes/client-node'); const createApiRule = require('./createApiRule'); var kyma_cluster = process.env.CLUSTER_DOMAIN || "UNKNOWN";
Replace <kyma-namespace> with your own namespace name
Create a new file named createApiRule.js
to provide the APIRule configuration object:
module.exports = { createApiRule: createApiRule } function createApiRule(svcName, svcPort, host, clusterName) { let forwardUrl = host + '.' + clusterName; const supportedMethodsList = [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', ]; const access_strategy = { path: '/.*', methods: supportedMethodsList, // mutators: [{ // handler: 'header', // config: { // headers: { // "x-forwarded-host": forwardUrl, // } // }, // }], accessStrategies: [{ handler: 'allow' }], }; const apiRuleTemplate = { apiVersion: 'gateway.kyma-project.io/v1alpha1', kind: 'APIRule', metadata: { name: host + '-apirule', }, spec: { gateway: 'kyma-gateway.kyma-system.svc.cluster.local', service: { host: host, name: svcName, port: svcPort, }, rules: [access_strategy], }, }; return apiRuleTemplate; }
Add dependency "@kubernetes/client-node"
in the package.js
under the directory kyma-multitenant-node
:
"dependencies": { "@kubernetes/client-node": "~0.15.0", ... } }
Step 4: Register the Multitenant Application to the SAP SaaS Provisioning Service
Create an instance and binding of SAP SaaS Provisioning Service by adding the following part to the deployment file k8s-deployment-services.yaml
:
################### SaaS Provisioning Service ################### --- apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: name: saas-registry-service spec: clusterServiceClassExternalName: saas-registry clusterServicePlanExternalName: application parameters: # the xsappname refers to the one defined in xsuaa service xsappname: multitenant-kyma-demo displayName: Multitenancy Sample in Kyma description: A NodeJS application to show how to use the SaaS registry to build a multi-tenant application on BTP Kyma Runtime' category: 'Provider: TIA' appUrls: # url registered in the kyma-broker which handles SaaS provisioning (subscription/deletion of saas instances) onSubscription: https://trial-kyma-vnrmtio8-node.e6803e4.kyma.shoot.live.k8s-hana.ondemand.com/callback/v1.0/tenants/{tenantId} onSubscriptionAsync: false onUnSubscriptionAsync: false --- apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: creationTimestamp: null name: saas-registry-service-binding spec: externalID: "" instanceRef: name: saas-registry-service secretName: saas-registry-service-binding
Specify the following parameters:
Parameters | Description |
---|---|
xsappname | The xsappname configured in the security descriptor file used to create the XSUAA instance (see ). |
getDependencies | (Optional) Any URL that the application exposes for GET dependencies. If the application doesn’t have dependencies and the callback isn’t implemented, it shouldn’t be declared.NoteThe JSON response of the callback must be encoded as either UTF8, UTF16, or UTF32, otherwise an error is returned. |
onSubscription | Any URL that the application exposes via PUT and DELETE subscription. It must end with /{tenantId}. The tenant for the subscription is passed to this callback as a path parameter. You must keep {tenantId} as a parameter in the URL so that it’s replaced at real time with the tenant calling the subscription. This callback URL is called when a subscription between a multitenant application and a consumer tenant is created (PUT) and when the subscription is removed (DELETE). |
displayName | (Optional) The display name of the application when viewed in the cockpit. For example, in the application’s tile. If left empty, takes the application’s technical name. |
description | (Optional) The description of the application when viewed in the cockpit. For example, in the application’s tile. If left empty, takes the application’s display name. |
category | (Optional) The category to which the application is grouped in the Subscriptions page in the cockpit. If left empty, gets assigned to the default category. |
onSubscriptionAsync | Whether the subscription callback is asynchronous.If set to true, callbackTimeoutMillis is mandatory. |
callbackTimeoutMillis | The number of milliseconds the SAP SaaS Provisioning service waits for the application’s subscription asynchronous callback to execute, before it changes the subscription status to FAILED. |
allowContextUpdates | Whether to send updates about the changes in contextual data for the service instance.For example, when a subaccount with which the instance is associated is moved to a different global account.Defaut value is false. |
Mount the Secret as a volume to the pod:
volumeMounts: ...... - name: saas-registry-volume mountPath: "/etc/secrets/sapcp/saas-registry/saas-registry-service" readOnly: true ...... volumes: ...... - name: saas-registry-volume secret: secretName: saas-registry-service-binding
Step 5: Deploy the Multitenant Application to the Provider Subaccount
In order to run your code on the Kyma Runtime (or on any Kubernetes-based platform), you need to provide an OCI image (aka Docker image) for your application. While you are in principle free to choose your image building tool, we recommend using .
The command-line tool pack
supports providing a buildpack and your local source code and creating an OCI image from it. We are working on a process to provide recommended and supported buildpacks. In the meantime, you can use the community-supported .
Log in to Docker using this command:
docker login -u <docker-id> -p <password>
Under the directory kyma-multitenant-approuter
, build the image for the approuter app from source, for example:
pack build multitenant-approuter --builder paketobuildpacks/builder:full docker tag multitenant-approuter tiaxu/multitenant-approuter:v1 docker push tiaxu/multitenant-approuter:v1
Under the directory kyma-multitenant-node
, build the image for the approuter app from source, for example:
pack build multitenant-kyma-backend --builder paketobuildpacks/builder:full docker tag multitenant-kyma-backend tiaxu/multitenant-kyma-backend:v1 docker push tiaxu/multitenant-kyma-backend:v1
Then we are ready to deploy it into the Kubernetes cluster with Kyma runtime.
Click on the Link to dashboard
to open the Kyma runtime console UI.
In the Kyma runtime console, download the kubeconfig.yml
file, which is used to configure access to a cluster. And, don’t forget to set an environment variable KUBECONFIG
to identify the directory where the kubeconfig.yml
file is so that you can create resources through kubectl
CLI. For more details on how to set the variable, please visit the .
Create a new namespace through the Kyma runtime console or kubectl
CLI, e.g. called multitenancy-ns
:
For the post, we assume that the images will be stored in a private repository on Docker hub or in a company repository like JFrog Artifactory. Therefore, you need to provide the access information to your Kyma cluster that you can pull the images from those repositories. Therefore, all deployment.yaml
files contain an imagePullSecret
entry, which is set to registry-secret
.
imagePullSecrets: - name: registry-secret # replace with your own registry secret
If you are using Docker hub and a private Docker repository, see the for more details.
As you can only create one private repository in a free Docker hub account, we have made sure in our instructions, that Docker images stored on Docker hub will have different tag names so that they can be stored under one repository.
When we speak about repository name, we mean the combination of account and repo name that is usual with docker hub: <docker account>/<repo name>
. An example would be tiaxu/kyma-multitentant
.
Addressing an image will include the tag name:<docker account>/<repo name>:<tag name>
. An example would be tiaxu/kyma-multitentant:v1
.
Apply the secret with this command for your namespace that needs to pull images from this repository:
kubectl -n <namespace> create secret docker-registry registry-secret --docker-server=https://index.docker.io/v1/ --docker-username=<docker-id> --docker-password=<password> --docker-email=<email>
Deploy services by executing this command:
kubectl -n multitenancy-ns apply -f k8s-deployment-services.yaml
Deploy approuter application by executing this command:
kubectl -n multitenancy-ns apply -f k8s-deployment-approuter.yaml
Deploy backend nodeJS applications by executing this command:
kubectl -n multitenancy-ns apply -f k8s-deployment-backend.yaml
Step 6: Subscribe SaaS Application by a Consumer
Now, a consumer can subscribe to the application through the SAP BTP Account Cockpit.
Switch to another subaccount under the same Global Account with the multitenant application provider subaccount, you can see and subscribe the multitenant application.
Create an instance for the SaaS Application:
Click on Create
button:
Once it is subscribed, you can try to access it by clicking on the Go to Application
button:
The SaaS application will display the logged in user’s name and customer’s tenant-related information, shown as below:
Conclusion
This post showed you how to use the SaaS registry, XSUAA as well as SAP Application Router to build a multi-tenant application in BTP Kyma Runtime based on a NodeJS application.
If you would like to know how to build a multi-tenant application in BTP Cloud Foundry Runtime, you can read this .
For more general descriptions of how many steps it takes to do from a normal application to a multitenant application, you can read this .