Multitenancy – Develop and Register Multitenant Application to the SAP SaaS Provisioning Service on the SAP BTP: Hands-on Tutorial on Kyma

In this post, you can get a step-by-step tutorial that shows 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 blog.

For more general descriptions of how many steps it takes to do from a normal application to a multitenant application, you can read this blog.

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: Express application generator. Or, you can git clone the skeleton project directly from: here.

    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 Development on BTP.

BTP account

You can get a Free Account on SAP BTP Trial by following this guide, then enable a Kyma Environment in the account by following this guide. 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 appear multitenancy-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: here.

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 SAP Application Router.

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:

  1. 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}
   ] 
  1. 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 approuter uses @sap/xsenv 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 hanahanaInst1 and hanaInst2 each with their own credentials/configurations and one instance of service xsuaa called xsuaaInst 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 @sap/xsenv.

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 Kubernetes client for NodeJs.

//******************************** 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 Develop the Multitenant Application).
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 Cloud Native Buildpacks (CNB).

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 Paketo Buildpacks.

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 official Kubernetes website.

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 Kubernetes documentation 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 blog.

For more general descriptions of how many steps it takes to do from a normal application to a multitenant application, you can read this blog.