Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
mariusobert
Developer Advocate
Developer Advocate
6,999
In this Cloud-Native Lab post, I'll review multi-tenant development in SAP BTP. This blog post won't go deep into the theory (I'll sketch the rough idea and link some resources, though) but instead, focus on a sample guestbook application that can be understood and built quickly with minimal effort.


Two tenants of the guestbook sample project


The image above shows two tenants of the guestbook application that you will build in this post. The idea of this guestbook is quite simple; each tenant will have its own guestbook that is available under a unique URL. The application comes with two role templates - reader and author. Readers can see all existing entries of that tenant, and authors can also add new entries. Once the project is deployed to the provider subaccount, you can create any number of tenants from the service marketplace.

To keep things simple, we won't add any persistence layer. We will use a standard JSON object in our extended approuter to temporarily save some data. Consequently, all data is lost once the application restarts, but this is fine for our simple demo. If you would like to persist the data, please look at the multitenancy guide from CAP.

Multi-Tenancy


Multitenancy refers to software architecture, in which tenants share the same technical resources bu...

Using a single set of computing resources for multiple tenants (aka clients or customers) has several advantages, such as a reduced maintenance effort or a lower cost of ownership. In other words: You only need one approuter and one backend application (like a CAP app) to serve any number of clients. This works because these applications neither hold any user information nor any tenant data (as this data is stored in individual HDI containers). Therefore, every additional tenant only comes with the overhead of one additional HDI container. This step can easily be automated - but in our database-less Guestbook sample, it isn't necessary at all.

The easiest way to build a multi-tenant project in SAP BTP is by using the  SAP SaaS Provisioning Service. An application can register with this service from a so-called "provider subaccount" to declare that they can serve multiple tenants. Once this registration happened, other (consumer) subaccounts that live in the same global account can subscribe to them via the service marketplace in the SAP BTP Cockpit.


Service Marketplace showing the Guestbook subscription


There are already several good blog posts that explain the underlying concepts of multi-tenancy. Have a look at them if you want to dive deeper into this topic:

 Multi-Tenancy in SAP BTP Trial


One thing that isn't highlighted in these posts is multi-tenancy in SAP BTP Trial. This is why I want to go a little bit deeper on this topic here.

Usually, you would use a dot (.) to separate the tenant id from the rest of the application URL in the tenant host pattern (like this ^(.*).application.cfapps.eu10.hana.ondemand.com). This configuration is not forbidden in the trial landscape and you  will even be able to deploy the project to the provider subaccount. But later, during the subscription, you will receive the following error message:

“No subject alternative DNS name matching <tenant id>.<app name>.cfapps.eu10.hana.ondemand.com found”

You'll get this error because you are trying to define a route with multiple dots, but this is not possible in the free SAP BTP Trial.
In a productive multi-tenant approuter, you need to apply for a custom domain. In this scenario, you can create a Cloud Foundry route with a wildcard host. This wildcard enables calls to the approuter from subscriber subaccount without creating a new route for each subscriber.

For example:

1. You apply for custom domain application.company.com

2. You create a cf route *. application.company.com 

3. You set the tenant host pattern to ^(.*).application.company.com

4. onSubscription URL would be provider.application.company.com

5. Subscriber URLs will contain subscriber subdomain in the host, e.g., subscriber1.application.company.com

The advantage of this scenario is that you only need one cf route (with a wildcard) to cover all tenants.

In Trial, this looks different as the cf route cannot contain two dots in the subdomain. That's why we'll use a dash (-) instead of a dot (.) as a separator of the tenant host pattern ^(.*)-application.company.com. As a result of that pattern, the callback endpoints of the approuter should look like this:
onSubscription: https://provider-mtx-guestbook.cfapps.eu10.hana.ondemand.com/callback/v1.0/tenants/{tenantId}
getDependencies: https://provider-mtx-guestbook.cfapps.eu10.hana.ondemand.com/callback/v1.0/dependencies

As the wilcard option won't work in the trial landscape, you need to add at least two fixed routes (one per tenant). While this is a little bit ugly, it's not too bad as we're just writing a demo app for the sake of learning. And for that, we use the free standard SAP domain "cfapps.eu10.hana.ondemand.com". One route will contain the subdomain of the provider subaccount and then you need one more for each planned consumer subaccount. These routes can easily be defined with the mta.yaml parameter routes:
  - name: mtx-approuter
type: approuter.nodejs
path: router
parameters:
routes:
- route: https://provider-application.company.com
- route: https://consumer-application.company.com
- route: https://secondConsumer-application.company.com

The credits for this great explanation go to sergio.rozenszajn. Many thanks for your help!

 

So much for the theory. Let's build this sample application!

Hands-on: Build a guestbook application


This section will show you the most important files of the guestbook sample project from above. Note that you can find the sample code on GitHub. Feel free to compare your files to the originals there or clone the repository as a whole.

0. Preparation


But before we start, you need to install a few tools. Please install Node.js, mbt, git, and the Cloud Foundry CLI (incl the MultiApps plugin) if you haven't done so. In case you need help, I recommend following this tutorial group that contains step-by-step guides.

As we want to build a multi-tenant application, you need at least two subaccounts that run in the same region. The first one will be the provider subaccount that hosts the application and all service instances. The second one is the consumer subaccount. We will use this one to subscribe to the application - there is no need to activate any runtime or to assign entitlements to the consumer subaccount. A plain subaccount will do the job.

1. Build the web application


Let's start with the web app HTML5Module/mainfest.json. We will deploy the app to the HTML5 application repository, so we need to include a name for the business service (next to the mandatory properties) of the web app to the manifest:
{
"_version": "1.12.0",
"sap.app": {
"id": "mtx-guestbook",
"type": "application",
"applicationVersion": {
"version": "1.0.0"
}
},
"sap.cloud": {
"service": "cloud.service"
}
}

I dismissed the MVC pattern here to keep this demo as tiny as possible. Therefore, the entire web app is defined in the HTML5Module/index.html file.
<!DOCTYPE HTML>
<html>

<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<link rel="shortcut icon"
href="https://static.community.services.sap/com-hdr/v7/453.190.7/shared-ui/1dx-assets/images/favicon.png"
type="image/x-icon">
<title>MTX Guestbook</title>
<script src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" id="sap-ui-bootstrap" data-sap-ui-libs="sap.m"
data-sap-ui-theme="sap_fiori_3_dark">
</script>

<style>
.sapMPageEnableScrolling {
padding: 35px;
}
</style>
<script>

const guestbook = new sap.ui.model.json.JSONModel({})

fetch("/guestbook").then(async (res) => {
if (res.ok) {
const data = await res.json();
guestbook.setData(data);
}
});

new sap.m.App({
pages: new sap.m.Page({
title: {
path: "/tenant",
formatter: tenant => `Multi-tenant Guestbook (${tenant})`
},
headerContent: new sap.m.Button({
icon: "sap-icon://log",
tooltip: {
path: "/user",
formatter: user => `Logout ${user}`
},
press: function () {
window.location.replace("/logout");
}
}),
content: [
new sap.m.List({
showSeparators: "Inner",
items: {
path: "/entries",
template: new sap.m.FeedListItem({
showIcon: false,
sender: "{author}",
timestamp: {
path: "timestamp",
formatter: ts => new Date(ts)
},
text: "{content}",
convertLinksToAnchorTags: "All"
})
}
}),
new sap.m.FeedInput({
showIcon: false,
enabled: {
path: "/canWrite",
formatter: scope => !!scope
},
post: async function (oEvent) {
const input = oEvent.getParameter("value");
const csrfRes = await fetch("/guestbook", {
method: "HEAD",
headers: { "x-csrf-token": "fetch" }
});
const res = await fetch(`/guestbook?content=${input}`, {
method: 'POST',
headers: { "x-csrf-token": csrfRes.headers.get("x-csrf-token") }
});
const newData = await res.json();
guestbook.setData(newData);
}
})
]
})
}).setModel(guestbook)
.placeAt("uiArea");
</script>
</head>

<body class="sapUiBody">
<div id="uiArea"></div>
</body>

</html>

Note that our entire app consists of only six ready-to-use SAPUI5 components and some trivial data binding. I think this example perfectly shows how (among other use-cases) SAPUI5/OpenUI5 can be used for rapid prototyping.  

As we want to upload the webapp to the HTML5 application repository, we need a HTML5Module/xs-app.json file. This file is mostly empty, but we need it anyway.
{
"routes": []
}

The last substep for the web app will help us package all resources into one zip archive to push the app to the HTML5 application repository. Using npm scripts in the HTML5Module/package.json file is one of the most convenient ways to realize this:
{
"name": "html5module",
"version": "0.0.1",
"scripts": {
"build": "npm run clean && npm run zip",
"zip": "npx bestzip HTML5Module-content.zip *",
"clean": "npx rimraf HTML5Module-content.zip dist"
}
}

2. Extend the default application router


Real persistency is no requirement for this guestbook, so it's ok to lose data on every restart. This "requirement" gives us the freedom to store all data in a JSON object in the application memory. To store these values, we need to customize the approuter to add a new endpoint. Add the following router/extended-server.js file:
const approuter = require('@sap/approuter');

const ar = approuter(),
entries = {};

ar.beforeRequestHandler.use('/guestbook', function myMiddleware(req, res) {
if (req.isUnauthenticated()) {
res.statusCode = 401;
res.end("Unauthorized");
return;
}
const canRead = req.user.scopes.find((scope => scope.includes("Read")));
if (!canRead) {
res.statusCode = 401;
res.end("Unauthorized");
return;
}
const canWrite = req.user.scopes.find((scope => scope.includes("Write")));
const tenant = req.user.tenant;
if (req.method === "POST" && canWrite) { // not the best permission check but ok for demo
if (!entries[tenant]) {
entries[tenant] = [];
}
entries[tenant].push({
content: req.query.content,
author: req.user.name,
timestamp: new Date()
})
}
res.end(JSON.stringify({
tenant,
canWrite,
user: req.user.name,
entries: entries[tenant]
}));
});
ar.start();

This mini script starts an approuter that contains one additional endpoint (/guestbook). Authenticated users can use the endpoint to receive all entries (with HTTP GET) or add a new entry (with HTTP POST). 

Let's say only users who have the scope Reader can access this web application. Besides, the approuter should also forward traffic to our web application and allow any users to see the logout page. All these features can be controlled with the router/xs-app.json configuration file:
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/logout",
"logoutPage": "/logout.html"
},
"routes": [
{
"source": "^/logout-page.html$",
"target": "/mtxguestbook/logout-page.html",
"service": "html5-apps-repo-rt",
"authenticationType": "none"
},
{
"source": "(.*)",
"scope": ["$XSAPPNAME.Read"],
"target": "/mtxguestbook/$1",
"service": "html5-apps-repo-rt"
}
]
}

Add the router/package.json file to declare the dependency to the standard approuter and define the start script.
{
"name": "appouter",
"description": "Node.js based application router service for html5-apps",
"dependencies": {
"@sap/approuter": "^10.2.0"
},
"scripts": {
"start": "node extended-server.js"
}
}

3. Tieing it all together with the project manifest


This is the most critical part of this application. The project manifest connects all the dots that we discussed so far and creates the needed service bindings. Copy this snippet to a new file mta.yaml, and then let's discuss the individual parts of it.
ID: mtx-guestbook
_schema-version: "2.1"
version: 1.0.0

parameters:
appname: mtx-guestbook
subdomain: <your provider subdomain>

modules:
- name: mtx-approuter
type: approuter.nodejs
path: router
parameters:
routes:
- route: https://${subdomain}-${appname}.${default-domain}
- route: https://<your first consumer subdomain>-${appname}.${default-domain}
- route: https://<your second consumer subdomain>-${appname}.${default-domain}
disk-quota: 256M
memory: 256M
host: ${appname}
domain: ${default-domain}
requires:
- name: html5-rt
- name: uaa
- name: saas-registry
properties:
TENANT_HOST_PATTERN: "^(.*)-${appname}.${default-domain}"
- name: html5_deployer
type: com.sap.application.content
path: .
requires:
- name: html5-host
parameters:
content-target: true
build-parameters:
build-result: resources
requires:
- artifacts:
- HTML5Module-content.zip
name: HTML5Module
target-path: resources/
- name: HTML5Module
type: html5
path: HTML5Module
build-parameters:
builder: custom
commands:
- npm run build
supported-platforms: []
resources:
- name: html5-host
type: org.cloudfoundry.managed-service
parameters:
service: html5-apps-repo
service-plan: app-host
service-name: ${appname}-html5-host
- name: html5-rt
parameters:
service: html5-apps-repo
service-plan: app-runtime
service-name: ${appname}-html5-rt
type: org.cloudfoundry.managed-service
- name: uaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
service-name: ${appname}-uaa
config:
xsappname: ${appname}
tenant-mode: shared
scopes:
- name: $XSAPPNAME.Read
description: Read permission
- name: $XSAPPNAME.Write
description: Write permission
- name: $XSAPPNAME.Callback
description: With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called.
grant-as-authority-to-apps:
- $XSAPPNAME(application,sap-provisioning,tenant-onboarding)
foreign-scope-references:
- uaa.user
role-templates:
- name: Reader
description: Can read
scope-references:
- $XSAPPNAME.Read
- name: Author
description: Can read and write
scope-references:
- $XSAPPNAME.Read
- $XSAPPNAME.Write
- name: saas-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
service-name: ${appname}-saas-registry
config:
xsappname: ${appname}
appName: ${appname}
displayName: Guestbook
description: A guestbook app to explain the concepts of Multitenancy
category: Custom Apps
appUrls:
onSubscription: https://${subdomain}-${appname}.${default-domain}/callback/v1.0/tenants/{tenantId}
getDependencies: https://${subdomain}-${appname}.${default-domain}/callback/v1.0/dependencies

The first thing you might notice is the usage of customer parameters (appname and subdomain). We can use these parameters as variables and reuse them throughout this file.

The first module is our extended approuter. Note that we use the routes parameter to define one route for the provider subdomain and two for the subdomains of the tenants. Change these values to the subdomains that you use in your setup. Besides this, we change the host parameter to get a URL that is a little bit shorter and easier to read than the default host. Besides the standard service bindings (html5-rt and uaa), you'll also notice two multi-tenant-specific artifacts there: The service binding to the saas registry service and the tenant host pattern that we discussed above.

The second and third modules are business-as-usual. Here we trigger the zip of the content of the HTML5Module and pass the archive to a deployer module. This deployer will upload the static resources to the HTML5 application repository during deployment.

Let's jump to the resources, aka the service instance definitions: The first two resources are the html5-apps-repo instances to upload the static resources (bound to the deployer) and to consume them (bound to the approuter).

The third service instance is mostly just a regular uaa service instance that defines three scopes and two role templates. Note the third scope $XSAPPNAME.Callback that is required to allow multiple tenants for the apps. It's a little bit unusual that we defined the uaa service within the mta.yaml file, whereas you might be more familiar with the definition as an external JSON file. I decided to do it like this to be able to reuse the parameter appname here but the other way works here as well.

Last but not least, we got the saas-registry service definition. This definition uses the parameters to link to the uaa service (via the xsappname) and approuter (via the appUrls). It also contains other configuration parameters like appName, displayName, description, and categorythat will be used in the BTP Cockpit to identify the subscription service.

At this point, I recommend that you compare your project to the sample code on GitHub to make sure everything is ready for deployment.

4. Deploy the project to the provider subaccount


I'm sure you've done this plenty of times by now. Run the following commands to deploy your app:
mbt build
cf deploy mta_archives/mtx-guestbook_1.0.0.mtar

5. Subscribe to the Guestbook



1. Go to the consumer subaccount and find the subscription. Click on the three dots and select Create



2. Keep the default values and confirm with Create




3. Click View Subscription to open the new subscription


4. Once you are subscribed, use the + icon to add the Author role template to one of your role collections (create one if needed)


5. Hit Go To Application to open your tenant



6 Test the app


You can notice a redirect to the authentication service of SAP BTP if you watch the address bar of your browser closely. This redirect means that you are automatically logged on via SSO to your new tenant. Depending on the assigned role template, you can see and possibly add new entries to the guestbook. New entries are automatically associated with the email address of your user. This flow shows the power of SSO in combination with the UAA service.


Adding new entries to the guestbook


From here, you could even create a second subaccount and subscribe there as well.

Troubleshooting


What to do when I get a red "Subscription failed" message?


There can be multiple reasons for a "Subscription failed" error message


My tip would be to inspect the payload of the request that returned the error message:

  1. Open the network tab of the dev tools of your browser.

  2. Filter all requests for getCFSaaSApplications.

  3. Find the object within the array that represents your subscription.

  4. Inspect the property stateDetails.message. The cause is usually mentioned in the last sentence of this string:


This error message, for example, indicates that the application is not running.



Summary


We saw that multi-tenant projects look almost like single-tenant projects. The main differences in the "knows components" are (a) the definition of the xsuaa service instance that uses the shared tenant-mode and contains an additional scope as well as (b) the TENANT_HOST_PATTERN environment variable of the application router. The component that we haven't used before is the SaaS registry service instance. This instance registers the multi-tenant application in SAP BTP and contains all needed information to display the subscription in the BTP cockpit of the consumer subaccounts.

We also learned about the workaround with fixed routes that is required for multi-tenant applications that run in SAP BTP Trial.

Next Steps



Previous episode: Cloud-Native Lab #3 – Comparing Cloud Foundry and Kyma Clients

Next episode: TBD

 
20 Comments
kachin_agarwal
Advisor
Advisor
0 Kudos
Hi Marius,

Great blog. Thanks for sharing. I have a good understanding on Multi-tenant apps now.

I have a beginners question. You have explained the above multi-tenant application based on standalone app-router. How can i achieve multi-tenancy for application with managed app-router?

Thanks,

Kachin
mariusobert
Developer Advocate
Developer Advocate
0 Kudos

I'm glad you found this post useful.

Update: I checked and this is currently not possible, sorry.

 

kachin_agarwal
Advisor
Advisor
Thank you so much for the reply Marius 🙂
former_member748
Participant
0 Kudos

Hello mariusobert ,

Cool and useful blog!

I am wondering why is it not possible to achieve multitenancy with managed approuter?

I saw this documentation available. Is it not refering to this topic? I have not yet tried, but I am currently looking for a way to do this.

Thank you,

Best regards,

Stefania

mariusobert
Developer Advocate
Developer Advocate
0 Kudos
Hi Stefania,

as far as I can see, this approach uses the launchpad module (which is something different than the managed approuter).
former_member748
Participant
Hello Marius,

Thanks for your quick reply! It's clear now

I was under the wrong impression that when using the Portal service you have to use the managed approuter.

Thank you,

Stefania
franklinlin
Advisor
Advisor
0 Kudos
When I add some logs in app . In BTP platform where to see these logs ?
mariusobert
Developer Advocate
Developer Advocate
0 Kudos
Which app are you referring to - the approuter or the web app?

The logs of the approuter can be found in the BTP cockpit (provider subaccount) or via the CF CLI.

The logs of the web app can only be access from the client's browser.
prasad
Participant
0 Kudos

Hi @Marius Obert

Thanks for the  blog .

How the above approach is different from generator-saphanaacademy-saas  Yeoman Generator . i see a different module for SaaS and also a xs-security.json file there

Regards

Prasad

 

mariusobert
Developer Advocate
Developer Advocate
0 Kudos
Hi Prasad,

it's hard to compare these two. The sample above is just that - a sample, the generators is a tool that you could use to build any saas solution, for example the sample above.

 

Regarding the xs-security.json: The config of the uaa service can be done in the mta.yaml (as done in the sample) or in a json file. Both will have the same result.
prasad
Participant
0 Kudos
Thanks for the reply Marius Obert.
mirco_roth
Advisor
Advisor
0 Kudos
Hi Marius,

great blog!! You covered so many topics and helped me a lot with this.

I´ve got one question and maybe you can help me:

How can I run scripts after a customer subscribed? In some git repos I saw files named "subscribe.sh" which should be called after the user clicks the subscribe button.

How can we achieve this in a spring boot context on Cloud Foundry?

Thank you a lot in advance!

Best regards
Mirco
mariusobert
Developer Advocate
Developer Advocate
0 Kudos
A good question (that can become complex very fast)!
The short answer is that your app needs to provide it's own "onSubscription" hook that executes your scripts and then calls the "onSubscription" hooks of your dependencies.

 

If you have this hook, make sure that you reference it in the definition of the "saas-registry"
mirco_roth
Advisor
Advisor
0 Kudos
Thanks Marius for your super fast response!
So there is no "easy" solution to just put in the shell script into the saas-config.json like:

"onSubscription" :  "run "myScript.sh"



Also do you have any example by any change in some git repo? 🙂


Best regards
Mirco
mariusobert
Developer Advocate
Developer Advocate
0 Kudos
No, there's no option to run a script - this needs to be done by your application.

 

Something similar happens in a Kyma sample app, so maybe this repo helps you.
thomasswolfs
Explorer
0 Kudos
Hi Marius,

Thanks for this informative blog post!

I was wondering if you have any information about best practices considering load balancing.
Is there a way to create several instances in different regions while using the same resources? For example caching inside a node js application.

Is there a strategy being pushed forward by SAP / anything on the road map considering this topic?

Kind regards,

Thomas

 
mariusobert
Developer Advocate
Developer Advocate
0 Kudos

Hi Thomas,

I'm afraid I can't fully answer this question as I'm that deep into these topics.

To my knowledge, CF and Kyma both come with built-in load balancing that kicks in when there are multiple app instances. As of now, runtimes and services are bound to a region, e.g. you cannot cross-consume without building an abstraction layer yourself.

 

Edit: I just found this post which might also be able to help you.

ancutamezi
Product and Topic Expert
Product and Topic Expert
0 Kudos
Hi mariusobert

Could you please let me know if this actually means that we cannot use the launchpad service for a multitenant saas app?

I found tutorials using the launchpad service, but not for saas app.

In order to still be able to have a launchpad ui, we should still stick with the portal service?

 

Thank you,

Ancuta
mariusobert
Developer Advocate
Developer Advocate
0 Kudos
Hi Ancuta,

as stated above, it's currently not possible to use the "managed approuter" for SaaS apps. Please note that "managed approuter" only refers to one feature of the Launchpad or Portal service.

You are still able to use the other features of the service with a subscribed web app when you add it manually to the launchpad.
sreehari_vpillai
Active Contributor
0 Kudos
Marius ,

We are in Dec 2022 - Does managed router now support multi tenancy ?

I after successful deployment, I cannot see this deployed application listed in the "HTML5 Applications" in providers as well as in the consumers sub account - is it expected ?