Deploy as Multitenant SaaS Application
Intro & Overview
Software-as-a-Service (SaaS) solutions are deployed once by a SaaS provider, and then used by multiple SaaS customers subscribing to the software.
SaaS applications need to register with the SaaS Provisioning Service to handle subscribe
and unsubscribe
events. In contrast to single-tenant deployments, databases or other tenant-specific resources aren't created and bootstrapped upon deployment, but upon subscription per tenant.
CAP includes the MTX services, which provide out-of-the-box handlers for subscribe
/unsubscribe
events, with automatic creation of SAP HANA database containers or SAP Event Mesh instances.
If everything is set up, the following graphic shows what's happening when a user subscribes to a SaaS application:
- The SaaS Provisioning Service sends a
subscribe
event to the CAP application. - The CAP application delegates the request to the MTX services.
- The MTX services use Service Manager to create the database tenant.
- The CAP Application connects to this tenant at runtime using Service Manager.
Prepare for Production
The detailed procedure is described in the Deploy to Cloud Foundry guide.
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples/bookshop
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples/bookshop
To start with this guide, you fulfill all Prerequisites and went through Prepare for Production, or instead run this command to fast-forward:
cds add hana,xsuaa,approuter,mta --for production
cds add hana,xsuaa,approuter,mta --for production
Add MTX
To enable MTX services for your project, run the following command:
cds add mtx --for production
cds add mtx --for production
This modifies the modules and resources in mta.yaml for multitenant deployment.
Build & Deploy
Cloud Foundry Environment
Consult the Deploy to Cloud Foundry guide for an overview of MTA-based deployment.
mbt build -t gen --mtar mta.tar
mbt build -t gen --mtar mta.tar
Learn more about Build & Assemble
cf deploy gen/mta.tar
cf deploy gen/mta.tar
Kyma
Consult the Deploy to Kyma guide for an overview.
To add the Helm Chart to your project, run the following command:
shcds add helm
cds add helm
To Build Approuter Image, run the following command:
shpack build bookshop-approuter \ --path app \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=
pack build bookshop-approuter \ --path app \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=
To Build CAP Node.js Image, run the following command:
shpack build bookshop-srv \ --path gen/srv \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=
pack build bookshop-srv \ --path gen/srv \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=
Follow the steps mentioned in Deploy using CAP Helm Chart and configure your Helm Chart.
Add backend destinations for Approuter using the
backendDestinations
property:yamlbackendDestinations: srv-api: service: srv mtx-api: service: srv
backendDestinations: srv-api: service: srv mtx-api: service: srv
To deploy your Helm Chart, run the following command:
shhelm upgrade --install bookshop ./chart
helm upgrade --install bookshop ./chart
Subscribe
Create a BTP subaccount to subscribe to your deployed application. This subaccount has to be in the same region, for example, us10.
In your subscriber account go to Instances and Subscription and select Create.
Select bookshop and use the only available plan default.
Learn more about subscribing to a SaaS application using the SAP BTP cockpit.Learn more about subscribing to a SaaS application using the btp
CLI.
Go to Application.
As you can see, your route doesn't exist yet. You need to create and map it first.
Leave the window open. You need the information to create the route.
404 Not Found: Requested route ('<prefix>-bookshop.cfapps.eu10.hana.ondemand.com') does not exist.
404 Not Found: Requested route ('<prefix>-bookshop.cfapps.eu10.hana.ondemand.com') does not exist.
Cloud Foundry
Switch to your provider account and go to your space → Routes
Click New Route
Use the broken route to fill the fields. For the following explanations let's use this route as example: https://my-account.cfapps.acme.com
As Domain, use the part that usually starts with cfapps... and ends with .com
In our example, the domain is: cfapps.acme.com
As Host Name, use the part between https:// and .cfapps
In our example, host name is: my-account
Click Save
You can now see the route is created but not mapped yet (none).
Click Map Route
Select bookshop, which is the App Router application we just deployed, and save.
Kyma
Use the broken route to fill the fields. For the following explanations let's use this route as example: https://my-account.c-abcdef.kyma.ondemand.com
As Host, use the part between https:// and c-abcdef
In our example, host is: my-account
Create an API Rule using the following code:
apiVersion: gateway.kyma-project.io/v1beta1
kind: APIRule
metadata:
name: <your-api-rule-name>
spec:
gateway: kyma-gateway.kyma-system.svc.cluster.local
host: my_account
service:
name: <your-release-name>-approuter
port: 8080
rules:
- accessStrategies:
- handler: allow
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
path: /.*
apiVersion: gateway.kyma-project.io/v1beta1
kind: APIRule
metadata:
name: <your-api-rule-name>
spec:
gateway: kyma-gateway.kyma-system.svc.cluster.local
host: my_account
service:
name: <your-release-name>-approuter
port: 8080
rules:
- accessStrategies:
- handler: allow
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
path: /.*
You've successfully created the route for your subscribed bookshop multitenant application. Refresh the browser tab of the bookshop application and log in.
Behind the Scenes
With adding the MTX services, your project configuration is adapted at all relevant places. Configuration and dependencies are added to your package.json and an xs-security.json
containing MTX-specific scopes and roles is created.
For the MTA deployment service dependencies are added to the mta.yaml file. Each SaaS application will have bindings to at least three SAP BTP service instances.
Service | Description |
---|---|
Service Manager (service-manager ) | CAP uses this service for creating a new SAP HANA Deployment Infrastructure (HDI) container for each tenant and for retrieving tenant-specific database connections. |
SaaS Provisioning Service (saas-registry ) | To make a SaaS application available for subscription to SaaS consumer tenants, the application provider must register the application in the SAP BTP Cloud Foundry environment through the SaaS Provisioning Service. |
User Account and Authentication Service (xsuaa ) | Binding information contains the OAuth client ID and client credentials. The XSUAA service can be used to validate the JSON Web Token (JWT) from requests and to retrieve the tenant context from the JWT. |
If you're interested, use version control to spot the exact changes.
Appendix — Configuration
SaaS Service Dependencies
If you require SAP reuse services, that need subscriptions, use cds.mtx.dependencies
in your package.json
and pass an array of their xsappname
s.
The SaaS Provisioning service (getDependencies
handler) needs this information.
"mtx": {
"dependencies": ["xsappname-1", "xsappname-2"]
}
"mtx": {
"dependencies": ["xsappname-1", "xsappname-2"]
}
Maven Dependencies
Multitenancy support is available as optional application feature of the CAP Java SDK. It's already included when you use the cds-starter-cloudfoundry
dependency. Otherwise, you can add the following Maven dependency to apply the feature:
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-mt</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-mt</artifactId>
</dependency>
TIP
Having added this dependency to your project, multitenancy support becomes only active when additional services are configured as described in Configuring Required Platform Services and Adding the MTX Sidecar Module.
Local Development and Testing
You can run your application locally with the MTX sidecar and SQLite database in multitenant mode. To do that, add the necessary dependencies and configure MTX sidecar to your project to enable multitenancy and set the following configuration in a new spring profile in your application.yaml
file:
spring:
config.activate.on-profile: local-mtxs
cds:
multi-tenancy:
mtxs:
enabled: true
sidecar:
url: http://localhost:4004
spring:
config.activate.on-profile: local-mtxs
cds:
multi-tenancy:
mtxs:
enabled: true
sidecar:
url: http://localhost:4004
Add mock users with the tenants you want to use.
Add SQLite JDBC driver to your POM:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
The default project template for CAP does not include it by default. H2 database is not supported in this mode.
WARNING
If you use the default-env.json
file to use services from Cloud Foundry for local development, set the property cds.environment.local.defaultEnvPath
to a directory that doesn't contain this file. Then, the SQLite database generated by the local sidecar will be used instead. It's best to do that with the separate Spring profile to keep both options available.
Start the MTX sidecar as described in the Multitenancy guide and keep it running. For example, use command cds watch mtx/sidecar
in the root directory of your project or cds watch
in the directory of the sidecar in a separate terminal. Adjust the default port in the application.yaml
file, if sidecar is running on different port.
Use subscription endpoints of CAP Java or endpoints of the sidecar directly to create tenant databases. Your CAP Java application will pick up SQLite files created by the sidecar.
TIP
If you have configured the location of SQLite files in the MTX sidecar and CAP doesn't pick up the files automatically, use the configuration option cds.multiTenancy.mock.sqliteDirectory
.
Configuring Platform Services
To enable multitenancy on SAP BTP, three services are involved:
- XSUAA (
xsuaa
) - Service Manager (
service-manager
) - SaaS Provisioning service (
saas-registry
)
Only when these services are bound to your application, the multitenancy feature is turned on. You can either create and configure these services manually. See section Developing Multitenant Applications in SAP BTP, Cloud Foundry Environment for more details. The following sections describe how to configure and bind these services by means of an mta.yaml file.
XSUAA
A special configuration of the application's XSUAA service instance is required to enable authorization between the SaaS Provisioning service, CAP Java application, and MTX sidecar.
The service can be configured in the mta.yaml by adding an xsuaa
resource as follows:
resources:
[...]
- name: xsuaa
type: com.sap.xs.uaa
parameters:
service-plan: application
path: ./xs-security.json
config:
xsappname: <appname>
resources:
[...]
- name: xsuaa
type: com.sap.xs.uaa
parameters:
service-plan: application
path: ./xs-security.json
config:
xsappname: <appname>
Choose a value for property xsappname
that is unique globally.
Also, you have to create an Application Security Descriptor (xs-security.json) file, which must include the scope mtcallback
. The mtcallback
scope is required by the onboarding process.
You can also use a custom scope name by configuring it. Use the following application configuration properties:
cds.multitenancy.security.subscription-scope
An example xs-security.json file looks like this:
{
"tenant-mode": "shared",
"scopes": [
{
"name": "$XSAPPNAME.mtcallback",
"description": "Multi Tenancy Callback Access",
"grant-as-authority-to-apps": [
"$XSAPPNAME(application, sap-provisioning, tenant-onboarding)"
]
},
{
"name": "$XSAPPNAME.cds.ExtensionDeveloper",
"description": "Extend CAP applications via extension projects"
}
],
"role-templates": [
{
"name": "ExtensionDeveloper",
"description": "Extension development",
"scope-references": [
"$XSAPPNAME.cds.ExtensionDeveloper"
]
}
]
}
{
"tenant-mode": "shared",
"scopes": [
{
"name": "$XSAPPNAME.mtcallback",
"description": "Multi Tenancy Callback Access",
"grant-as-authority-to-apps": [
"$XSAPPNAME(application, sap-provisioning, tenant-onboarding)"
]
},
{
"name": "$XSAPPNAME.cds.ExtensionDeveloper",
"description": "Extend CAP applications via extension projects"
}
],
"role-templates": [
{
"name": "ExtensionDeveloper",
"description": "Extension development",
"scope-references": [
"$XSAPPNAME.cds.ExtensionDeveloper"
]
}
]
}
In this example, the grant-as-authority-to-apps
section is used to grant the mtcallback
scope to the applications sap-provisioning
and tenant-onboarding
. These are services provided by SAP BTP involved in the onboarding process. The ExtensionDeveloper
scope and role can be used to grant SaaS Customers the capabilities to extend the application, if the extensibility feature is enabled.
Learn more about extending SaaS applications.
It isn’t necessary to have the security configuration in a separate file. It can also be added to the mta.yaml file directly.
❗ Warning
The mtcallback
scope must not be exposed to any business user, for example, using a role template. Else a malicious user could update or even delete the artifacts of arbitrary tenants.
Service Manager
A service instance of the Service Manager (service-manager
) is required for CAP Java to create and use database containers per tenant at application runtime. It doesn’t require special parameters and can be added as a resource in mta.yaml as follows:
resources:
[...]
- name: service-manager
type: org.cloudfoundry.managed-service
parameters:
service: service-manager
service-plan: container
resources:
[...]
- name: service-manager
type: org.cloudfoundry.managed-service
parameters:
service: service-manager
service-plan: container
SaaS Provisioning Service
A service instance of the SaaS Provisioning service (type saas-registry
) is required to make your application known to the SAP BTP Provisioning Service and to register the endpoints that should be called when tenants are added or removed. The service can be configured as a resource in mta.yaml as follows. See section Register the Multitenant Application to the SaaS Provisioning Service for more details.
resources:
[...]
- name: saas-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: <app display name>
xsappname: <appname>
appUrls:
getDependencies: ~{srv/url}/mt/v1.0/subscriptions/dependencies
onSubscription: ~{srv/url}/mt/v1.0/subscriptions/tenants/{tenantId}
onSubscriptionAsync: true
onUnSubscriptionAsync: true
onUpdateDependenciesAsync: true
callbackTimeoutMillis: 3600000
requires:
- name: srv
resources:
[...]
- name: saas-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: <app display name>
xsappname: <appname>
appUrls:
getDependencies: ~{srv/url}/mt/v1.0/subscriptions/dependencies
onSubscription: ~{srv/url}/mt/v1.0/subscriptions/tenants/{tenantId}
onSubscriptionAsync: true
onUnSubscriptionAsync: true
onUpdateDependenciesAsync: true
callbackTimeoutMillis: 3600000
requires:
- name: srv
It’s required to configure the parameters:
appName
: Choose an appropriate application display name.xsappname
: Use the value forxsappname
you configured at your UAA service instance.appUrls
: Configure the callback URLs used by the SaaS Provisioning service to get the dependencies of the application and to trigger a subscription. In the above example, the property~{srv/url}
is used, which is provided by thesrv
module. See section Configuring the Java Service for more details. If you use different module and property names for your CAP Java backend module, you have to adapt these properties here accordingly.
TIP
Make sure to choose the asynchronous subscription (onSubscriptionAsync: true
) and unsubscription mode (onUnSubscriptionAsync: true
). Always keep callbackTimeoutMillis
in sync with cds.multitenancy.provisioningservice.pollingTimeout
(default: 10min).
Adding the MTX Sidecar
This section describes how to use the cds-mtxs
Node.js module and add the MTX sidecar microservice to the mta.yaml file.
To define the dependencies and start command, create a subfolder named mtx/sidecar
, and within that folder create a file named package.json :
{
"cds": {
"requires": {
"[production]": {
"db": "hana-mt",
"auth": "xsuaa"
},
"[development]": {
"db": "sqlite",
"auth": "dummy"
},
"cds.xt.ModelProviderService": "in-sidecar",
"cds.xt.DeploymentService": true,
"cds.xt.SaasProvisioningService": true,
"cds.xt.ExtensibilityService": true
}
},
"devDependencies": {
"sqlite3": "^5"
},
"engines": {
"node": "^18"
},
"scripts": {
"start": "cds run",
"build": "npm install && cds build ../.. --for mtx-sidecar && cd gen && npm install"
}
}
{
"cds": {
"requires": {
"[production]": {
"db": "hana-mt",
"auth": "xsuaa"
},
"[development]": {
"db": "sqlite",
"auth": "dummy"
},
"cds.xt.ModelProviderService": "in-sidecar",
"cds.xt.DeploymentService": true,
"cds.xt.SaasProvisioningService": true,
"cds.xt.ExtensibilityService": true
}
},
"devDependencies": {
"sqlite3": "^5"
},
"engines": {
"node": "^18"
},
"scripts": {
"start": "cds run",
"build": "npm install && cds build ../.. --for mtx-sidecar && cd gen && npm install"
}
}
Note that it's required to activate the ModelProviderService
in the sidecar application so that tenant-specific CDS models are served at runtime. In contrast, ExtensibilityService
is only required for projects that allow custom extensions at runtime.
Activate multitenancy in the main application's .cdsrc.json. If required, activate extensibility and/or feature toggles as well:
{
"requires": {
...
"multitenancy": true,
"extensibility": true,
"toggles": true,
...
}
}
{
"requires": {
...
"multitenancy": true,
"extensibility": true,
"toggles": true,
...
}
}
Learn more about MTX services and their configuration.
Next, add the required dependencies by calling the below command in the mtx/sidecar
directory:
npm add @sap/cds @sap/cds-mtxs @sap/xssec hdb express passport
npm add @sap/cds @sap/cds-mtxs @sap/xssec hdb express passport
TIP
The cds-mtxs
module requires @sap/cds
version 6
at least.
Because the MTX sidecar needs to know the base CDS model and features, you additionally need to configure the build task by means of the .cdsrc.json file in the root folder of your project:
{
"requires": {
"multitenancy": true,
"extensibility": true,
"toggles": true
},
"build": {
"target": ".",
"tasks": [
{
"for": "java"
},
{
"for": "mtx-sidecar"
},
{
"for": "hana"
}
]
},
"hana": {
"deploy-format": "hdbtable"
}
}
{
"requires": {
"multitenancy": true,
"extensibility": true,
"toggles": true
},
"build": {
"target": ".",
"tasks": [
{
"for": "java"
},
{
"for": "mtx-sidecar"
},
{
"for": "hana"
}
]
},
"hana": {
"deploy-format": "hdbtable"
}
}
This configuration relates to the default project folder layout with app
, db
, srv
, fts
and mtx/sidecar
. If you have a different layout, please adapt the configuration accordingly.
TIP
Make sure to run cds build
with @sap/cds-dk
version 6
at least.
A detailed description of this configuration file can be found in section Build Configuration.
Now, add the mtx-sidecar
module to your mta.yaml file:
modules:
[...]
- name: mtx-sidecar
type: nodejs
path: mtx/sidecar
build-parameters:
builder: custom
build-result: gen
commands:
- npm run build
parameters:
memory: 256M
disk-quota: 512M
requires:
- name: xsuaa
- name: service-manager
provides:
- name: sidecar
properties:
url: ${default-url}
modules:
[...]
- name: mtx-sidecar
type: nodejs
path: mtx/sidecar
build-parameters:
builder: custom
build-result: gen
commands:
- npm run build
parameters:
memory: 256M
disk-quota: 512M
requires:
- name: xsuaa
- name: service-manager
provides:
- name: sidecar
properties:
url: ${default-url}
The mtx-sidecar
module requires the XSUAA and Service Manager services. Also the sidecar exposes its own URL (note that ${default-url}
is a placeholder for the own URL) which is required to configure the service module. The Cloud MTA Build Tool executes the build
script which is provided in mtx/sidecar/package.json
to build the MTX Sidecar application.
Configuring the Java Service
To make use of the previously mentioned services and the MTX sidecar in your CAP Java multitenant application, you need to enhance the srv
module in the mta.yaml file:
modules:
[...]
- name: srv
type: java
path: srv
parameters:
[...]
requires:
- name: service-manager
- name: xsuaa
- name: saas-registry
- name: sidecar
properties:
CDS_MULTITENANCY_MTXS_ENABLED: true
CDS_MULTITENANCY_SIDECAR_URL: ~{url}
- name: app
properties:
CDS_MULTITENANCY_APPUI_URL: ~{url}
CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "."
provides:
- name: srv
properties:
url: '${default-url}'
modules:
[...]
- name: srv
type: java
path: srv
parameters:
[...]
requires:
- name: service-manager
- name: xsuaa
- name: saas-registry
- name: sidecar
properties:
CDS_MULTITENANCY_MTXS_ENABLED: true
CDS_MULTITENANCY_SIDECAR_URL: ~{url}
- name: app
properties:
CDS_MULTITENANCY_APPUI_URL: ~{url}
CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "."
provides:
- name: srv
properties:
url: '${default-url}'
- The environment variable
CDS_MULTITENANCY_MTXS_ENABLED
sets application propertycds.multitenancy.mtxs.enabled
and is used to activate streamlined MTX (default is classic MTX). CDS_MULTITENANCY_SIDECAR_URL
sets the application propertycds.multitenancy.sidecar.url
. This URL is required by the CAP Java runtime to connect to the MTX Sidecar application and is derived from the propertyurl
of the mtx-sidecar module.- Similarly,
CDS_MULTITENANCY_APPUI_URL
configures the URL that is shown in the SAP BTP Cockpit. Usually it points to the app providing the UI, which is the moduleapp
in this example.
The tenant application requests are separated by the tenant specific app URLs. The tenant specific URL is made up of:
https://<subaccount subdomain><CDS_MULTITENANCY_APPUI_TENANTSEPARATOR><CDS_MULTITENANCY_APPUI_URL>
https://<subaccount subdomain><CDS_MULTITENANCY_APPUI_TENANTSEPARATOR><CDS_MULTITENANCY_APPUI_URL>
You can also define the environment variable CDS_MULTITENANCY_APPUI_TENANTSEPARATOR
in the extension descriptor. It’s either "."
or "-"
depending on whether you run the app on a live landscape or on Canary. The extension descriptor file could look like this:
_schema-version: "3.1"
extends: my-app
ID: my-app.id
modules:
- name: srv
properties:
CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-"
- name: app
properties:
TENANT_HOST_PATTERN: ^(.*)-${default-uri}
_schema-version: "3.1"
extends: my-app
ID: my-app.id
modules:
- name: srv
properties:
CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-"
- name: app
properties:
TENANT_HOST_PATTERN: ^(.*)-${default-uri}
Deployment to Kyma
Build
Follow the Build Images Guide to build your CAP Java and Approuter Image.
To build your sidecar image, run the following commands:
shcds build --production
cds build --production
shpack build bookshop-sidecar --path mtx/sidecar/gen \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=""
pack build bookshop-sidecar --path mtx/sidecar/gen \ --buildpack gcr.io/paketo-buildpacks/nodejs \ --builder paketobuildpacks/builder:base \ --env BP_NODE_RUN_SCRIPTS=""
Follow the Push Images Guide and push images to your registry.
Deploy
To add the Helm Chart to your project, run the following command:
shcds add helm
cds add helm
Follow the steps mentioned in Deploy using CAP Helm Chart and configure your Helm Chart.
To deploy your Helm Chart, run the following command:
shhelm upgrade --install bookshop ./chart
helm upgrade --install bookshop ./chart
Adding Custom Handlers
The SaaS Provisioning service in SAP BTP sends specific requests to applications when tenants are subscribed or unsubscribed. For these requests, CAP Java internally generates CAP events on the technical service DeploymentService
.
For a general introduction to CAP events, see Event Handlers.
Register event handlers for the following CAP events to add custom logic for requests sent by the SaaS Provisioning service. Each event passes a special type of EventContext
object to the event handler method and provides event-specific information:
Event Name | Event Context | Use Case |
---|---|---|
DEPENDENCIES | DependenciesEventContext | Dependencies |
SUBSCRIBE | SubscribeEventContext | Add a tenant |
UNSUBSCRIBE | UnsubscribeEventContext | Remove a tenant |
You only need to register event handlers to override the default behavior.
Default behaviors:
- A new tenant-specific database container is created through the Service Manager during subscription.
- The tenant-specific database container is deleted during unsubscription.
WARNING
Note that by default a compatibility mode is enabled to ensure compatibility with the old MtSubscriptionService API. If this mode is enabled, the database container is not deleted during unsubscription by default. Refer to Multitenancy (Classic) > Unsubscribe Tenant for more information.
The following sections describe how to register to these events in more detail.
Subscribe Tenant
Subscription events are generated when a new tenant is added. By default, subscription creates a new database container for a newly subscribed tenant. This happens during the @On
phase of the SUBSCRIBE
event. You can add additional @On
handlers to perform additional subscription steps. Note that these @On
handlers should not call setCompleted()
, as the event processing is auto-completed.
The following examples show how to register custom handlers for the SUBSCRIBE
event:
@Before
public void beforeSubscription(SubscribeEventContext context) {
// Activities before tenant database container is created
}
@After
public void afterSubscribe(SubscribeEventContext context) {
// For example, send notification, ...
}
@Before
public void beforeSubscription(SubscribeEventContext context) {
// Activities before tenant database container is created
}
@After
public void afterSubscribe(SubscribeEventContext context) {
// For example, send notification, ...
}
Tenant Unsubscription
By default, the tenant-specific database container is deleted during off-boarding. This happens during the @On
phase of the UNSUBSCRIBE
event. You can add additional @On
handlers to perform additional unsubscription steps. Note that these @On
handlers should not call setCompleted()
, as the event processing is auto-completed.
The following example shows how to add custom logic for the UNSUBSCRIBE
event:
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
// Activities before off-boarding
}
@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
// Notify off-boarding finished
}
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
// Activities before off-boarding
}
@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
// Notify off-boarding finished
}
WARNING
If you are accessing the tenant database container during unsubscription, you need to wrap the access into a dedicated ChangeSetContext or transaction. This ensures that the transaction to the tenant database container is committed, before the container is deleted.
Skipping Deletion of Tenant Containers During Tenant Unsubscription
By default, tenant-specific resources (for example, database containers) are deleted during removal. However, you can register a customer handler to change this behavior. This is required, for example, in case a tenant is subscribed to your application multiple times and only the last unsubscription should remove its resources.
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
if (keepResources(context.getTenant())) {
context.setCompleted(); // avoid @On handler phase
}
}
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
if (keepResources(context.getTenant())) {
context.setCompleted(); // avoid @On handler phase
}
}
Returning Dependencies
The event DEPENDENCIES
fires when the SaaS Provisioning service calls the getDependencies
callback. Hence, if your application consumes any reuse services provided by SAP, you must implement the DEPENDENCIES
event to return the service dependencies of the application. The event must return a list of all of the dependent services' xsappname
values. CAP automatically adds dependencies of services to the list, for which it provides dedicated integrations. This includes AuditLog and Event Mesh.
TIP
The xsappname
of an SAP reuse service that is bound to your application can be found as part of the VCAP_SERVICES
JSON structure under the path VCAP_SERVICES.<service>.credentials.xsappname
.
The following example shows this in more detail:
@Value("${vcap.services.<my-service-instance>.credentials.xsappname}")
private String xsappname;
@On
public void onDependencies(DependenciesEventContext context) {
List<Map<String, Object>> dependencies = new ArrayList<>();
dependencies.add(SaasRegistryDependency.create(xsappname));
context.setResult(dependencies);
}
@Value("${vcap.services.<my-service-instance>.credentials.xsappname}")
private String xsappname;
@On
public void onDependencies(DependenciesEventContext context) {
List<Map<String, Object>> dependencies = new ArrayList<>();
dependencies.add(SaasRegistryDependency.create(xsappname));
context.setResult(dependencies);
}
Returning a Database ID
When you’ve registered exactly one SAP HANA instance in your SAP BTP space, a new tenant-specific database container is created automatically. However, if you’ve registered more than one SAP HANA instances in your SAP BTP space, you have to pass the target database ID for the new database container in a customer handler, as illustrated in the following example:
@Before
public void beforeSubscription(SubscribeEventContext context) {
context.getOptions().put("provisioningParameters",
Collections.singletonMap("database_id", "<database ID>"));
}
@Before
public void beforeSubscription(SubscribeEventContext context) {
context.getOptions().put("provisioningParameters",
Collections.singletonMap("database_id", "<database ID>"));
}
Logging Support
Logging service support gives you the capability to observe properly correlated requests between the different components of your CAP application in Kibana. This is especially useful for multi-tenant aware applications
that use the MTX sidecar
. Just enable the Cloud Foundry application-logs
service for both, the Java service (see details in Observability > Logging) as well as for the MTX sidecar
, to get correlated log messages from these components.
This can easily be seen in Kibana
, which is part of the ELK Stack (Elasticsearch/Logstash/Kibana) on Cloud Foundry and available by default with the application-logs
service:
Subscribe & Unsubscribe
The CAP Java SDK offers main
methods for Subscribe/Unsubscribe in the classes com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe
that can be called from the command line. This way, you can run the tenant subscribe/unsubscribe for the specified tenant. This would trigger also your custom handlers, which is useful for the local testing scenarios.
In order to register all handlers of the application properly during the execution of a tenant operation main
method, the component scan package must be configured. To set the component scan, the property cds.multitenancy.component-scan
must be set to the package name of your application.
The handler registration provides additional information that is used for the tenant subscribe, for example, messaging subscriptions that are created.
WARNING
You can stop the CAP Java back end when you call this method, but the MTX Sidecar application must be running!
The main
method optionally takes tenant ID (string) as the first input argument and tenant options (JSON string) as the second input argument. Alternatively, you can use the environment variables MTCOMMAND_TENANTS
and MTCOMMAND_OPTIONS
instead of arguments. The command-line arguments have higher priority, so you can use them to override the environment variables.
The method returns the following exit codes.
Exit Code | Result |
---|---|
0 | Tenant subscribed/unsubsribed successfully. |
3 | Failed to subscribe/unsubscribe the tenant. Rerun the procedure to make sure the tenant is subscribed/unsubscribed. |
To run this method locally, use the following command where <jar-file>
is the one of your applications:
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher <tenant> [<tenant options>]
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher <tenant> [<tenant options>]
In the SAP BTP Cloud Foundry environment, it can be tricky to construct such a command. The reason is that the JAR file is extracted by the Java buildpack and the place of the Java executable isn’t easy to determine. Also the place differs for different Java versions. Therefore, we recommend adapting the start command that is generated by the buildpack and run the adapted command:
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
If you use Java 8, you need to use the following command:
sed -i 's/org.springframework.boot.loader.JarLauncher/org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && sed -i 's/-Dsun.net.inetaddr.negative.ttl=0/-Dsun.net.inetaddr.negative.ttl=0 -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
sed -i 's/org.springframework.boot.loader.JarLauncher/org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && sed -i 's/-Dsun.net.inetaddr.negative.ttl=0/-Dsun.net.inetaddr.negative.ttl=0 -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
Database Schema Update
When shipping a new application version with an updated CDS model, the database schema for each subscribed tenant needs an update. The database schema update needs to be triggered explicitly.
When the database schema update is triggered, the following CAP events are sent.
Event Name | Event Context |
---|---|
UPGRADE | UpgradeEventContext |
By registering custom handlers for these events, you can add custom logic to influence the deployment and upgrade process of a tenant. By default, the CAP Java SDK notifies the MTX Sidecar to perform any schema upgrade if necessary.
It's often desired to update the whole service in a zero downtime manner. This section doesn't deal with the details about updating a service productively, but describes tool support the CAP Java SDK offers to update tenants.
The following sections describe how to trigger the update for tenants, including the database schema upgrade.
Deploy Main Method
The CAP Java SDK offers a main
method in the class com.sap.cds.framework.spring.utils.Deploy
that can be called from the command line while the CAP Java application is still stopped. This way, you can run the update for all tenants before you start a new version of the Java application. This prevents new application code to access database artifacts that aren't yet deployed.
In order to register all handlers of the application properly during the execution of a tenant operation main
method, the component scan package must be configured. To set the component scan, the property cds.multitenancy.component-scan
must be set to the package name of your application.
The handler registration provides additional information that is used for the tenant upgrade, for example, messaging subscriptions that are created.
WARNING
While the CAP Java backend might be stopped when you call this method, the MTX Sidecar application must be running!
This synchronization can also be automated, for example using Cloud Foundry Tasks on SAP BTP and Module Hooks in your MTA.
The main
method takes an optional list of tenant IDs as input arguments. If tenant IDs are specified, only these tenants are updated. If no input parameters are specified, all tenants are updated. The method waits until all deployments are finished and then prints the result.
The method returns the following exit codes
Exit Code | Result |
---|---|
0 | All tenants updated successfully. |
1 | Failed to update at least one tenant. Re-run the procedure to make sure that all tenants are updated. |
To run this method locally, use the following command where <jar-file>
is the one of your application:
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher [<tenant 1>] ... [<tenant n>]
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher [<tenant 1>] ... [<tenant n>]
In the SAP BTP, Cloud Foundry environment it can be tricky to construct such a command. The reason is, that the JAR file is extracted by the Java Buildpack and the place of the Java executable isn’t easy to determine. Also the place differs for different Java versions. Therefore, we recommend to adapt the start command that is generated by the buildpack and run the adapted command:
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bash
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bash
If you use Java 8, you need to use the following command:
sed -i 's/org.springframework.boot.loader.JarLauncher/org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && sed -i 's/-Dsun.net.inetaddr.negative.ttl=0/-Dsun.net.inetaddr.negative.ttl=0 -Dloader.main=com.sap.cds.framework.spring.utils.Deploy/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bash
sed -i 's/org.springframework.boot.loader.JarLauncher/org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && sed -i 's/-Dsun.net.inetaddr.negative.ttl=0/-Dsun.net.inetaddr.negative.ttl=0 -Dloader.main=com.sap.cds.framework.spring.utils.Deploy/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bash
For local development you can create a launch configuration in your IDE. For example in case of VS Code it looks like this:
{
"type": "java",
"name": "MTX Update tenants",
"request": "launch",
"mainClass": "com.sap.cds.framework.spring.utils.Deploy",
"args": "", // optional: specify the tenants to upgrade, defaults to all
"projectName": "<your project>",
"vmArgs": "-Dspring.profiles.active=local-mtxs" // or any other profile required for MTX
}
{
"type": "java",
"name": "MTX Update tenants",
"request": "launch",
"mainClass": "com.sap.cds.framework.spring.utils.Deploy",
"args": "", // optional: specify the tenants to upgrade, defaults to all
"projectName": "<your project>",
"vmArgs": "-Dspring.profiles.active=local-mtxs" // or any other profile required for MTX
}
MtSubscriptionService API Compatibility
The former MtSubscriptionService
API described in Multitenancy (Classic) has been deprecated, but can still be used in a compatibility mode. This compatibility mode is enabled by default and can be disabled by setting application property cds.multitenancy.compatibility.enabled
to false
.
In this compatibility mode the event handlers as described in Multitenancy (Classic) are still triggered in addition to the event handlers of the new API. Therefore the UNSUBSCRIBE
event no longer deletes the tenant database container by default. Refer to Multitenancy (Classic) > Unsubscribe Tenant for more information.
As part of the compatibility mode the HTTP-based deploy REST API is also available. However it only uses the deprecated MtSubscriptionService
API. It doesn't trigger the UPGRADE
event of the new API.
Once all event handlers have been migrated to the new API and the compatibility mode is no longer required it is recommended to disable the compatibility mode. The compatibility mode will be removed again in the future.
Configuration Properties
A number of multitenancy settings can be configured through application configuration properties. See section Application Configuration for more details. They can be found in the following table. The prefix for multitenancy-related settings is cds.multitenancy
.
Name | Description | Default |
---|---|---|
servlet.path | Path of the subscription and deployment endpoints | /mt/v1.0/subscriptions |
servlet.enabled | Flag to deactivate the endpoints | true |
datasource.pool | Pool to use for the tenant-dependent data source. Possible values are: hikari , tomcat , atomikos | hikari |
datasource.<pool>.<property> | Pool-specific properties. See the tools documentation for HikariCP, Apache Tomcat, and Atomikos for more details. | Default depends on the pool and property |
datasource.combinePools.enabled | Only one data source pool per database is created. | false |
servicemanager.timeout | Timeout for requests to the Service Manager in seconds | 3600 |
security.subscriptionScope | Scope necessary to call the subscription endpoints | mtcallback |
sidecar.url | URL of the MTX sidecar | Not set by default |
app-ui.url | URL of the application UI | Not set by default |
app-ui.tenantSeparator | Separator of the tenant in the URL, for example, "." or "-" | Not set by default |
mtxs | Enables the usage of @sap/cds-mtxs module (MTX streamlined) | false |
provisioning.pollingTimeout | The maximum waiting time for the provisioning operation to finish | 10 minutes |
mock.enabled | Option to deactivate local multitenancy mode. | true |
mock.sqliteDirectory | Relative path to directory where SQLite files for tenants are stored. | Not set by default. Only specify if the MTX sidecar stores database files in a non-default location, as CAP discovers database files in common directories of the project. |
deploy.componentScan | Specifies the package to be included in component scan of Deploy Main Method with custom handlers. | Not set by default |