名前空間を使用したマルチテナンシーの実装

Namespaces API では、namespace_manager パッケージを使用して appengine_config.py の各テナントに対する名前空間文字列を選択するだけで、アプリケーションでマルチテナンシーを簡単に有効にできます。

現在の名前空間の設定

名前空間は、namespace_manager を使用して取得、設定、検証できます。名前空間マネージャーを使用すると、名前空間に対応した API 用として現在の名前空間を設定できます。appengine_config.py を使用して現在の名前空間を事前に設定すると、データストアと memcache では自動的にその名前空間が使用されます。

ほとんどの App Engine デベロッパーは、Google Workspace(旧 G Suite)ドメインを現在の名前空間として使用します。Google Workspace では、所有している任意のドメインにアプリをデプロイできるため、この仕組みを使用してドメインごとに異なる名前空間を簡単に構成できます。その後で、それらの個別の名前空間を使用して、データをドメイン間で分離できます。詳細については、カスタム ドメインのマッピングをご覧ください。

次のコードサンプルは、URL のマッピングに使用した Google Workspace ドメインを現在の名前空間として設定する方法を示しています。同じ Google Workspace ドメインでマッピングされるすべての URL に対して同じ文字列が使用されることに注意してください。

Python で名前空間を設定するには、アプリケーションのルートディレクトリにある App Engine 構成システム appengine_config.py を使用します。次の簡単な例は、Google Workspace ドメインを現在の名前空間として使用する方法を示しています。

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

namespace の値を指定しない場合、名前空間が空の文字列に設定されます。namespace には任意の文字列を指定できますが、文字数は 100 文字までに制限されます。また、使用できる文字は英数字、ピリオド、アンダースコア、ハイフンだけです。より明示的に設定するには、名前空間文字列は正規表現 [0-9A-Za-z._-]{0,100} と同じ形式で設定する必要があります。

慣例として、「_」(アンダースコア)で始まるすべての名前空間はシステムで使用するために予約されています。システムの名前空間に関するこのルールは強制的なものではありませんが、従わないと、不安定で好ましくない結果を引き起こす原因になることがあります。

appengine_config.py の構成に関するその他一般的な情報については、Python モジュールの構成をご覧ください。

データ漏洩の防止

マルチテナント アプリに関連する一般的なリスクの 1 つに名前空間でのデータ漏洩があります。次のようなさまざまな状況で、意図しないデータ漏洩が発生する可能性があります。

  • 名前空間をまだサポートしていない App Engine API で名前空間を使用した場合。たとえば、Blobstore では名前空間がサポートされていません。Blobstore で名前空間を使用する場合は、エンドユーザーのリクエストに対して Blobstore クエリを使用しないようにするか、信頼できないソースから取得した Blobstore キーを使用しないようにする必要があります。
  • URL Fetch やその他のメカニズムで、名前空間の区分化スキームを指定せずに外部ストレージ メディアを使用した場合(Memcache とデータストアは使用しない)。
  • ユーザーのメールドメインに基づいて名前空間を設定した場合。ほとんどの場合、特定のドメインのすべてのメールアドレスが特定の名前空間にアクセスするのは適切ではありません。また、メールドメインを使用すると、ユーザーがログインするまでアプリケーションで名前空間を使用できません。

名前空間のデプロイ

以降のセクションでは、その他の App Engine ツールや API を使用して名前空間をデプロイする方法について説明します。

ユーザー単位での名前空間の作成

一部のアプリケーションでは、名前空間をユーザー単位で作成する必要があります。ログインしているユーザーのユーザーレベルでデータを区分けする場合は、ユーザーを一意に識別する永続的な ID を返す User.user_id() の使用を検討してください。次のコードサンプルは、この目的のために Users API を使用する方法を示したものです。

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

通常、名前空間をユーザー単位で作成するアプリでは、ユーザーごとに固有のランディング ページも用意します。その場合は、ユーザーに表示するランディング ページを決定する URL スキームをアプリケーションで指定する必要があります。

データストアでの名前空間の使用

データストアのデフォルトでは、名前空間マネージャーで設定された現在の名前空間がデータストアのリクエストに使用されます。Key または Query オブジェクトの作成時には、API により現在の名前空間がこのオブジェクトに適用されます。したがって、アプリケーションで Key または Query オブジェクトをシリアル化された形式で格納する場合は、それらのシリアル化で名前空間が保持されるので、注意が必要です。

シリアル化解除されたオブジェクト(KeyQuery)を使用している場合は、意図したとおりに動作することを確認します。他のストレージ メカニズムを使用せずにデータストアを使用するシンプルなアプリケーション(put / query / get)の多くは、データストア API を呼び出す前に現在の名前空間を設定すれば、想定どおりに動作します。

QueryKey オブジェクトは、名前空間に関して次のような独特の動作をします。

  • Query オブジェクトと Key オブジェクトは、明示的な名前空間を設定しない限り、現在の名前空間を継承します。
  • アプリケーションが新しい Key を祖先から作成すると、新しい Key は祖先の名前空間を継承します。

次のコードサンプルは、グローバル名前空間と任意に指定された名前空間のデータストア内のカウンタをインクリメントするサンプル リクエスト ハンドラを示しています。

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)

Memcache での名前空間の使用

デフォルトで、Memcache のリクエストには名前空間マネージャーで設定された現在の名前空間が使用されます。ほとんどの場合、Memcache で名前空間を明示的に設定する必要はなく、設定すると予期しないバグの原因になることもあります。

ただし、特定のまれな状況では、Memcache で名前空間を明示的に設定した方がよい場合もあります。たとえば、すべての名前空間で共有される共通のデータ(国コードが格納されたテーブルなど)をアプリケーションで使用する場合などです。

次のコード スニペットは、Memcache で名前空間を明示的に設定する方法を示したものです。

Memcache 用の Python API を使用すると、現在の名前空間を名前空間マネージャーから取得したり、Memcache サービスの作成時に名前空間を明示的に設定できます。

次のコードサンプルは、グローバル名前空間と任意に指定された名前空間の Memcache 内のカウンタをインクリメントするサンプル リクエスト ハンドラを示しています。

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)

次の例では、Memcache に値を保存するときに名前空間を明示的に設定しています。

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

タスクキューでの名前空間の使用

デフォルトで、push キューはタスクの作成時に名前空間マネージャーで設定された現在の名前空間を使用します。ほとんどの場合、タスクキューで名前空間を明示的に設定する必要はなく、設定すると予期しないバグの原因になることもあります。

タスク名はすべての名前空間で共有されます。使用する名前空間が異なる場合でも、同じ名前のタスクを複数作成することはできません。同じタスク名を複数の名前空間で使用したい場合は、タスク名にそれぞれの名前空間を付加できます。

新しいタスクでタスクキュー add() メソッドを呼び出すと、現在の名前空間と Google Workspace ドメイン(該当する場合)が名前空間マネージャーからコピーされます。タスクの実行時に、現在の名前空間と Google Workspace の名前空間が復元されます。

現在の名前空間が元のリクエストに設定されていない場合(つまり、get_namespace()'' を返した場合)、set_namespace() を使用してタスクの現在の名前空間を設定できます。

すべての名前空間に関連するタスクの名前空間を明示的に設定した方がよいような、まれな状況もあります。たとえば、すべての名前空間の使用統計情報を集約するタスクを作成する場合が挙げられます。この場合は、タスクの名前空間を明示的に設定できます。次のコードサンプルは、タスクキューで名前空間を明示的に設定する方法を示したものです。

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)

Blobstore での名前空間の使用

Blobstore は名前空間で分割されません。Blobstore で名前空間を保持するには、名前空間に対応したストレージ メディア(現在は Memcache、データストア、タスクキューのみ)から Blobstore にアクセスする必要があります。たとえば、blob の Key がデータストア エンティティに格納されている場合、名前空間に対応したデータストアの Key または Query を使用してアクセスできます。

名前空間対応のストレージに格納されたキーを使用してアプリケーションから Blobstore にアクセスする場合、Blobstore 自体を名前空間で分割する必要はありません。名前空間での blob のデータ漏洩を防ぐために、次の点に注意してください。

  • エンドユーザー リクエストに BlobInfo.gql() を使用しないでください。管理者のリクエスト(アプリケーションのすべての blob に関するレポートの生成など)には BlobInfo クエリを使用してもかまいませんが、エンドユーザーのリクエストに使用すると、すべての BlobInfo レコードが名前空間で区分されないためデータ漏洩が発生する可能性があります。
  • 信頼できないソースから取得した Blobstore キーは使用しないでください。

データストア クエリの名前空間の設定

Google Cloud Console では、データストア クエリの名前空間を設定できます。

デフォルト以外の名前空間を使用する場合は、使用する名前空間をプルダウンから選択します。

一括ローダーでの名前空間の使用

一括ローダーは、使用する名前空間を指定できる --namespace=NAMESPACE フラグをサポートしています。それぞれの名前空間が別々に処理されるため、すべての名前空間にアクセスする場合は反復処理が必要になります。

Index の新しいインスタンスを作成すると、デフォルトでは現在の名前空間に割り当てられます。

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

名前空間をコンストラクタで明示的に割り当てることもできます。

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

インデックス仕様の作成後は、その名前空間を変更できません。

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