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

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

現在の名前空間の設定

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

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

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

Java の名前空間は、サーブレットのメソッドを呼び出す前にサーブレットのフィルタ インターフェースを使用して設定できます。次の簡単な例は、Google Workspace ドメインを現在の名前空間として使用する方法を示しています。

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

名前空間フィルタは web.xml ファイルで構成する必要があります。フィルタのエントリが複数ある場合は、最初に設定される名前空間が使用されることに注意してください。

次のコードサンプルは、web.xml で名前空間フィルタを構成する方法を示しています。

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

web.xml ファイルに関する一般的な情報とサーブレットへの URL パスのマッピングについては、デプロイ記述子: web.xml をご覧ください。

一時的なオペレーション用に新しい名前空間を設定し、そのオペレーションの完了後に元の名前空間にリセットすることもできます。これには、次のような try / finally のパターンを使用します。

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

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.getUserId() の使用を検討してください。次のコードサンプルは、この目的のために Users API を使用する方法を示したものです。

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

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

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

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

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

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

  • Query オブジェクトと Key オブジェクトは、明示的な名前空間を設定しない限り、現在の名前空間を継承します。
  • アプリケーションが新しい Key を祖先から作成すると、新しい Key は祖先の名前空間を継承します。
  • Key または Query の名前空間を明示的に設定する Java 用の API はありません。
次のコード例は、Counter データストア エンティティの現在の名前空間と任意の名前の -global- 名前空間のカウントを増分する SomeRequest リクエスト ハンドラを示しています。

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Memcache での名前空間の使用

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

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

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

デフォルトでは、Java 用 Memcache API は名前空間マネージャを照会して MemcacheService から現在の名前空間を取得します。Memcache の作成時に getMemcacheService(Namespace) を使用して名前空間を明示的に指定することもできます。ほとんどのアプリケーションでは、名前空間を明示的に指定する必要はありません。

次のコードサンプルは、名前空間マネージャの現在の名前空間を使用する Memcache の作成方法を示しています。

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

次のコードサンプルは、Memcache サービスの作成時に名前空間を明示的に指定する方法を示しています。

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

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

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

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

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

元のリクエストで現在の名前空間が設定されていない場合(つまり get()null を返す場合)、実行されるタスクの名前空間は "" に設定されます。

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

最初に、Counter データストア エンティティのカウントを増分するタスクキュー ハンドラを作成します。

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

および、

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

次に、サーブレットを使用してタスクを作成します。

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Blobstore での名前空間の使用

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

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

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

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

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

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

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

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

Index の新しいインスタンスには、作成に使用された SearchService の名前空間が継承されます。インデックスへの参照の作成後に名前空間を変更することはできません。SearchService を使用してインデックスを作成する前に、次の 2 つの方法で名前空間を設定できます。

  • デフォルトでは、新しい SearchService は現在の名前空間を使用します。現在の名前空間はサービスの作成前に設定できます。
// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

サービスの作成時に SearchServiceConfig で名前空間を指定できます。

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);