Mettre en œuvre l'architecture mutualisée avec des espaces de nommage

L'API Namespaces vous permet d'activer facilement une architecture mutualisée dans votre application, en sélectionnant simplement une chaîne d'espace de noms pour chaque locataire dans appengine_config.py à l'aide du package namespace_manager.

Définir l'espace de nommage actuel

Vous pouvez obtenir, définir et valider des espaces de noms à l'aide de namespace_manager. Le gestionnaire d'espaces de noms vous permet de définir un espace de noms actuel pour les API compatibles avec les espaces de noms. Utilisez appengine_config.py pour définir un espace de noms actuel à l'avance. Le datastore et le système Memcache utiliseront automatiquement cet espace de noms.

La plupart des développeurs App Engine utilisent leur domaine Google Workspace (anciennement G Suite) comme espace de noms actuel. Google Workspace vous permet de déployer votre application sur n'importe quel domaine que vous possédez. Vous pouvez ainsi facilement utiliser ce mécanisme pour configurer différents espaces de nommage pour des domaines distincts. Vous pouvez ensuite utiliser ces espaces de nommage séparés pour isoler les données sur l'ensemble des domaines. Pour en savoir plus, consultez la section Mapper des domaines personnalisés.

L'exemple de code suivant vous montre comment définir l'espace de noms actuel sur le domaine Google Workspace utilisé pour mapper l'URL. En particulier, cette chaîne sera la même pour toutes les URL mappées via le même domaine Google Workspace.

Pour définir un espace de noms en Python, utilisez le système de configuration appengine_config.py App Engine dans le répertoire racine de votre application. L'exemple simple ci-dessous montre comment utiliser votre domaine Google Workspace comme espace de noms actuel :

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()

Si vous ne spécifiez pas de valeur pour namespace, l'espace de noms est défini sur une chaîne vide. La chaîne namespace est arbitraire, mais également limitée à un maximum de 100 caractères alphanumériques, points, traits de soulignement et tirets. Pour être plus explicite, cela signifie que les chaînes d'espace de noms doivent correspondre à l'expression régulière [0-9A-Za-z._-]{0,100}.

Par convention, tous les espaces de noms commençant par "_" (trait de soulignement) sont destinés à être utilisés par le système. Cette règle concernant les espaces de nommage système n'est pas appliquée de manière stricte, mais vous risquez d'être confronté à des conséquences négatives non définies si vous ne la respectez pas.

Pour plus d'informations générales sur la configuration de appengine_config.py, consultez la section Configuration du module Python.

Conseils pour éviter les fuites de données

Les fuites de données sur l'ensemble des espaces de nommage constituent l'un des risques communément associés aux applications mutualisées. Les fuites de données involontaires peuvent avoir plusieurs causes, telles que :

  • L'utilisation d'espaces de nommage avec des API App Engine qui ne sont pas encore compatibles avec les espaces de nommage. C'est le cas de Blobstore, par exemple. Si vous utilisez les espaces de noms avec l'API Blobstore, vous devez éviter d'utiliser des requêtes Blobstore pour les requêtes d'utilisateur final ou des clés Blobstore provenant de sources non fiables.
  • L'utilisation d'un support de stockage externe (à la place d'un système Memcache ou d'un datastore), via URL Fetch ou un autre mécanisme, sans prévoir de modèle de compartimentation pour les espaces de noms.
  • La définition d'un espace de noms basé sur le domaine de messagerie d'un utilisateur. Dans la plupart des cas, il n'est pas souhaitable que toutes les adresses e-mail d'un domaine accèdent à un espace de nommage. L'utilisation du domaine de messagerie permet également d'éviter que votre application utilise un espace de nommage tant que l'utilisateur n'est pas connecté.

Déployer des espaces de nommage

Les sections suivantes décrivent comment déployer des espaces de nommage avec d'autres API et outils App Engine.

Créer des espaces de nommage pour chaque utilisateur

Certaines applications nécessitent la création d'espaces de nommage pour chaque utilisateur. Si vous souhaitez compartimenter les données au niveau de l'utilisateur pour les utilisateurs connectés, pensez à inclure l'élément User.user_id(), qui renvoie un ID unique et permanent pour l'utilisateur. L'exemple de code suivant indique comment utiliser l'API Users dans ce sens :

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()

De manière générale, les applications qui créent des espaces de nommage par utilisateur proposent également des pages de destination spécifiques aux différents utilisateurs. Dans ces cas, l'application doit fournir un modèle d'URL qui dicte quelle page de destination afficher à un utilisateur.

Utiliser les espaces de nommage avec le datastore

Pour les requêtes de datastore, le datastore utilise par défaut le paramètre de l'espace de noms actuel défini dans le gestionnaire d'espaces de noms. L'API applique cet espace de noms actuel aux objets Key ou Query lors de leur création. Vous devez donc être vigilant si une application stocke des objets Key ou Query sous des formes sérialisées, étant donné que l'espace de noms est préservé dans ces sérialisations.

Si vous utilisez des objets Key et Query désérialisés, assurez-vous qu'ils se comportent comme prévu. La plupart des applications simples utilisant un datastore (put/query/get) sans autre mécanisme de stockage fonctionnent comme prévu si vous définissez l'espace de noms actuel avant d'appeler une API de datastore.

Les objets Query et Key adoptent les comportements uniques suivants en ce qui concerne les espaces de noms :

  • Lors de leur création, les objets Query et Key héritent de l'espace de noms actuel, à moins que vous ne définissiez explicitement un espace de noms.
  • Lorsqu'une application crée un objet Key à partir d'un ancêtre, ce nouvel objet Key hérite de l'espace de nommage de l'ancêtre.

L'exemple de code suivant indique que le gestionnaire de requêtes incrémente un compteur dans le datastore pour l'espace de nommage global et un espace de nommage nommé arbitrairement.

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)

Utiliser les espaces de noms avec Memcache

Pour les requêtes de cache mémoire, le cache mémoire utilise, par défaut, l'espace de noms actuel à partir du gestionnaire d'espaces de noms. Dans la plupart des cas, il n'est pas nécessaire de définir explicitement un espace de nommage dans le cache mémoire, et des bogues inattendus peuvent survenir si vous le faites.

Il existe cependant certaines instances uniques pour lesquelles il convient de définir explicitement un espace de nommage dans le cache mémoire. Imaginons, par exemple, que votre application possède des données communes partagées par l'ensemble des espaces de nommage (telles qu'un tableau contenant des codes pays).

L'extrait de code suivant indique comment définir explicitement l'espace de nommage dans le cache mémoire :

Avec l'API Python de cache mémoire (Memcache), vous pouvez obtenir l'espace de nommage actuel du gestionnaire d'espaces de nommage ou le définir de façon explicite lors de la création du service Memcache.

L'exemple de code suivant indique que le gestionnaire de requêtes incrémente un compteur dans memcache pour l'espace de nommage global et un espace de nommage nommé arbitrairement.

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)

Dans l'exemple ci-dessous, l'espace de nommage est explicitement défini lorsque vous stockez une valeur dans le cache mémoire :

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

Utiliser les espaces de noms avec l'API Task Queue

Par défaut, les files d'attente d'envoi utilisent l'espace de nommage actuel tel qu'il est défini dans le gestionnaire d'espaces de nommage au moment de la création de la tâche. Dans la plupart des cas, il n'est pas nécessaire de définir explicitement un espace de noms dans la file d'attente de tâches, et des bogues inattendus peuvent survenir si vous le faites.

Les noms de tâches sont partagés par l'ensemble des espaces de nommage. Vous ne pouvez pas créer deux tâches dotées du même nom, même si elles utilisent des espaces de nommage différents. Si vous voulez utiliser le même nom de tâche pour plusieurs espaces de nommage, il vous suffit d'ajouter chaque espace de nommage au nom de tâche.

Lorsqu'une nouvelle tâche appelle la méthode add() de file d'attente de tâches, celle-ci copie l'espace de noms actuel et (le cas échéant) le domaine Google Workspace à partir du gestionnaire d'espaces de noms. Une fois la tâche exécutée, l'espace de noms actuel et l'espace de noms Google Workspace sont restaurés.

Si l'espace de noms actuel n'est pas défini dans la requête d'origine (en d'autres termes, si get_namespace() renvoie ''), vous pouvez utiliser set_namespace() pour définir l'espace de noms actuel pour la tâche.

Il existe certaines instances uniques pour lesquelles il convient de définir explicitement un espace de noms pour une tâche qui fonctionne sur l'ensemble des espaces de noms. Imaginons, par exemple, que vous créiez une tâche qui agrège les statistiques d'utilisation sur l'ensemble des espaces de nommage. Vous pourriez alors définir explicitement l'espace de nommage de la tâche. L'exemple de code suivant indique comment définir explicitement des espaces de nommage avec la file d'attente de tâches.

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)

Utiliser les espaces de nommage avec le Blobstore

Le Blobstore n'est pas segmenté par espace de nommage. Pour préserver un espace de noms dans le Blobstore, vous devez accéder au Blobstore via un support de stockage prenant en charge l'espace de noms (à l'heure actuelle, le système Memcache, le datastore et la file d'attente de tâches). Par exemple, si la clé (Key) d'un objet blob est stockée dans une entité de datastore, vous pouvez y accéder à l'aide d'une clé (Key) ou d'une requête (Query) de datastore qui tient compte de l'espace de noms.

Si l'application accède au Blobstore via des clés stockées dans un emplacement de stockage compatible avec les espaces de noms, il n'est pas nécessaire que le Blobstore lui-même soit segmenté par espace de noms. Afin d'éviter les fuites d'objets blob entre les espaces de noms :

  • Les applications ne doivent pas utiliser BlobInfo.gql() pour des requêtes d'utilisateur final. Les requêtes BlobInfo peuvent être utilisées pour des requêtes administratives (telles que la génération de rapports relatifs à tous les objets blob des applications), mais il est déconseillé de les utiliser pour des requêtes d'utilisateur final du fait du risque de fuites de données, les enregistrements BlobInfo n'étant pas tous compartimentés par espace de noms.
  • Les applications ne doivent pas utiliser de clés Blobstore provenant de sources non approuvées.

Définir des espaces de nommage pour les requêtes Datastore

Dans Google Cloud Console, vous pouvez définir l'espace de noms pour les requêtes Datastore.

Si vous ne souhaitez pas utiliser la valeur par défaut, sélectionnez l'espace de noms à utiliser dans la liste déroulante.

Utiliser les espaces de noms avec l'API Bulk Loader

L'outil de chargement groupé accepte une option --namespace=NAMESPACE qui vous permet de spécifier l'espace de noms à utiliser. Chaque espace de noms est traité séparément, de sorte que si vous voulez accéder à tous les espaces de noms, vous devez tous les parcourir.

Lorsque vous créez une instance de Index, celle-ci est attribuée par défaut à l'espace de noms actuel :

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

Vous pouvez également attribuer des droits d'administrateur de manière explicite dans le constructeur :

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

Une fois que vous avez créé une spécification d'index, son espace de noms ne peut plus être modifié :

# 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')