Multitenancy dans ABAP Cloud : Developper des applications SaaS

Catégorie
ABAP Cloud
Publié
Auteur
Johannes

Le Multitenancy permet d’exploiter une application ABAP Cloud pour plusieurs clients (tenants) sans deployer le code plusieurs fois. Pour les fournisseurs SaaS, c’est la base de solutions evolutives et rentables.

Qu’est-ce que le Multitenancy ?

Dans une architecture multi-locataires, plusieurs clients partagent la meme instance d’application, tandis que leurs donnees restent strictement separees :

TermeDescription
TenantUn client ou locataire avec ses propres donnees
Provider AccountCompte SAP BTP du fournisseur SaaS
Consumer SubaccountSubaccount BTP d’un client SaaS
Tenant IDIdentifiant unique pour chaque locataire
Tenant-awareCode qui prend en compte le locataire actuel

Modeles de Multitenancy sur SAP BTP

ModeleDescriptionCas d’utilisation
Shared SchemaTous les tenants dans une instance de base de donneesStandard pour ABAP Cloud
Separate SchemaSchema DB propre par tenantIsolation stricte
Separate InstanceInstance ABAP propre par tenantIsolation maximale

ABAP Cloud utilise principalement le modele Shared Schema avec des tables dependantes du mandant. La separation se fait via le champ CLIENT (mandant).

Architecture d’une application SaaS multi-locataires

+-----------------------------------------------------------------------------+
| SAP BTP Global Account |
| |
| +------------------------------------------------------------------------+ |
| | Provider Subaccount | |
| | +--------------------+ +-------------------------------------+ | |
| | | Systeme ABAP Cloud | | SaaS Provisioning Service | | |
| | | (Multitenant) | | - Tenant Onboarding | | |
| | | | | - Tenant Offboarding | | |
| | | - Code partage | | - Lifecycle Callbacks | | |
| | | - Tables partagees | +-------------------------------------+ | |
| | | - Mandant par | | |
| | | Tenant | +-------------------------------------+ | |
| | +--------------------+ | Service XSUAA | | |
| | | - Tokens JWT specifiques au tenant | | |
| +----------------------------+-------------------------------------+-----+ |
| |
| +------------------+ +------------------+ +------------------+ |
| | Consumer | | Consumer | | Consumer | |
| | Subaccount A | | Subaccount B | | Subaccount C | |
| | (Client Alpha) | | (Client Beta) | | (Client Gamma) | |
| | | | | | | |
| | Mandant : 100 | | Mandant : 200 | | Mandant : 300 | |
| +------------------+ +------------------+ +------------------+ |
+-----------------------------------------------------------------------------+

Composants principaux

ComposantFonction
Systeme ABAPABAP Environment compatible multi-locataires
SaaS Provisioning ServiceGere le cycle de vie des tenants
XSUAAAuthentification et autorisation par tenant
Destination ServiceConfigurations de connexion specifiques au tenant

Contexte du Tenant dans ABAP Cloud

Le contexte du tenant actuel est disponible via differentes APIs :

Lire le Tenant-ID

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 depuis le 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 correspond au GUID du subaccount
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 : string vide
rv_zone_id = ''.
ENDTRY.
ENDMETHOD.
METHOD is_provider_tenant.
" Verifier si le tenant actuel est le provider
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.

Donnees dependantes du mandant

Dans ABAP Cloud, les donnees des tenants sont separees via le mandant (CLIENT). Chaque tenant recoit son propre mandant :

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.
" Le SELECT standard lit automatiquement dependant du mandant
" Le mandant est defini par le systeme en fonction du token JWT
SELECT SINGLE *
FROM ztenant_config
INTO rs_config.
" Pas de clause WHERE pour CLIENT necessaire - c'est automatique
ENDMETHOD.
ENDCLASS.

CDS Views Tenant-aware

Les CDS Views sont automatiquement dependants du mandant quand la table de base de donnees sous-jacente est dependante du mandant :

Comportement standard du mandant

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Produits specifiques au tenant"
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
}

La clause WHERE WHERE mandt = sy-mandt est automatiquement ajoutee par le systeme. Chaque tenant ne voit que ses propres donnees.

Acces Cross-Client pour le Provider

Dans certains cas, le provider doit acceder aux donnees de tous les tenants (par ex. pour le reporting ou l’administration) :

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Tous les produits (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
}

Important : Les vues Cross-Client necessitent des controles d’autorisation speciaux pour eviter les fuites de donnees.

Gestion du cycle de vie

La gestion du cycle de vie des tenants comprend l’onboarding (accueillir de nouveaux clients) et l’offboarding (retirer des clients).

SaaS Provisioning Service

Le SaaS Provisioning Service appelle des callbacks lors d’evenements tenant :

+----------------------------------------------------------------+
| Cycle de vie du Tenant |
| |
| +----------+ +-------------+ +-------------------------+ |
| | Subscribe|-->| SaaS |-->| ABAP Cloud | |
| | (Client) | | Provisioning| | Callback Onboarding | |
| +----------+ +-------------+ | - Creer le mandant | |
| | - Charger donnees init | |
| | - Setup autorisations | |
| +-------------------------+ |
| |
| +------------+ +-------------+ +-------------------------+ |
| | Unsubscribe|->| SaaS |-->| ABAP Cloud | |
| | (Client) | | Provisioning| | Callback Offboarding | |
| +------------+ +-------------+ | - Archiver les donnees | |
| | - Desactiver mandant | |
| +-------------------------+ |
+----------------------------------------------------------------+

Implementer le Callback Onboarding

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.
" Appele quand un nouveau tenant s'abonne
DATA(lv_tenant_id) = iv_tenant_id.
DATA(lv_subdomain) = iv_subdomain.
TRY.
" 1. Creer la configuration initiale pour le tenant
create_tenant_config(
iv_tenant_id = lv_tenant_id
iv_subdomain = lv_subdomain
).
" 2. Charger les donnees de base standard
load_initial_master_data( lv_tenant_id ).
" 3. Configurer l'utilisateur admin
setup_tenant_admin(
iv_tenant_id = lv_tenant_id
iv_admin_email = is_subscription_params-admin_email
).
" Signaler le succes
ev_status = if_saas_provisioning_callback=>cs_status-success.
CATCH cx_root INTO DATA(lx_error).
" Erreur lors de l'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.
" Appele quand un tenant resilie
DATA(lv_tenant_id) = iv_tenant_id.
TRY.
" 1. Archiver les donnees pour conservation legale
archive_tenant_data( lv_tenant_id ).
" 2. Terminer les sessions actives
terminate_tenant_sessions( lv_tenant_id ).
" 3. Desactiver la configuration du tenant
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.
" Declarer les dependances vers d'autres services SaaS
" par ex. si ce service necessite un autre service SaaS
rt_dependencies = VALUE #( ).
ENDMETHOD.
ENDCLASS.

Donnees initiales specifiques au tenant

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.
" Donnees de base standard pour chaque nouveau tenant
DATA lt_default_categories TYPE STANDARD TABLE OF zcategory.
lt_default_categories = VALUE #(
( category_id = 'CAT01' name = 'Standard' description = 'Categorie standard' )
( category_id = 'CAT02' name = 'Premium' description = 'Categorie premium' )
).
INSERT zcategory FROM TABLE lt_default_categories.
ENDMETHOD.
METHOD initialize_customizing.
" Customizing specifique au tenant
DATA(ls_config) = VALUE zconfig_s(
tenant_id = mv_tenant_id
currency = 'EUR"
language = 'FR"
date_format = 'DD/MM/YYYY"
time_zone = 'CET"
).
INSERT ztenant_config FROM ls_config.
ENDMETHOD.
METHOD initialize_number_ranges.
" Initialiser les tranches de numeros par tenant
" Dans ABAP Cloud via objets de tranches de numeros
DATA(lo_nr_runtime) = cl_numberrange_runtime=>create(
iv_object = 'ZORDER"
).
" Creer l'intervalle pour le tenant
lo_nr_runtime->create_interval(
is_interval = VALUE #(
nrrangenr = '01"
fromnumber = '0000000001"
tonumber = '9999999999"
procind = 'I' " Tranche de numeros interne
)
).
ENDMETHOD.
ENDCLASS.

Autorisations specifiques au tenant

Chaque tenant a ses propres roles d’autorisation :

Concept IAM pour Multitenancy

+-------------------------------------------------------------------+
| Architecture des autorisations |
| |
| Provider : |
| +---------------------------------------------------------+ |
| | IAM Business Catalog (Provider) | |
| | - Z_PROVIDER_ADMIN : Gerer tous les tenants | |
| | - Z_PROVIDER_SUPPORT : Support tenant | |
| | - Z_PROVIDER_BILLING : Acces facturation | |
| +---------------------------------------------------------+ |
| |
| Consumer (par tenant) : |
| +---------------------------------------------------------+ |
| | IAM Business Catalog (Tenant) | |
| | - Z_TENANT_ADMIN : Gerer son propre tenant | |
| | - Z_TENANT_USER : Utilisateur standard | |
| | - Z_TENANT_VIEWER : Acces lecture seule | |
| +---------------------------------------------------------+ |
+-------------------------------------------------------------------+

Isolation du tenant dans 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.
" Controle d'autorisation standard
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).
" Verifier l'objet d'autorisation pour Create
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.
" Verification tenant supplementaire pour les scenarios cross-tenant
" Pertinent seulement quand le provider accede aux donnees tenant
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).
" Le provider peut voir toutes les donnees
IF lv_is_provider = abap_true.
CONTINUE.
ENDIF.
" Le consumer ne peut modifier que les donnees de son propre tenant
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.

Configuration specifique au tenant

Customizing par 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.
" Charger la configuration dependante du mandant
SELECT SINGLE *
FROM ztenant_config
INTO @ms_config.
" Charger les Feature Flags
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.

Tests dans les scenarios Multitenancy

Tests unitaires avec 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.
" Creer l'environnement de test pour les CDS Views
environment = cl_cds_test_environment=>create(
i_for_entity = 'ZI_PRODUCT"
).
ENDMETHOD.
METHOD class_teardown.
environment->destroy( ).
ENDMETHOD.
METHOD test_tenant_isolation.
" Donnees de test pour differents mandants
DATA lt_test_data TYPE STANDARD TABLE OF zproduct.
lt_test_data = VALUE #(
( mandt = '100' product_id = 'P001' product_name = 'Produit Tenant A' )
( mandt = '200' product_id = 'P002' product_name = 'Produit Tenant B' )
).
environment->insert_test_data( lt_test_data ).
" Test : Tenant A ne voit que ses produits
" Le mandant courant est defini sur 100 par le framework de test
SELECT COUNT(*)
FROM zi_product
INTO @DATA(lv_count).
cl_abap_unit_assert=>assert_equals(
act = lv_count
exp = 1
msg = 'Le tenant ne devrait voir que ses propres produits"
).
ENDMETHOD.
METHOD test_cross_tenant_access.
" Test pour l'acces cross-tenant du provider
" Necessite l'autorisation speciale Z_PROVIDER
" ...
ENDMETHOD.
ENDCLASS.

Bonnes pratiques

A faire

RecommandationJustification
Utiliser des tables dependantes du mandantIsolation automatique par le systeme
Inclure le Tenant-ID dans les logsPour le debugging et la piste d’audit
Feature Flags par tenantDeployments de fonctionnalites individuels
Tranches de numeros separeesPas de collisions entre tenants
Destinations specifiques au tenantChaque client a ses propres backends

A eviter

A eviterRisque
Mandants codes en durNe fonctionne pas en multitenancy
Cross-Client sans autorisationFuite de donnees entre tenants
Variables globales pour donnees tenantConditions de course
Parser le Tenant-ID depuis l’URLFaille de securite
Operations tenant synchronesProblemes de performance

Comparaison : Single-Tenant vs. Multi-Tenant

AspectSingle-TenantMulti-Tenant
IsolationPhysiquement separeeLogiquement separee
CoutsPlus eleves (par instance)Plus bas (partages)
ScalabilitePar tenantSur tous les tenants
CustomizingIllimiteDans le cadre de la plateforme
Mises a jourIndividuellesPour tous simultanement
ConformitePlus simplePlus complexe

Sujets complementaires

Resume

Le Multitenancy dans ABAP Cloud permet des applications SaaS evolutives :

  1. Shared Schema : Un systeme ABAP pour tous les tenants, separation via mandants
  2. Isolation automatique : Les CDS Views et acces base de donnees sont par defaut dependants du mandant
  3. Gestion du cycle de vie : SaaS Provisioning Service pour l’onboarding et l’offboarding
  4. Contexte Tenant : cl_abap_context_info pour acceder aux informations du tenant
  5. Autorisations : IAM Business Catalogs pour les roles provider et consumer

La combinaison des services SAP BTP (XSUAA, Destination Service, SaaS Provisioning) et ABAP Cloud fournit une base solide pour les applications metier multi-locataires.