Mengimplementasikan Multitenancy Menggunakan Namespace

Namespaces API memungkinkan Anda mengaktifkan multitenancy dengan mudah di aplikasi, cukup dengan memilih string namespace untuk setiap tenant di appengine_config.py menggunakan paket namespace_manager.

Menetapkan namespace saat ini

Anda bisa mendapatkan, menetapkan, dan memvalidasi namespace menggunakan namespace_manager. Pengelola namespace memungkinkan Anda menyetel namespace saat ini untuk API yang mendukung namespace. Anda sudah menetapkan namespace saat ini di awal menggunakan appengine_config.py, dan datastore serta memcache akan otomatis menggunakan namespace tersebut.

Sebagian besar developer App Engine akan menggunakan domain Google Workspace (sebelumnya G Suite) mereka sebagai namespace saat ini. Dengan Google Workspace, Anda dapat men-deploy aplikasi ke domain apa pun yang Anda miliki, sehingga mekanisme ini dapat dengan mudah digunakan untuk mengonfigurasi namespace yang berbeda untuk domain yang berbeda. Kemudian, Anda dapat menggunakan namespace yang terpisah untuk memisahkan data di seluruh domain. Untuk informasi selengkapnya, lihat Memetakan Domain Kustom.

Contoh kode berikut menunjukkan cara menetapkan namespace saat ini ke domain Google Workspace yang digunakan untuk memetakan URL. Secara khusus, string ini akan sama untuk semua URL yang dipetakan melalui domain Google Workspace yang sama.

Untuk menyetel namespace di Python, gunakan sistem konfigurasi App Engine appengine_config.py di direktori root aplikasi Anda. Contoh sederhana berikut menunjukkan cara menggunakan domain Google Workspace Anda sebagai namespace saat ini:

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

Jika Anda tidak menentukan nilai untuk namespace, namespace akan disetel ke string kosong. String namespace bersifat arbitrer, tetapi juga dibatasi maksimal 100 karakter alfanumerik, titik, garis bawah, dan tanda hubung. Secara lebih eksplisit, string namespace harus cocok dengan ekspresi reguler [0-9A-Za-z._-]{0,100}.

Berdasarkan konvensi, semua namespace yang diawali dengan "_" (garis bawah) dicadangkan untuk penggunaan sistem. Aturan namespace sistem ini tidak diterapkan, tetapi Anda dapat dengan mudah mengalami konsekuensi negatif yang tidak terdefinisi jika tidak mengikutinya.

Untuk informasi umum lebih lanjut tentang cara mengonfigurasi appengine_config.py, lihat Konfigurasi Modul Python.

Menghindari kebocoran data

Salah satu risiko yang umumnya terkait dengan aplikasi multitenant adalah bahaya kebocoran data ke seluruh namespace. Kebocoran data yang tidak disengaja dapat muncul dari banyak sumber, termasuk:

  • Menggunakan namespace dengan API App Engine yang belum mendukung namespace. Misalnya, Blobstore tidak mendukung namespace. Jika menggunakan Namespace dengan Blobstore, Anda harus menghindari penggunaan kueri Blobstore untuk permintaan pengguna akhir, atau kunci Blobstore dari sumber yang tidak dipercaya.
  • Menggunakan media penyimpanan eksternal (bukannya memcache dan datastore), melalui URL Fetch atau mekanisme lainnya, tanpa menyediakan skema pemisahan untuk namespace.
  • Menetapkan namespace berdasarkan domain email pengguna. Pada umumnya, Anda tidak ingin semua alamat email domain mengakses namespace. Menggunakan domain email juga mencegah aplikasi Anda menggunakan namespace hingga pengguna login.

Men-deploy namespace

Bagian berikut menjelaskan cara men-deploy namespace dengan alat dan API App Engine lainnya.

Membuat namespace per pengguna

Beberapa aplikasi perlu membuat namespace untuk masing-masing pengguna. Jika Anda ingin memisahkan data di tingkat pengguna untuk pengguna yang login, sebaiknya gunakan User.user_id(), yang menampilkan ID permanen yang unik untuk pengguna tersebut. Contoh kode berikut menunjukkan cara menggunakan Users API untuk tujuan ini:

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

Biasanya, aplikasi yang membuat namespace per pengguna juga menyediakan halaman landing tertentu kepada pengguna yang berbeda. Dalam hal ini, aplikasi harus menyediakan skema URL yang menentukan halaman landing yang akan ditampilkan kepada pengguna.

Menggunakan namespace dengan Datastore

Secara default, datastore menggunakan setelan namespace saat ini pada pengelola namespace untuk permintaan datastore. API menerapkan namespace saat ini ke objek Key atau Query saat dibuat. Oleh karena itu, Anda harus berhati-hati jika aplikasi menyimpan objek Key atau Query dalam bentuk serial, karena namespace dipertahankan dalam serialisasi tersebut.

Jika Anda menggunakan objek Key dan Query yang dideserialisasi, pastikan objek tersebut berperilaku sebagaimana mestinya. Sebagian besar aplikasi sederhana yang menggunakan datastore (put/query/get) tanpa menggunakan mekanisme penyimpanan lain akan berfungsi seperti yang diharapkan dengan menyetel namespace saat ini sebelum memanggil datastore API apa pun.

Objek Query dan Key menunjukkan perilaku unik berikut terkait namespace:

  • Objek Query dan Key mewarisi namespace saat ini ketika dibuat, kecuali jika Anda menetapkan namespace eksplisit.
  • Saat aplikasi membuat Key baru dari ancestor, Key baru akan mewarisi namespace ancestor.

Contoh kode berikut menunjukkan contoh pengendali permintaan yang menambahkan penghitung di datastore untuk namespace global dan namespace yang ditentukan secara acak.

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)

Menggunakan namespace dengan Memcache

Secara default, memcache menggunakan namespace saat ini dari pengelola namespace untuk permintaan memcache. Pada umumnya, Anda tidak perlu secara eksplisit menetapkan namespace di memcache, dan melakukannya dapat menimbulkan bug tidak terduga.

Namun, ada beberapa instance unik yang situasinya sesuai untuk menetapkan namespace secara eksplisit di memcache. Misalnya, aplikasi Anda mungkin memiliki data umum yang dibagikan di seluruh namespace (misalnya tabel yang berisi kode negara).

Cuplikan kode berikut menunjukkan cara menetapkan namespace secara eksplisit di memcache:

Dengan menggunakan Python API untuk memcache, Anda bisa mendapatkan namespace saat ini dari pengelola namespace atau menyetelnya secara eksplisit saat Anda membuat layanan memcache.

Contoh kode berikut menunjukkan contoh pengendali permintaan yang menambahkan penghitung dalam memcache untuk namespace global dan namespace yang ditentukan secara acak.

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)

Contoh di bawah menetapkan namespace secara eksplisit saat Anda menyimpan nilai dalam memcache:

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

Menggunakan namespace dengan Task Queue

Secara default, push queue menggunakan namespace saat ini seperti yang ditetapkan dalam pengelola namespace pada saat tugas dibuat. Pada umumnya, Anda tidak perlu menetapkan namespace secara eksplisit dalam task queue, dan hal itu dapat menyebabkan bug yang tidak terduga.

Nama tugas digunakan bersama di semua namespace. Anda tidak dapat membuat dua tugas dengan nama yang sama, meskipun keduanya menggunakan namespace berbeda. Jika ingin menggunakan nama tugas yang sama untuk banyak namespace, Anda cukup menambahkan setiap namespace ke nama tugas.

Saat tugas baru memanggil metode add() task queue, task queue akan menyalin namespace saat ini dan (jika ada) domain Google Workspace dari pengelola namespace. Saat tugas dijalankan, namespace saat ini dan namespace Google Workspace akan dipulihkan.

Jika namespace saat ini tidak ditetapkan dalam permintaan asal (dengan kata lain, jika get_namespace() menampilkan ''), Anda dapat menggunakan set_namespace() untuk menetapkan namespace saat ini untuk tugas tersebut.

Ada beberapa instance unik yang sesuai untuk menetapkan namespace secara eksplisit untuk tugas yang berfungsi di semua namespace. Misalnya, Anda dapat membuat tugas yang menggabungkan statistik penggunaan di seluruh namespace. Selanjutnya, Anda dapat menetapkan namespace tugas secara eksplisit. Contoh kode berikut menunjukkan cara menetapkan namespace secara eksplisit dengan task queue.

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)

Menggunakan namespace dengan Blobstore

Blobstore tidak tersegmentasi berdasarkan namespace. Untuk mempertahankan namespace di Blobstore, Anda perlu mengakses Blobstore melalui media penyimpanan yang memahami namespace (saat ini hanya memcache, datastore, dan task queue). Misalnya, jika Key blob disimpan dalam entity datastore, Anda dapat mengaksesnya dengan Key atau Query datastore yang mengetahui namespace.

Jika aplikasi mengakses Blobstore melalui kunci yang disimpan dalam penyimpanan berbasis namespace, Blobstore itu sendiri tidak perlu disegmentasikan berdasarkan namespace. Aplikasi harus menghindari kebocoran blob di antara namespace dengan cara:

  • Tidak menggunakan BlobInfo.gql() untuk permintaan pengguna akhir. Anda dapat menggunakan kueri BlobInfo untuk permintaan administratif (seperti membuat laporan tentang semua blob aplikasi). Namun, menggunakannya untuk permintaan pengguna akhir dapat menyebabkan kebocoran data karena semua data BlobInfo tidak dikelompokkan menurut namespace.
  • Tidak menggunakan kunci Blobstore dari sumber yang tidak tepercaya.

Menetapkan namespace untuk Kueri Datastore

Di konsol Google Cloud, Anda dapat menetapkan namespace untuk kueri Datastore.

Jika tidak ingin menggunakan default-nya, pilih namespace yang ingin digunakan dari menu drop-down.

Menggunakan namespace dengan Loader Massal

Loader massal mendukung flag --namespace=NAMESPACE yang memungkinkan Anda menentukan namespace yang akan digunakan. Setiap namespace ditangani secara terpisah dan, jika ingin mengakses semua namespace, Anda harus melakukan iterasi.

Saat Anda membuat instance Index baru, instance tersebut akan ditetapkan ke namespace saat ini secara default:

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

Anda juga dapat menetapkan namespace secara eksplisit di konstruktor:

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

Setelah Anda membuat spesifikasi indeks, namespace-nya tidak dapat diubah:

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