Deploy as Multitenant SaaS Application
A comprehensive guide on deploying CAP applications as Software as a Service (SaaS) to SAP BTP Cloud Foundry environment or Kyma environment, and how SaaS Customers would subscribe to them.
This guide is available for Node.js and Java. Press v to switch or use the toggle.
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 SAP BTP 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, for example to manage SAP HANA database containers.
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.
In CAP Java, tenant provisioning is delegated to CAP Node.js based services. This has the following implications:
- Java applications need to run and maintain the cds-mtxs module as a sidecar application (called MTX sidecar in this documentation). The following sections describe the setup as a Multitarget Application using a Multitarget Application Development Descriptor (mta.yaml) file. It can be packaged by means of the Cloud MTA Build Tool and deployed to the SAP BTP by means of the Deploy Service.
- Multitenant CAP Java applications automatically expose the tenant provisioning API called by the SaaS Provisioning service so that custom logic during tenant provisioning can be written in Java.
How to setup CAP Java with multitenancy support is described in the following sections.
Prepare for Production
The detailed procedure is described in the Deploy to Cloud Foundry guide.
cds init bookshop --add sample
cd bookshop
cds init bookshop --add sample
cd bookshop
cds init bookshop --add java,tiny-sample
cd bookshop
cds init bookshop --add java,tiny-sample
cd bookshop
To start with this guide, ensure you fulfill all Prerequisites before going through Prepare for Production. To fast-forward instead, you may simply run this command:
cds add hana,xsuaa,approuter --for production
cds add hana,xsuaa,approuter --for production
Add Multitenancy
To enable multitenancy for your project, run the following command:
cds add multitenancy --for production
cds add multitenancy --for production
This modifies the modules and resources in mta.yaml for multitenant deployment.
Learn more about MTX services.
Build & Deploy
Cloud Foundry
Deployments are done using an mta.yaml deployment descriptor, created as follows:
cds add mta
cds add mta
Consult the Deploy to Cloud Foundry guide for details on MTA-based deployment.
Now, you can freeze dependencies, build and assemble your project, and finally deploy it to Cloud Foundry.
npm update --package-lock-only
npm update --package-lock-only --prefix mtx/sidecar
npm update --package-lock-only
npm update --package-lock-only --prefix mtx/sidecar
Learn more about Freezing Dependencies.
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
Learn more about Deploy to Cloud Foundry.
Kyma
- Add a Helm Chart to your project as follows:
cds add helm
cds add helm
Consult the Deploy to Kyma guide for details on Kyma-based deployment.
- Now, build an approuter image like this:
pack 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=
- Build an MTX sidecar image like this:
pack build bookshop-mtx \
--path gen/mtx/sidecar \
--buildpack gcr.io/paketo-buildpacks/nodejs \
--builder paketobuildpacks/builder:base \
--env BP_NODE_RUN_SCRIPTS=
pack build bookshop-mtx \
--path gen/mtx/sidecar \
--buildpack gcr.io/paketo-buildpacks/nodejs \
--builder paketobuildpacks/builder:base \
--env BP_NODE_RUN_SCRIPTS=
- Build a CAP Node.js image like this:
pack 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=
Learn more about Building & Pushing Images
Follow the steps in Deploy using CAP Helm Chart and configure your Helm Chart.
Add destinations for the approuter using the
backendDestinations
property:
backendDestinations:
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:
helm upgrade --install bookshop ./chart
helm upgrade --install bookshop ./chart
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
Subscribe
Create a BTP subaccount to subscribe to your deployed application. This subaccount has to be in the same region as the provider subaccount, for example, us10
.
See the list of all available regions.
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.
You can now access your subscribed application via Go to Application.
As you can see, your route doesn't exist yet. You need to create and map it first.
404 Not Found: Requested route ('...') does not exist.
404 Not Found: Requested route ('...') does not exist.
Leave the window open. You need the information to create the route.
Cloud Foundry
Use the following command to create and map a route to your application:
cf map-route ‹app› ‹paasDomain› --hostname ‹subscriberSubdomain›-‹saasAppName›
cf map-route ‹app› ‹paasDomain› --hostname ‹subscriberSubdomain›-‹saasAppName›
In our example, let's assume our saas-registry
is configured in the mta.yaml like this:
- name: bookshop-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: bookshop-${org}-${space}
- name: bookshop-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: bookshop-${org}-${space}
Let's also assume we've deployed to our app to Cloud Foundry org myOrg
and space mySpace
. This would be the full command to create a route for the subaccount with subdomain subscriber1
:
cf map-route bookshop cfapps.us10.hana.ondemand.com --hostname subscriber1-myOrg-mySpace-bookshop
cf map-route bookshop cfapps.us10.hana.ondemand.com --hostname subscriber1-myOrg-mySpace-bookshop
Learn how to do this in the BTP cockpit instead…
Switch to your provider account and go to your space → Routes. Click on New Route.
Here, you need to enter a Domain and Host Name.
Let's use this route as example:
https://subscriber1-bookshop.cfapps.us10.hana.ondemand.com
- The Domain here is cfapps.us10.hana.ondemand.com
- The Host Name here is subscriber1-bookshop
Hit Save to create the route.
You can now see the route is created but not mapped to an application yet.
Click on Map Route, choose your approuter module and hit Save.
You should now see the route mapped to your application.
Kyma
Let's use this route as an example: https://my-account.c-abcdef.kyma.ondemand.com
- The Host here 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.
Optional: SaaS Service Dependencies
If you require SAP reuse services that need to be subscribed, you can use the following configuration in your package.json to pass them to the getDependencies
handler of the SAP BTP SaaS Provisioning service:
"cds": {
"requires": {
"cds.xt.SaasProvisioningService": {
"dependencies": ["xsappname-1", "xsappname-2"]
}
}
}
"cds": {
"requires": {
"cds.xt.SaasProvisioningService": {
"dependencies": ["xsappname-1", "xsappname-2"]
}
}
}
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 offboarding. 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 offboarding
}
@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
// Notify offboarding finished
}
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
// Activities before offboarding
}
@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
// Notify offboarding 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.
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>]
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
}
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/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
If you use Java 8, you need to use the following 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
To run the command manually or automated, you can for example use Cloud Foundry Tasks on SAP BTP and Module Hooks in your MTA. To trigger it as part of a Cloud Foundry Task, login to the Cloud Foundry landscape using the Cloud Foundry command line client and execute:
cf run-task <application_name> "<command>"
cf run-task <application_name> "<command>"
<application_name>
needs to be replaced with the name of a Cloud Foundry application, typically the srv module of your CAP project. You can find the name for example in the section modules
in your mta.yaml
. <command>
represents the adapted start command. The output of the command will be logged to the application logs of the application you have specified in <application_name>
.
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.