Implementazione della multitenancy mediante gli spazi dei nomi

L'API Namespaces consente di abilitare facilmente la multitenancy nella tua applicazione, semplicemente selezionando una stringa dello spazio dei nomi per ogni tenant in appengine_config.py utilizzando il pacchetto namespace_manager.

Impostazione dello spazio dei nomi attuale

Puoi ottenere, impostare e convalidare gli spazi dei nomi utilizzando namespace_manager. Il gestore dello spazio dei nomi consente di impostare uno spazio dei nomi attuale per le API abilitate per lo spazio dei nomi. Puoi impostare preventivamente uno spazio dei nomi attuale utilizzando appengine_config.py e il datastore e memcache utilizzano automaticamente questo spazio dei nomi.

La maggior parte degli sviluppatori di App Engine utilizzerà il proprio dominio Google Workspace (in precedenza G Suite) come spazio dei nomi attuale. Google Workspace ti consente di eseguire il deployment della tua app in qualsiasi dominio di tua proprietà, quindi puoi utilizzare facilmente questo meccanismo per configurare diversi spazi dei nomi per domini diversi. Questi spazi dei nomi possono essere utilizzati per separare i dati tra i domini. Per ulteriori informazioni, consulta la sezione Mappatura di domini personalizzati.

Il seguente esempio di codice mostra come impostare lo spazio dei nomi attuale sul dominio Google Workspace utilizzato per mappare l'URL. In particolare, questa stringa sarà la stessa per tutti gli URL mappati tramite lo stesso dominio Google Workspace.

Per impostare uno spazio dei nomi in Python, utilizza il sistema di configurazione di App Engine appengine_config.py nella directory root dell'applicazione. Il seguente esempio semplice mostra come utilizzare il dominio Google Workspace come spazio dei nomi attuale:

from google.appengine.api import namespace_manager

# Called only if the current namespace is not set.
def namespace_manager_default_namespace_for_request():
    # The returned string will be used as the Google Apps domain.
    return namespace_manager.google_apps_namespace()

Se non specifichi un valore per namespace, lo spazio dei nomi viene impostato su una stringa vuota. La stringa namespace è arbitraria, ma è limitata a un massimo di 100 caratteri alfanumerici, punti, trattini bassi e trattini. In modo più esplicito, le stringhe dello spazio dei nomi devono corrispondere all'espressione regolare [0-9A-Za-z._-]{0,100}.

Per convenzione, tutti gli spazi dei nomi che iniziano con "_" (trattino basso) sono riservati all'uso da parte del sistema. Questa regola per lo spazio dei nomi di sistema non viene applicata, ma se non la segui, potresti facilmente riscontrare conseguenze negative non definite.

Per informazioni più generali sulla configurazione di appengine_config.py, consulta Configurazione del modulo Python.

Evitare fughe di dati

Uno dei rischi comunemente associati alle app multitenant è il pericolo che i dati vadano persi negli spazi dei nomi. Le fughe di dati involontarie possono derivare da molte origini, tra cui:

  • Utilizzo di spazi dei nomi con API App Engine che non supportano ancora gli spazi dei nomi. Ad esempio, Blobstore non supporta gli spazi dei nomi. Se utilizzi Spazi dei nomi con Blobstore, devi evitare di utilizzare query Blobstore per le richieste degli utenti finali o chiavi Blobstore provenienti da fonti non attendibili.
  • Utilizzo di un supporto di archiviazione esterno (anziché memcache e datastore), tramite URL Fetch o qualche altro meccanismo, senza fornire uno schema di compartimentazione per gli spazi dei nomi.
  • Impostazione di uno spazio dei nomi basato sul dominio email di un utente. Nella maggior parte dei casi, non è consigliabile che tutti gli indirizzi email di un dominio accedano a uno spazio dei nomi. L'uso del dominio email impedisce inoltre all'applicazione di utilizzare uno spazio dei nomi finché l'utente non esegue l'accesso.

Deployment degli spazi dei nomi

Le seguenti sezioni descrivono come eseguire il deployment degli spazi dei nomi con altri strumenti e API di App Engine.

Creazione di spazi dei nomi in base all'utente

Alcune applicazioni devono creare spazi dei nomi in base all'utente. Se vuoi suddividere i dati a livello di utente per gli utenti che hanno eseguito l'accesso, ti consigliamo di utilizzare User.user_id(), che restituisce un ID univoco e permanente per l'utente. Il seguente esempio di codice mostra come utilizzare l'API Users per questo scopo:

from google.appengine.api import users

def namespace_manager_default_namespace_for_request():
    # assumes the user is logged in.
    return users.get_current_user().user_id()

In genere, le app che creano spazi dei nomi per singolo utente forniscono anche pagine di destinazione specifiche a utenti diversi. In questi casi, l'applicazione deve fornire uno schema URL che detta la pagina di destinazione da mostrare a un utente.

Utilizzo degli spazi dei nomi con Datastore

Per impostazione predefinita, il datastore utilizza l'impostazione dello spazio dei nomi attuale nel gestore dello spazio dei nomi per le richieste del datastore. L'API applica questo spazio dei nomi attuale agli oggetti Key o Query al momento della creazione. Di conseguenza, devi fare attenzione se un'applicazione archivia gli oggetti Key o Query in forma serializzata, poiché lo spazio dei nomi viene conservato in queste serializzazioni.

Se utilizzi oggetti Key e Query deserializzati, assicurati che si comportino come previsto. Le applicazioni più semplici che utilizzano il datastore (put/query/get) senza utilizzare altri meccanismi di archiviazione funzioneranno come previsto impostando lo spazio dei nomi attuale prima di chiamare qualsiasi API del datastore.

Gli oggetti Query e Key mostrano i seguenti comportamenti unici in merito agli spazi dei nomi:

  • Gli oggetti Query e Key ereditano lo spazio dei nomi attuale quando vengono creati, a meno che tu non imposti uno spazio dei nomi esplicito.
  • Quando un'applicazione crea un nuovo Key da un predecessore, il nuovo Key eredita lo spazio dei nomi del predecessore.

Il seguente esempio di codice mostra un gestore di richieste di esempio che incrementa un contatore nel datastore per lo spazio dei nomi globale e uno spazio dei nomi arbitrario.

from google.appengine.api import namespace_manager
from google.appengine.ext import ndb
import webapp2

class Counter(ndb.Model):
    count = ndb.IntegerProperty()

@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count

class DatastoreCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = update_counter('counter')

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = update_counter('counter')
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))

app = webapp2.WSGIApplication([
    (r'/datastore', DatastoreCounterHandler),
    (r'/datastore/(.*)', DatastoreCounterHandler)
], debug=True)

Utilizzo degli spazi dei nomi con memcache

Per impostazione predefinita, memcache utilizza lo spazio dei nomi attuale del gestore dello spazio dei nomi per le richieste memcache. Nella maggior parte dei casi, non è necessario impostare esplicitamente uno spazio dei nomi nella memcache e questo potrebbe introdurre bug imprevisti.

Tuttavia, ci sono alcune istanze univoche in cui è opportuno impostare esplicitamente uno spazio dei nomi nella memcache. Ad esempio, la tua applicazione potrebbe avere dati comuni condivisi tra tutti gli spazi dei nomi (ad esempio una tabella contenente i codici paese).

Lo snippet di codice riportato di seguito mostra come impostare esplicitamente lo spazio dei nomi in memcache:

Utilizzando l'API Python per memcache, puoi ottenere lo spazio dei nomi attuale dal gestore dello spazio dei nomi o impostarlo in modo esplicito quando crei il servizio memcache.

Il seguente esempio di codice mostra un gestore di richieste di esempio che incrementa un contatore in memcache per lo spazio dei nomi globale e uno spazio dei nomi specificato arbitrariamente.

from google.appengine.api import memcache
from google.appengine.api import namespace_manager
import webapp2

class MemcacheCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = memcache.incr('counter', initial_value=0)

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = memcache.incr('counter', initial_value=0)
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))

app = webapp2.WSGIApplication([
    (r'/memcache', MemcacheCounterHandler),
    (r'/memcache/(.*)', MemcacheCounterHandler)
], debug=True)

L'esempio seguente imposta lo spazio dei nomi in modo esplicito quando archivi un valore in memcache:

  // Store an entry to the memcache explicitly
memcache.add("key", data, namespace='abc')

Utilizzo degli spazi dei nomi con la coda di attività

Per impostazione predefinita, le code in modalità push utilizzano lo spazio dei nomi attuale impostato nel gestore dello spazio dei nomi al momento della creazione dell'attività. Nella maggior parte dei casi, non è necessario impostare esplicitamente uno spazio dei nomi nella coda di attività e questo potrebbe introdurre bug imprevisti.

I nomi delle attività sono condivisi tra tutti gli spazi dei nomi. Non puoi creare due attività con lo stesso nome, anche se utilizzano spazi dei nomi diversi. Se vuoi utilizzare lo stesso nome dell'attività per molti spazi dei nomi, puoi semplicemente aggiungere ogni spazio dei nomi al nome dell'attività.

Quando una nuova attività chiama il metodo add() della coda di attività, la coda di attività copia lo spazio dei nomi attuale e, se applicabile, il dominio Google Workspace dal gestore dello spazio dei nomi. Quando l'attività viene eseguita, lo spazio dei nomi attuale e quello di Google Workspace vengono ripristinati.

Se lo spazio dei nomi attuale non è impostato nella richiesta di origine (in altre parole, se get_namespace() restituisce ''), puoi utilizzare set_namespace() per impostare lo spazio dei nomi attuale per l'attività.

Esistono alcune istanze univoche in cui è opportuno impostare esplicitamente uno spazio dei nomi per un'attività che funziona in tutti gli spazi dei nomi. Ad esempio, potresti creare un'attività che aggrega le statistiche sull'utilizzo per tutti gli spazi dei nomi. Puoi quindi impostare esplicitamente lo spazio dei nomi dell'attività. Il seguente esempio di codice mostra come impostare esplicitamente gli spazi dei nomi con la coda di attività.

from google.appengine.api import namespace_manager
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
import webapp2

class Counter(ndb.Model):
    count = ndb.IntegerProperty()

@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count

def get_count(name):
    counter = Counter.get_by_id(name)
    if not counter:
        return 0
    return counter.count

class DeferredCounterHandler(webapp2.RequestHandler):
    def post(self):
        name = self.request.get('counter_name')
        update_counter(name)

class TaskQueueCounterHandler(webapp2.RequestHandler):
    """Queues two tasks to increment a counter in global namespace as well as
    the namespace is specified by the request, which is arbitrarily named
    'default' if not specified."""
    def get(self, namespace='default'):
        # Queue task to update global counter.
        current_global_count = get_count('counter')
        taskqueue.add(
            url='/tasks/counter',
            params={'counter_name': 'counter'})

        # Queue task to update counter in specified namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            current_namespace_count = get_count('counter')
            taskqueue.add(
                url='/tasks/counter',
                params={'counter_name': 'counter'})
        finally:
            namespace_manager.set_namespace(previous_namespace)

        self.response.write(
            'Counters will be updated asyncronously.'
            'Current values: Global: {}, Namespace {}: {}'.format(
                current_global_count, namespace, current_namespace_count))

app = webapp2.WSGIApplication([
    (r'/tasks/counter', DeferredCounterHandler),
    (r'/taskqueue', TaskQueueCounterHandler),
    (r'/taskqueue/(.*)', TaskQueueCounterHandler)
], debug=True)

Utilizzo degli spazi dei nomi con Blobstore

Il Blobstore non è segmentato per spazio dei nomi. Per mantenere uno spazio dei nomi in Blobstore, devi accedere a Blobstore tramite un supporto di archiviazione che conosca lo spazio dei nomi (attualmente solo memcache, datastore e coda di attività). Ad esempio, se il Key di un BLOB viene archiviato in un'entità datastore, puoi accedervi con un datastore Key o Query che conosca lo spazio dei nomi.

Se l'applicazione accede al Blobstore tramite chiavi archiviate in uno spazio di archiviazione sensibile allo spazio dei nomi, il Blobstore non deve essere segmentato per spazio dei nomi. Le applicazioni devono evitare fughe di BLOB tra gli spazi dei nomi:

  • Non utilizzare BlobInfo.gql() per le richieste degli utenti finali. Puoi utilizzare le query BlobInfo per le richieste amministrative (ad esempio per generare report su tutti i BLOB delle applicazioni), ma il loro utilizzo per le richieste degli utenti finali potrebbe causare fughe di dati, perché tutti i record BlobInfo non sono compartimentati per spazio dei nomi.
  • Non utilizzano chiavi Blobstore da fonti non attendibili.

Impostazione degli spazi dei nomi per le query Datastore

Nella console Google Cloud, puoi impostare lo spazio dei nomi per le query Datastore.

Se non vuoi utilizzare quello predefinito, seleziona dal menu a discesa quello che vuoi usare.

Utilizzo degli spazi dei nomi con il caricatore collettivo

Il caricatore collettivo supporta un flag --namespace=NAMESPACE che consente di specificare lo spazio dei nomi da utilizzare. Ogni spazio dei nomi viene gestito separatamente e, se vuoi accedervi, dovrai eseguirne l'iterazione.

Quando crei una nuova istanza di Index, questa viene assegnata per impostazione predefinita allo spazio dei nomi attuale:

# set the current namespace
namespace_manager.set_namespace("aSpace")
index = search.Index(name="myIndex")
# index namespace is now fixed to "aSpace"

Puoi anche assegnare uno spazio dei nomi esplicitamente nel costruttore:

index = search.Index(name="myIndex", namespace="aSpace")

Una volta creata una specifica dell'indice, il relativo spazio dei nomi non può essere modificato:

# change the current namespace
namespace_manager.set_namespace("anotherSpace")
# the namespaceof 'index' is still "aSpace" because it was bound at create time
index.search('hello')