Skip to main content

Introduction to multi-tenant concepts

Overview

The code discussed in this guide can be found in the samples repository. The idea behind this tutorial is to explain the main concepts of multi-tenant applications and how to implement them on SAP BTP. The code in the example is not a copy-paste solution for productive use but a didactic sample. You need to adjust things like application names or routes for your use case.

The term "multi-tenant application" is more or less a synonym for a software as a service (SaaS) offering. The idea behind this architecture is that the consumers share the application resources, so they are used more effectively. On SAP BTP, organizations are modeled by subaccounts, and multi-tenant applications are subscribed per subaccount. These organizations are potentially different companies or strongly separated parts of one company. In any case, each organization has its subaccount on SAP BTP and subscribes to a multi-tenant application.

Some vocabulary is necessary to understand the next chapters of this tutorial:

  • provider account: The SAP BTP account which hosts the actual application. This account is under the control of the application developer.
  • subscriber accounts: The accounts using the application. These accounts are controlled by the consumer.
  • tenant-aware service: A service which separates the data of different subscriber accounts rigorously. If you build a multi-tenant application, all services handling account specific data need to be tenant-aware services.
note

This tutorial is not a complete guide on the multi-tenancy topic in SAP. It rather covers only points where the SAP Cloud SDK team saw the need of a more detailed guide with samples. Have a look at the following guides and tutorials offering detailed information:

info

Note that the subscriber and provider account need to be in the same global account. In case you want to offer a service across global accounts you may follow the service broker approach which has other limitations.

Prerequisites

To execute this tutorial, you need:

  • Two CF subaccounts in the same global account to represent provider and subscriber accounts.
  • The provider account needs some quota:
    • To host two applications (sample application and approuter)
    • To create a service instance for the destination and XSUAA service
  • You need a basic understanding of SAP BTP and the Cloud Foundry CLI.

The Application

The application is a minimal example which contains only one endpoint containing business logic. This endpoint will call the destination service using the SAP Cloud SDK. Since the destination service is tenant aware, it can be used to illustrate service usage within your multi-tenant application. You can find the application code in the multi-tenant-app folder. The relevant application logic and configuration is located in the following three files:

  • In the application.ts file, the different endpoints are defined. For now, only the /service endpoint is relevant, which represents our multi-tenant service.
  • In the manifest.yml file, the route to the application is given and the used services are defined.
  • In the service-endpoint.ts a tenant-aware service (destination service) is called and tenant information is collected. The endpoint represents the service offering for the subscriber accounts.
import * as bodyParser from 'body-parser';
import express from 'express';
import { serviceRoute } from './service-endpoint';
import { dependencyRoute } from './dependencies-endpoint';
import { subscribeRoute, unsubscribeRoute } from './subscription-endpoint';

class App {
public app: express.Application;

constructor() {
this.app = express();
this.config();
this.routes();
}

private config(): void {
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: false }));
}

private routes(): void {
const router = express.Router();

router.get('/service', serviceRoute);
router.get('/dependencies', dependencyRoute);
router.put('/subscription/:subscriberTenantId', subscribeRoute);
router.delete('/subscription/:subscriberTenantId', unsubscribeRoute);
router.get('/index.html', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
this.app.use('/', router);
}
}

export default new App().app;

Deploy the Application

Before you can deploy the application, you need to create a service instance for the destination and XSUAA service in your account. There is an xs-sercurity.json file in the service-config folder to create the XSUAA instance. Align the name of your service instances with the ones in the manifest.yml. Also, adjust the route to use the region of your CF, e.g., cfapps.us10.hana.ondemand.com, and the route path to make it unique in the region.

Now, log into CF using the CLI cf login and enter the account information of the provider account. Navigate to the multi-tenant-app folder and execute cf push to deploy the application.

Call the Service

In our example, the service is reachable via (for you the URL will be different depending on landscape):

GET https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/service

The implementation of service-endpoint.ts does the following:

  • Extracts a JSON web token (JWT) from the request.
  • Fetches a destination with the name myDestination using the destination service.

Since there is no destination with that name, the service will return 404.

Create a destination with the name in your provider account and also enter some description for that destination. After the destination is created, the service should return:

No jwt given in request. Provider tenant used. The destination description is: Provider Destination Description

The Approuter

The response shows that there is no JWT attached to the request. This task is done by the application router, the XSUAA, and the identity provider (IdP). Just think of the approuter as an application taking requests and initiating the authorization flow with the XSUAA and IdP. Once the user enters their credentials, the request is sent to the target with the JWT issued for the user and account.

note

More information on the approuter topic can be found in this guide.

In a productive case, the approuter may redirect requests to multiple applications. In our simple example, there is just one route.

The approuter does not require code, only configuration. You can find all files in the approuter folder.

  • The manifest.yml file contains the config for the approuter
  • The xs-app.json file contains the config for the route resolution.
applications:
- name: approuter
routes:
- route: 'route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com'
path: .
memory: 128M
buildpacks:
- nodejs_buildpack
env:
TENANT_HOST_PATTERN: 'route-prefix-(.*).cfapps.YOUR_REGION.hana.ondemand.com'
destinations: >
[
{"name":"multi-tenant-app","url":"https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com","forwardAuthToken":true}
]
services:
- xsuaa
- destination

Deploy the Approuter

Please adjust the route property in the manifest.yml file. Replace the placeholders for subdomain and region. Log into the provider account using cf login and call cf push from the approuter directory. This deploys the approuter. Once the approuter is deployed, you will see it running as a separate application next to your multi-tenant-app.

When you open the approuter application you see one route created by the manifest:


GET https://route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com/

When you follow this route, you will get redirected to the welcomeFile defined in the xs-app.json. The index.html is located in the application. How did the routing work:

  • In our simple scenario, the xs-app.json file defines only one route consisting of a source, target, and destination. The source is a regex and the target defines which capturing group is used in the destination. In our example https://route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com/SOME_VALUE will lead to SOME_VALUE as the capturing group and SOME_VALUE is attached to the destination. There are many more options to the routing config explained here.
  • The manifest.yml defines the available destinations for the approuter. The destination multi-tenant-app points to the URL of our application. Therefore https://route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com/SOME_VALUE goes to https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/SOME_VALUE

Call the Service via the Approuter

The reason for introducing the approuter was the missing JWT in the request. If you call the service via the approuter:


GET https://route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com/service

you will see a response like:


You are on tenant: a89ea924-d9c2-4gaf-84fb-3ffcff123456. The destination description is: Provider Destination Description

which shows that the request contains a JWT issued for the provider account.

The Subscription

Up to now, you called the application via the provider account. In this chapter you learn how to call the service from a different account.

The first thing to do is to create an instance of the SaaS provisioning service in your provider account. You can find the saas-registry-config.json in the samples repository. This makes the service subscribable from other accounts. You need to adjust the providerTenantId to contain your ID and the appUrls to match your region and application URL. Within the saas-registry-config.json two URLs are mentioned: the getDependencies and onSubscription.

{
"xsappname": "xs-multi-tenant-sample-app",
"appName": "multi-tenant-app",
"providerTenantId": "YOUR_TENANT_GUID",
"displayName": "multi tenant example application",
"appUrls": {
"getDependencies": "https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/dependencies",
"onSubscription": "https://multi-tenant-app.cfapps.YOUR_REGION.hana.ondemand.com/subscription/{tenantId}"
}
}

These two endpoints are the entry point for the SAP BTP platform to:

  • Create and delete a subscription to the application
  • Obtain the services used by the application

In our example the application uses the destination service. The application has a binding to a service instance, so it is clear that the application may call the service. However, the subscriber account does not know anything about the internal details of the application. Therefore, the /dependencies endpoint provides the information that the destination service may be used also from the subscriber account.

Remember to add all SAP BTP services used by your application in the response of the dependencies endpoint. If you do not do that, you retrieve a 403 error from the XSUAA when you request a service token on behalf of a subscriber account.

Creating a Subscription

After you have created an instance of the SaaS provisioning service in the provider account, you can create a subscription via the user interface. A subscription is a route to the provider application, including the unique subdomain of the subscriber account. The TENANT_HOST_PATTERN in the manifest.yml of the approuter defines how to extract the subscriber account from the URL. A route like:

GET https://route-prefix-YOUR_SUBDOMAIN.cfapps.YOUR_REGION.hana.ondemand.com/service

would mean that the subscriber account is YOUR_SUBDOMAIN. To automate the onboarding of accounts, the script in subscription-endpoint.ts does the following:

  • It creates a route
  • Binds the created route to the approuter.
  • Returns the route URL so that it can appear in the subscriber account.

The creation of the route uses the CF API. Unfortunately, there is no out-of-the-box access of this API when you are in the context of an application. The code assumes a destination with the name cf-api in the sample implementation which contains the access data for the CF API:

propertyvalue
namecf-api
authenticationOAuth2Password
usera user with permission to the provider account
passwordpassword of this user
client idcf
client secretempty string
token service URLhttps://login.cf.YOUR_REGION.hana.ondemand.com/oauth/token
URLhttps://api.cf.YOUR_REGION.hana.ondemand.com

You have to adjust the URL and token service URL to for your region e.g. https://api.cf.us10.hana.ondemand.com. Once the destination is present, you can subscribe to the application and routes are created automatically.

Log into your second SAP BTP account. Go to Service->Instances and Subscriptions and create a subscription to the multi-tenant-app. Once the application is subscribed, you can have a look at the approuter in the provider account. You should see a second route with the subdomain of the subscriber. If you call the new route:

GET https://route-prefix-someSubscriberDomain.cfapps.YOUR_REGION.hana.ondemand.com/service

you will see a response with the tenant ID from the subscriber account:

You are on tenant: a89ea924-d9c2-4gaf-84fb-3ffcff7891011. The destination description is: Provider Destination Description.

The approuter has extracted the subscriber subdomain from the URL and issued a token for this account. As an application developer, you can use the token to determine the account which calls your code.

Removing a Subscription

If the consumer deletes the subscription, the SAP BTP will invoke the DELETE method on the subscription-endpoint. The code will remove the route from the approuter and make the application unreachable for that consumer. The details of the implementation can be found in the subscription-endpoint.ts of the sample application.

Real World View

The presented example is totally artificial. This chapter elaborate a bit on what an actual multi-tenant application would look like and how the SAP Cloud SDK helps you. Different consumer are divided by their unique application URL including their subdomain. However, up to now, nothing subscriber-specific is happening in the implementation.

To get an idea create a destination in the subscriber account with the same name myDestination with a different description e.g. Subscriber Destination. A call to the same /service endpoint will lead now :

You are on tenant: a89ea924-d9c2-4gaf-84fb-3ffcff7891011. The destination description is: Subscriber Destination.

The destination of the subscriber account is used at runtime, because the call in the service-endpoint.ts uses the selection strategy subscriberFirst. You can change this by using different selection strategies. This enables consumers to maintain their custom destination used within a multi-tenant application. The destination from the provider account could be seen as a fallback.

This is only one example of a tenant-aware service. Imagine a database with a tenantId column to store consumer specific configuration. You can extract the value from the JWT as shown in the example:

const jwt = retrieveJwt(req);
const tenantId = jwt ? decodeJwt(jwt).zid : `No jwt given - Provider Tenant?`;
//do something for the specific tenantId