Skip to Content
Technical Articles
Author's profile photo Tia Xu

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.

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

Assigned Tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo Venu Ravipati
      Venu Ravipati

      Thank you Tia Xu for the details on multitenancy with Kyma