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

Namespaces API を使用すると、アプリケーションでマルチテナンシーを簡単に有効にできます。必要なのは各テナントに対する名前空間文字列を選択することだけです。これには appengine.Namespace 関数を使用します。

現在の名前空間の設定

名前空間は、appengine.Namespace を使用して設定できます。この関数は新しいコンテキストを返し、名前空間に対応した API では自動的に特定の名前空間が使用されるようになります。

ほとんどの App Engine デベロッパーは、G Suite ドメインを現在の名前空間として使用します。G Suite では所有している任意のドメインにアプリをデプロイできるため、このメカニズムを使用してドメインごとに異なる名前空間を簡単に構成できます。その後で、それらの個別の名前空間を使用して、データをドメイン間で分離できます。G Suite ステータス ダッシュボードで複数のドメインを設定する方法の詳細については、G Suite の URL でアプリケーションをデプロイするをご覧ください。

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

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

データ漏洩の防止

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

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

名前空間のデプロイ

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

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

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

import (
	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/user"
)

func namespace(ctx context.Context) context.Context {
	// assumes the user is logged in.
	ctx, err := appengine.Namespace(ctx, user.Current(ctx).ID)
	if err != nil {
		// ...
	}
	return ctx
}

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

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

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

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

次のコード例は、Counter データストア エンティティの現在の名前空間と任意の名前の -global- 名前空間のカウントを増分する SomeRequest リクエスト ハンドラを示したものです。
import (
	"io"
	"net/http"

	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
)

type Counter struct {
	Count int64
}

func incrementCounter(ctx context.Context, name string) error {
	key := datastore.NewKey(ctx, "Counter", name, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		var ctr Counter
		err := datastore.Get(ctx, key, &ctr)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		ctr.Count++
		_, err = datastore.Put(ctx, key, &ctr)
		return err
	}, nil)
}

func someHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)
	err := incrementCounter(ctx, "SomeRequest")
	if err != nil {
		// ... handle err
	}

	// temporarily use a new namespace
	{
		ctx, err := appengine.Namespace(ctx, "-global-")
		if err != nil {
			// ... handle err
		}
		err = incrementCounter(ctx, "SomeRequest")
		if err != nil {
			// ... handle err
		}
	}

	io.WriteString(w, "Updated counters.\n")
}

func init() {
	http.HandleFunc("/some_url", someHandler)
}

Memcache での名前空間の使用

Memcache は、Memcache API の呼び出しで使用される appengine.Context の名前空間を使用します。

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

push キューは、タスクの追加時に appengine.Context で設定されていた名前空間を使用します。

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

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

import (
	"io"
	"net/http"

	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/taskqueue"
)

type Counter struct {
	Count int64
}

func incrementCounter(ctx context.Context, name string) error {
	key := datastore.NewKey(ctx, "Counter", name, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		var ctr Counter
		err := datastore.Get(ctx, key, &ctr)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		ctr.Count++
		_, err = datastore.Put(ctx, key, &ctr)
		return err
	}, nil)
}

// taskQueueHandler serves /_ah/counter.
func taskQueueHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	ctx := appengine.NewContext(r)
	err := incrementCounter(ctx, r.FormValue("counter_name"))
	if err != nil {
		// ... handle err
	}
}

func someRequest(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	// Perform asynchronous requests to update counter.
	// (missing error handling here.)
	t := taskqueue.NewPOSTTask("/_ah/counter", map[string][]string{
		"counter_name": {"someRequest"},
	})

	taskqueue.Add(ctx, t, "")

	// temporarily use a new namespace
	{
		ctx, err := appengine.Namespace(ctx, "-global-")
		if err != nil {
			// ... handle err
		}
		taskqueue.Add(ctx, t, "")
	}

	io.WriteString(w, "Counters will be updated.\n")
}

func init() {
	http.HandleFunc("/_ah/counter", taskQueueHandler)
	http.HandleFunc("/some_request", someRequest)
}

Blobstore での名前空間の使用

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

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

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

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

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

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

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

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

検索オペレーションでは、そのオペレーションの呼び出し時に渡されたコンテキストに関連付けられていた名前空間が使用されます。

名前空間でオペレーションを実行するには、コンテキストを appengine.Namespace でラップし、インデックスに対してメソッドを呼び出すときにそのコンテキストを使用します。以下の例では、コンテキストが「aSpace」名前空間にラップされています。

// Open an index
index, err := search.Open("index")

// Wrap the context into a namespace
c = appengine.Namespace(c, "aSpace")

// The Search operation occurs in the namespace "aSpace"
it := index.Search(c, "hello", nil)
このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Go の App Engine スタンダード環境