네임스페이스를 사용한 멀티테넌시 구현

Namespaces API를 사용하면 namespace_manager 패키지를 사용하여 appengine_config.py의 각 테넌트에 네임스페이스 문자열을 선택하여 애플리케이션에서 멀티테넌시를 쉽게 사용 설정할 수 있습니다.

현재 네임스페이스 설정

namespace_manager를 사용하여 네임스페이스를 가져오고 설정하고 확인할 수 있습니다. 네임스페이스 관리자는 네임스페이스 사용 API에 현재 네임스페이스를 설정할 수 있습니다. appengine_config.py을 사용해 미리 현재 네임스페이스를 설정하면 datastore 및 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 모듈 구성을 참조하세요.

데이터 유출 방지

일반적인 다중 테넌트 지원 앱과 관련된 위험 중 하나는 바로 네임스페이스에서의 데이터 유출 위험입니다. 의도치 않은 데이터 유출은 다음을 비롯한 다양한 소스에서 발생할 수 있습니다.

  • 네임스페이스를 아직 지원하지 않는 App Engine API와 함께 네임스페이스를 사용하는 경우입니다. 예를 들어, Blobstore는 네임스페이스를 지원하지 않습니다. Blobstore에서 네임스페이스를 사용하는 경우, 최종 사용자 요청에 대한 Blobstore 쿼리 또는 신뢰할 수 없는 소스의 Blobstore 키 사용을 방지해야 합니다.
  • 네임스페이스에 구획 스키마를 제공하지 않고 URL Fetch 또는 기타 메커니즘을 통해 Memcache 및 Datastore 대신 외부 스토리지 매체를 사용하는 경우입니다.
  • 사용자의 이메일 도메인을 사용해 네임스페이스를 설정하는 경우입니다. 도메인의 모든 이메일 주소가 네임스페이스에 액세스하면 안 되는 것이 일반적입니다. 또한 이메일 도메인을 사용하면 사용자가 로그인할 때까지 애플리케이션에서 네임스페이스를 사용하지 못합니다.

네임스페이스 배포

다음 섹션에서는 다른 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 스키마를 제공해야 합니다.

Datastore에서 네임스페이스 사용

기본적으로 Datastore에서는 Datastore 요청에 네임스페이스 관리자의 현재 네임스페이스 설정을 사용합니다. API는 Key 또는 Query 객체가 생성되면 현재 네임스페이스를 해당 객체에 적용합니다. 따라서 애플리케이션이 Key 또는 Query 객체를 직렬화 형식으로 저장하는 경우 해당 직렬화 형식으로 네임스페이스가 보존되므로 유의해야 합니다.

직렬화를 해제한 KeyQuery 객체를 사용할 때는 정상적으로 동작하는지 확인해야 합니다. 다른 스토리지 메커니즘을 사용하지 않고 Datastore(put/query/get)를 사용하는 대부분의 간단한 애플리케이션은 Datastore API를 호출하기 전에 현재 네임스페이스를 설정해 예상대로 작동합니다.

QueryKey 객체는 네임스페이스와 관련하여 다음과 같은 고유한 동작을 보여줍니다.

  • 명시적 네임스페이스를 설정하지 않으면 QueryKey 객체가 생성될 때 현재 네임스페이스를 상속합니다.
  • 애플리케이션이 상위 항목에서 새 Key를 만들 때 새 Key가 상위 항목의 네임스페이스를 상속합니다.

다음 코드 예시는 전역 네임스페이스 및 임의로 지정된 네임스페이스의 Datastore에서 카운터를 증분하는 샘플 요청 핸들러를 보여줍니다.

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에서 네임스페이스를 명시적으로 설정하는 방법을 보여줍니다.

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

태스크 큐에 네임스페이스 사용

기본적으로 내보내기 대기열에서는 작업이 생성될 당시의 네임스페이스 관리자 설정에 따라 현재 네임스페이스를 사용합니다. 일반적으로 태스크 큐에서는 네임스페이스를 명시적으로 설정할 필요가 없으며 이를 설정할 경우 예기치 않은 버그를 초래할 수 있습니다.

작업 이름이 모든 네임스페이스에서 공유됩니다. 네임스페이스를 다르게 사용해도 동일한 이름의 작업을 2개 만들 수는 없습니다. 여러 네임스페이스에 동일한 작업 이름을 사용하려면 각 네임스페이스를 작업 이름에 추가하면 됩니다.

새 태스크가 태스크 큐 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, Datastore, 태스크 큐만 가능)를 통해 Blobstore에 액세스해야 합니다. 예를 들어 blob의 Key가 Datastore 항목에 저장된 경우 네임스페이스를 인식하는 Datastore Key 또는 Query를 사용하여 여기에 액세스할 수 있습니다.

애플리케이션에서 네임스페이스 인식 스토리지에 저장된 키를 통해 Blobstore에 액세스하는 경우 Blobstore 자체를 네임스페이스에 따라 분할할 필요는 없습니다. 애플리케이션에서 다음과 같이 네임스페이스 간 blob 유출을 방지해야 합니다.

  • 최종 사용자 요청에 대해 BlobInfo.gql()를 사용하지 않습니다. 관리 요청(모든 애플리케이션 blob에 대한 보고서 생성 등)에 BlobInfo 쿼리를 사용할 수 있지만 모든 BlobInfo 레코드가 네임스페이스별로 분류되지는 않기 때문에 최종 사용자 요청에 이를 사용할 경우 데이터가 유출될 수 있습니다.
  • 신뢰할 수 없는 소스의 Blobstore 키를 사용하지 않아야 합니다.

Datastore 쿼리의 네임스페이스 설정

Google Cloud 콘솔에서 Datastore 쿼리의 네임스페이스를 설정할 수 있습니다.

기본값을 사용하지 않으려면 드롭다운에서 사용할 네임스페이스를 선택합니다.

일괄 로더에서 네임스페이스 사용

일괄 로더에서는 사용할 네임스페이스를 지정할 수 있는 --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')