Multitenancy in ABAP Cloud: SaaS-Anwendungen entwickeln

kategorie
ABAP Cloud
Veröffentlicht
autor
Johannes

Multitenancy ermöglicht es, eine ABAP Cloud-Anwendung für mehrere Kunden (Tenants) zu betreiben, ohne den Code mehrfach zu deployen. Für SaaS-Anbieter ist dies das Fundament für skalierbare und kosteneffiziente Lösungen.

Was ist Multitenancy?

In einer mandantenfähigen Architektur teilen sich mehrere Kunden dieselbe Anwendungsinstanz, während ihre Daten strikt voneinander getrennt bleiben:

BegriffBeschreibung
TenantEin Kunde oder Mandant mit eigenen Daten
Provider AccountSAP BTP-Account des SaaS-Anbieters
Consumer SubaccountBTP-Subaccount eines SaaS-Kunden
Tenant IDEindeutiger Identifier für jeden Mandanten
Tenant-awareCode, der den aktuellen Tenant berücksichtigt

Multitenancy-Modelle auf SAP BTP

ModellBeschreibungAnwendungsfall
Shared SchemaAlle Tenants in einer Datenbank-InstanzStandard für ABAP Cloud
Separate SchemaEigenes DB-Schema pro TenantStrenge Isolation
Separate InstanceEigene ABAP-Instanz pro TenantMaximale Isolation

ABAP Cloud nutzt primär das Shared Schema-Modell mit mandantenabhängigen Tabellen. Die Trennung erfolgt über das Feld CLIENT (Mandant).

Architektur einer Multitenant-SaaS-Anwendung

┌─────────────────────────────────────────────────────────────────────────────┐
│ SAP BTP Global Account │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Provider Subaccount │ │
│ │ ┌────────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ ABAP Cloud System │ │ SaaS Provisioning Service │ │ │
│ │ │ (Multitenant) │ │ - Tenant Onboarding │ │ │
│ │ │ │ │ - Tenant Offboarding │ │ │
│ │ │ - Shared Code │ │ - Lifecycle Callbacks │ │ │
│ │ │ - Shared Tables │ └─────────────────────────────────────────┘ │ │
│ │ │ - Mandant pro │ │ │
│ │ │ Tenant │ ┌─────────────────────────────────────────┐ │ │
│ │ └────────────────────┘ │ XSUAA Service │ │ │
│ │ │ - Tenant-spezifische JWT-Tokens │ │ │
│ └────────────────────────────┴─────────────────────────────────────────┘─┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Consumer │ │ Consumer │ │ Consumer │ │
│ │ Subaccount A │ │ Subaccount B │ │ Subaccount C │ │
│ │ (Kunde Alpha) │ │ (Kunde Beta) │ │ (Kunde Gamma) │ │
│ │ │ │ │ │ │ │
│ │ Mandant: 100 │ │ Mandant: 200 │ │ Mandant: 300 │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

Kernkomponenten

KomponenteFunktion
ABAP SystemMultitenant-fähiges ABAP Environment
SaaS Provisioning ServiceVerwaltet Tenant-Lifecycle
XSUAAAuthentifizierung und Autorisierung pro Tenant
Destination ServiceTenant-spezifische Verbindungskonfigurationen

Tenant Context in ABAP Cloud

Der aktuelle Tenant-Kontext ist über verschiedene APIs verfügbar:

Tenant-ID auslesen

CLASS zcl_tenant_utils DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
CLASS-METHODS:
get_current_tenant_id
RETURNING VALUE(rv_tenant_id) TYPE string,
get_current_tenant_zone_id
RETURNING VALUE(rv_zone_id) TYPE string,
is_provider_tenant
RETURNING VALUE(rv_is_provider) TYPE abap_bool.
ENDCLASS.
CLASS zcl_tenant_utils IMPLEMENTATION.
METHOD get_current_tenant_id.
" Tenant-ID aus dem Security Context
DATA(lo_context) = cl_abap_context_info=>get_system_context( ).
rv_tenant_id = lo_context->get_attribute( 'zoneId' ).
ENDMETHOD.
METHOD get_current_tenant_zone_id.
" Zone-ID entspricht der Subaccount-GUID
TRY.
DATA(lo_context) = cl_abap_context_info=>get_system_context( ).
rv_zone_id = lo_context->get_attribute( 'zoneId' ).
CATCH cx_abap_context_info_error.
" Fallback: leerer String
rv_zone_id = ''.
ENDTRY.
ENDMETHOD.
METHOD is_provider_tenant.
" Prüfen ob aktueller Tenant der Provider ist
DATA(lv_zone_id) = get_current_tenant_zone_id( ).
DATA(lv_provider_zone) = cl_abap_context_info=>get_system_context(
)->get_attribute( 'providerZoneId' ).
rv_is_provider = xsdbool( lv_zone_id = lv_provider_zone ).
ENDMETHOD.
ENDCLASS.

Mandant-abhängige Daten

In ABAP Cloud werden Tenant-Daten über den Mandanten (CLIENT) getrennt. Jeder Tenant erhält einen eigenen Mandanten:

CLASS zcl_tenant_data DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
get_tenant_specific_config
RETURNING VALUE(rs_config) TYPE zconfig_s.
ENDCLASS.
CLASS zcl_tenant_data IMPLEMENTATION.
METHOD get_tenant_specific_config.
" Standard-Select liest automatisch mandantenabhängig
" Der Mandant wird vom System basierend auf dem JWT-Token gesetzt
SELECT SINGLE *
FROM ztenant_config
INTO rs_config.
" Keine WHERE-Klausel für CLIENT nötig - das passiert automatisch
ENDMETHOD.
ENDCLASS.

Tenant-aware CDS Views

CDS Views sind automatisch mandantenabhängig, wenn die zugrundeliegende Datenbanktabelle mandantenabhängig ist:

Standard Mandanten-Verhalten

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Tenant-spezifische Produkte'
define view entity ZI_Product
as select from zproduct
{
key product_id as ProductId,
product_name as ProductName,
product_price as ProductPrice,
created_by as CreatedBy,
created_at as CreatedAt
}

Die WHERE-Klausel WHERE mandt = sy-mandt wird vom System automatisch hinzugefügt. Jeder Tenant sieht nur seine eigenen Daten.

Cross-Client Zugriff für Provider

In bestimmten Fällen muss der Provider auf Daten aller Tenants zugreifen (z.B. für Reporting oder Administration):

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Alle Produkte (Cross-Client)'
@ClientHandling.type: #CLIENT_INDEPENDENT
define view entity ZI_ProductCrossClient
as select from zproduct
{
key mandt as Client,
key product_id as ProductId,
product_name as ProductName,
tenant_id as TenantId
}

Wichtig: Cross-Client Views erfordern besondere Berechtigungsprüfungen, um Datenlecks zu vermeiden.

Berechtigungsprüfung für Cross-Client Views

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Provider-Reporting View'
define view entity ZI_ProviderReporting
as select from zproduct
association [1..1] to ZI_TenantMaster as _Tenant
on $projection.TenantId = _Tenant.TenantId
{
key mandt as Client,
key product_id as ProductId,
product_name as ProductName,
tenant_id as TenantId,
_Tenant
}
where
-- Nur für Provider-Tenant sichtbar
$session.system_date is not null

Mit Access Control:

@EndUserText.label: 'Provider Reporting DCL'
@MappingRole: true
define role ZR_ProviderReporting {
grant select on ZI_ProviderReporting
where ( ) = aspect pfcg_auth ( Z_PROVIDER, ACTVT = '03' );
}

Lifecycle Management

Das Tenant-Lifecycle-Management umfasst Onboarding (neue Kunden aufnehmen) und Offboarding (Kunden entfernen).

SaaS Provisioning Service

Der SaaS Provisioning Service ruft bei Tenant-Ereignissen Callbacks auf:

┌─────────────────────────────────────────────────────────────────┐
│ Tenant Lifecycle │
│ │
│ ┌──────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Subscribe│───>│ SaaS │───>│ ABAP Cloud │ │
│ │ (Kunde) │ │ Provisioning│ │ Onboarding Callback │ │
│ └──────────┘ └─────────────┘ │ - Mandant anlegen │ │
│ │ - Initiale Daten laden │ │
│ │ - Berechtigungen setup │ │
│ └─────────────────────────┘ │
│ │
│ ┌────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Unsubscribe│─>│ SaaS │───>│ ABAP Cloud │ │
│ │ (Kunde) │ │ Provisioning│ │ Offboarding Callback │ │
│ └────────────┘ └─────────────┘ │ - Daten archivieren │ │
│ │ - Mandant deaktivieren │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Onboarding Callback implementieren

CLASS zcl_tenant_provisioning DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_saas_provisioning_callback.
ENDCLASS.
CLASS zcl_tenant_provisioning IMPLEMENTATION.
METHOD if_saas_provisioning_callback~on_subscription.
" Wird aufgerufen, wenn ein neuer Tenant abonniert
DATA(lv_tenant_id) = iv_tenant_id.
DATA(lv_subdomain) = iv_subdomain.
TRY.
" 1. Initiale Konfiguration für den Tenant anlegen
create_tenant_config(
iv_tenant_id = lv_tenant_id
iv_subdomain = lv_subdomain
).
" 2. Standard-Stammdaten laden
load_initial_master_data( lv_tenant_id ).
" 3. Admin-Benutzer einrichten
setup_tenant_admin(
iv_tenant_id = lv_tenant_id
iv_admin_email = is_subscription_params-admin_email
).
" Erfolg melden
ev_status = if_saas_provisioning_callback=>cs_status-success.
CATCH cx_root INTO DATA(lx_error).
" Fehler beim Onboarding
ev_status = if_saas_provisioning_callback=>cs_status-failed.
ev_message = lx_error->get_text( ).
ENDTRY.
ENDMETHOD.
METHOD if_saas_provisioning_callback~on_unsubscription.
" Wird aufgerufen, wenn ein Tenant kündigt
DATA(lv_tenant_id) = iv_tenant_id.
TRY.
" 1. Daten für gesetzliche Aufbewahrung archivieren
archive_tenant_data( lv_tenant_id ).
" 2. Aktive Sessions beenden
terminate_tenant_sessions( lv_tenant_id ).
" 3. Tenant-Konfiguration deaktivieren
deactivate_tenant( lv_tenant_id ).
ev_status = if_saas_provisioning_callback=>cs_status-success.
CATCH cx_root INTO DATA(lx_error).
ev_status = if_saas_provisioning_callback=>cs_status-failed.
ev_message = lx_error->get_text( ).
ENDTRY.
ENDMETHOD.
METHOD if_saas_provisioning_callback~get_dependencies.
" Abhängigkeiten zu anderen SaaS-Services deklarieren
" z.B. wenn dieser Service einen anderen SaaS-Service benötigt
rt_dependencies = VALUE #( ).
ENDMETHOD.
ENDCLASS.

Tenant-spezifische Initialdaten

CLASS zcl_tenant_initializer DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
create
IMPORTING iv_tenant_id TYPE string
RETURNING VALUE(ro_instance) TYPE REF TO zcl_tenant_initializer.
METHODS:
initialize_master_data,
initialize_customizing,
initialize_number_ranges.
PRIVATE SECTION.
DATA mv_tenant_id TYPE string.
ENDCLASS.
CLASS zcl_tenant_initializer IMPLEMENTATION.
METHOD create.
ro_instance = NEW #( ).
ro_instance->mv_tenant_id = iv_tenant_id.
ENDMETHOD.
METHOD initialize_master_data.
" Standard-Stammdaten für jeden neuen Tenant
DATA lt_default_categories TYPE STANDARD TABLE OF zcategory.
lt_default_categories = VALUE #(
( category_id = 'CAT01' name = 'Standard' description = 'Standardkategorie' )
( category_id = 'CAT02' name = 'Premium' description = 'Premiumkategorie' )
).
INSERT zcategory FROM TABLE lt_default_categories.
ENDMETHOD.
METHOD initialize_customizing.
" Tenant-spezifisches Customizing
DATA(ls_config) = VALUE zconfig_s(
tenant_id = mv_tenant_id
currency = 'EUR'
language = 'DE'
date_format = 'DD.MM.YYYY'
time_zone = 'CET'
).
INSERT ztenant_config FROM ls_config.
ENDMETHOD.
METHOD initialize_number_ranges.
" Nummernkreise pro Tenant initialisieren
" In ABAP Cloud über Nummernkreis-Objekte
DATA(lo_nr_runtime) = cl_numberrange_runtime=>create(
iv_object = 'ZORDER'
).
" Intervall für den Tenant anlegen
lo_nr_runtime->create_interval(
is_interval = VALUE #(
nrrangenr = '01'
fromnumber = '0000000001'
tonumber = '9999999999'
procind = 'I' " Interner Nummernkreis
)
).
ENDMETHOD.
ENDCLASS.

Tenant-spezifische Berechtigungen

Jeder Tenant hat eigene Berechtigungsrollen:

IAM Konzept für Multitenancy

┌───────────────────────────────────────────────────────────────────┐
│ Berechtigungsarchitektur │
│ │
│ Provider: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ IAM Business Catalog (Provider) │ │
│ │ - Z_PROVIDER_ADMIN: Alle Tenants verwalten │ │
│ │ - Z_PROVIDER_SUPPORT: Tenant-Support │ │
│ │ - Z_PROVIDER_BILLING: Abrechnungszugriff │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Consumer (pro Tenant): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ IAM Business Catalog (Tenant) │ │
│ │ - Z_TENANT_ADMIN: Eigenen Tenant verwalten │ │
│ │ - Z_TENANT_USER: Standardnutzer │ │
│ │ - Z_TENANT_VIEWER: Nur Lesezugriff │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘

Tenant-Isolation in RAP

CLASS lhc_product DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS:
authorize_create FOR VALIDATE ON SAVE
IMPORTING keys FOR Product~authorizeCreate,
check_tenant_access FOR VALIDATE ON SAVE
IMPORTING keys FOR Product~checkTenantAccess.
ENDCLASS.
CLASS lhc_product IMPLEMENTATION.
METHOD authorize_create.
" Standard-Berechtigungsprüfung
READ ENTITIES OF ZI_Product IN LOCAL MODE
ENTITY Product
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_products).
LOOP AT lt_products INTO DATA(ls_product).
" Berechtigungsobjekt für Create prüfen
AUTHORITY-CHECK OBJECT 'Z_PRODUCT'
ID 'ACTVT' FIELD '01'.
IF sy-subrc <> 0.
APPEND VALUE #(
%tky = ls_product-%tky
%msg = NEW zcx_product_auth( severity = if_abap_behv_message=>severity-error )
) TO failed-product.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD check_tenant_access.
" Zusätzliche Tenant-Prüfung für Cross-Tenant-Szenarien
" Nur relevant wenn Provider auf Tenant-Daten zugreift
DATA(lv_current_tenant) = zcl_tenant_utils=>get_current_tenant_id( ).
DATA(lv_is_provider) = zcl_tenant_utils=>is_provider_tenant( ).
READ ENTITIES OF ZI_Product IN LOCAL MODE
ENTITY Product
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_products).
LOOP AT lt_products INTO DATA(ls_product).
" Provider darf alle Daten sehen
IF lv_is_provider = abap_true.
CONTINUE.
ENDIF.
" Consumer darf nur eigene Tenant-Daten ändern
IF ls_product-TenantId <> lv_current_tenant.
APPEND VALUE #(
%tky = ls_product-%tky
%msg = NEW zcx_tenant_violation( )
) TO failed-product.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Tenant-spezifische Konfiguration

Customizing pro Tenant

CLASS zcl_tenant_customizing DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
get_config
RETURNING VALUE(rs_config) TYPE zconfig_s,
get_feature_flags
RETURNING VALUE(rt_flags) TYPE zfeature_flags_t,
is_feature_enabled
IMPORTING iv_feature TYPE string
RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION.
DATA ms_config TYPE zconfig_s.
DATA mt_feature_flags TYPE zfeature_flags_t.
DATA mv_loaded TYPE abap_bool.
METHODS load_config.
ENDCLASS.
CLASS zcl_tenant_customizing IMPLEMENTATION.
METHOD get_config.
IF mv_loaded = abap_false.
load_config( ).
ENDIF.
rs_config = ms_config.
ENDMETHOD.
METHOD load_config.
" Mandantenabhängige Konfiguration laden
SELECT SINGLE *
FROM ztenant_config
INTO @ms_config.
" Feature Flags laden
SELECT *
FROM zfeature_flags
INTO TABLE @mt_feature_flags.
mv_loaded = abap_true.
ENDMETHOD.
METHOD get_feature_flags.
IF mv_loaded = abap_false.
load_config( ).
ENDIF.
rt_flags = mt_feature_flags.
ENDMETHOD.
METHOD is_feature_enabled.
DATA(lt_flags) = get_feature_flags( ).
READ TABLE lt_flags INTO DATA(ls_flag)
WITH KEY feature = iv_feature.
rv_enabled = xsdbool( sy-subrc = 0 AND ls_flag-enabled = abap_true ).
ENDMETHOD.
ENDCLASS.

Tenant-spezifische Destinations

Jeder Tenant kann eigene externe Verbindungen konfigurieren:

CLASS zcl_tenant_destination DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
get_http_destination
IMPORTING iv_destination_name TYPE string
RETURNING VALUE(ro_destination) TYPE REF TO if_http_destination
RAISING cx_http_dest_provider_error.
ENDCLASS.
CLASS zcl_tenant_destination IMPLEMENTATION.
METHOD get_http_destination.
" Tenant-spezifische Destination aus dem Destination Service
" Der Destination Service liefert automatisch die Destination
" für den aktuellen Tenant-Kontext
ro_destination = cl_http_destination_provider=>create_by_cloud_destination(
i_name = iv_destination_name
i_service_instance_name = 'destination-service'
i_authn_mode = if_a4c_cp_service=>service_specific
).
ENDMETHOD.
ENDCLASS.

Testing in Multitenancy-Szenarien

Unit Tests mit Tenant-Mocking

CLASS ltcl_tenant_aware DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA:
environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
test_tenant_isolation FOR TESTING,
test_cross_tenant_access FOR TESTING.
ENDCLASS.
CLASS ltcl_tenant_aware IMPLEMENTATION.
METHOD class_setup.
" Test-Environment für CDS Views erstellen
environment = cl_cds_test_environment=>create(
i_for_entity = 'ZI_PRODUCT'
).
ENDMETHOD.
METHOD class_teardown.
environment->destroy( ).
ENDMETHOD.
METHOD test_tenant_isolation.
" Testdaten für verschiedene Mandanten
DATA lt_test_data TYPE STANDARD TABLE OF zproduct.
lt_test_data = VALUE #(
( mandt = '100' product_id = 'P001' product_name = 'Tenant A Product' )
( mandt = '200' product_id = 'P002' product_name = 'Tenant B Product' )
).
environment->insert_test_data( lt_test_data ).
" Test: Tenant A sieht nur seine Produkte
" Der aktuelle Mandant wird vom Testframework auf 100 gesetzt
SELECT COUNT(*)
FROM zi_product
INTO @DATA(lv_count).
cl_abap_unit_assert=>assert_equals(
act = lv_count
exp = 1
msg = 'Tenant sollte nur eigene Produkte sehen'
).
ENDMETHOD.
METHOD test_cross_tenant_access.
" Test für Provider-Cross-Tenant-Zugriff
" Benötigt spezielle Berechtigung Z_PROVIDER
" ...
ENDMETHOD.
ENDCLASS.

Best Practices

Do’s

EmpfehlungBegründung
Mandantenabhängige Tabellen nutzenAutomatische Isolation durch das System
Tenant-ID in Logs aufnehmenFür Debugging und Audit-Trail
Feature Flags pro TenantIndividuelle Feature-Rollouts
Separate NummernkreiseKeine Kollisionen zwischen Tenants
Tenant-spezifische DestinationsJeder Kunde hat eigene Backends

Don’ts

VermeidenRisiko
Hardcodierte MandantenFunktioniert nicht in Multitenancy
Cross-Client ohne BerechtigungDatenleck zwischen Tenants
Globale Variablen für Tenant-DatenRace Conditions
Tenant-ID aus URL parsenSicherheitslücke
Synchrone Tenant-OperationenPerformance-Probleme

Vergleich: Single-Tenant vs. Multi-Tenant

AspektSingle-TenantMulti-Tenant
IsolationPhysisch getrenntLogisch getrennt
KostenHöher (pro Instanz)Niedriger (geteilt)
SkalierungPro TenantÜber alle Tenants
CustomizingUnbegrenztIm Rahmen der Plattform
UpdatesIndividuellFür alle gleichzeitig
ComplianceEinfacherKomplexer

Weiterführende Themen

Zusammenfassung

Multitenancy in ABAP Cloud ermöglicht skalierbare SaaS-Anwendungen:

  1. Shared Schema: Ein ABAP-System für alle Tenants, Trennung über Mandanten
  2. Automatische Isolation: CDS Views und Datenbankzugriffe sind standardmäßig mandantenabhängig
  3. Lifecycle Management: SaaS Provisioning Service für Onboarding und Offboarding
  4. Tenant Context: cl_abap_context_info für Zugriff auf Tenant-Informationen
  5. Berechtigungen: IAM Business Catalogs für Provider- und Consumer-Rollen

Die Kombination aus SAP BTP-Services (XSUAA, Destination Service, SaaS Provisioning) und ABAP Cloud bietet eine solide Grundlage für mandantenfähige Geschäftsanwendungen.