マルチテナントサービスにおけるDynamoDBの設計を考えてみる




今回はマルチテナントサービスにおけるDynamoDBの設計を考えてみたいと思います。

マルチテナントサービスの開発にあたりDB設計やセキュリティ対策について調べた備忘録的な記事ではありますが参考になれば幸いです。

この記事では、AWSのNoSQLデータベースであるDynamoDBの設計戦略を紹介します。


## そもそもマルチテナントサービスとは何か
マルチテナントサービスとは、一つのアプリケーションインスタンスを複数の利用者や組織が共有する形態のサービスです。

## DynamoDBの複合主キーについて
DynamoDBでは、Partition KeyとSort Keyの二つからなるComposite Primary Key(複合主キー)を使うことができます。Partition Keyはデータをパーティションに分散させ、Sort Keyはそのパーティション内でのデータの並べ方を制御します。これにより、効率的なデータアクセスパターンが実現できます。

## マルチテナントのデータ設計の戦略

マルチテナントサービスのDB設計にはいくつかのパターンがありますが、
本記事ではテナントごとにDBを分ける方法と、単一テーブルで全てのデータを管理する方法の2つのパターンを紹介します。

### パターン①: テナントごとにDBを分ける
<dl>
<dt>説明:</dt>
<dd>各テナントに対して別々のDynamoDBテーブルを作成します。</dd>
<dt>利点:</dt>
<dd>完全なデータ隔離</dd>
<dd>セキュリティのシンプリシティ</dd>
<dt>欠点:</dt>
<dd>多くのテーブルを管理する必要がある</dd>
<dd>クエリを跨いでテーブルを操作することが難しい</dd>
<dd>コストが高くなる場合がある</dd>
</dl>

### パターン②: 全てのテナントデータを一つのDynamoDBテーブルに保存

<dl>
<dt>説明:</dt>
<dd>一つのDynamoDBテーブルを用い、複合主キーを採用します。ここで、テナントIDをPartition Keyとし、それに紐づくデータの識別子（例えば、ユーザーIDやオブジェクトID）をSort Keyとして使用します。これにより、同一のテーブル内で各テナントのデータを効率的に隔離・管理できます。</dd>
<dt>利点:</dt>
<dd>管理が簡単（テーブル数が少ない）</dd>
<dd>スケールが容易</dd>
<dd>コスト効率が良い場合がある</dd>
<dt>欠点:</dt>
<dd>セキュリティの実装が複雑になる場合がある</dd>
<dd>テナント間でリソースの競合が発生する可能性がある</dd>
</dl>

**この記事ではパターン②を採用します。**

これは、シングルテーブル設計によって運用・管理の複雑性を抑え、コストを効率化できるためです。
後述のセキュリティ対策も含め、実際の設計例を紹介します。

## 実践例: マルチテナントDynamoDBテーブル設計

### シナリオ1: テナントとユーザーのデータ構造

このシナリオは、単純なマルチテナントのユーザー管理が目的です。
各テナントが独立したユーザーデータを持ち、それに対して効率的にアクセスしたい場合に適しています。

* 主キー (Partition Key): TenantId
* ソートキー (Sort Key): UserId

例:
|TenantId|UserId|UserName|Email|
|--|--|--|--|
|Tenant1|U001|Alice|alice@example.com|
|Tenant1|U002|Bob|bob@example.com|
|Tenant2|U001|Charlie|charlie@example.com|


#### ▶︎利点
<dl>
<dt>データ隔離の明確さ</dt>
<dd>TenantIdをPartition Keyとして使うことで、各テナントのデータが自然に隔離されます。これにより、セキュリティの実装が容易になります。</dd>
<dt>効率的なクエリ</dt>
<dd>TenantIdを基にしたクエリによって、特定のテナントのユーザー情報に対する読み書きが効率的に行えます。</dd>
</dl>

### シナリオ2: テナント > ユーザー > 投稿のデータ構造

このシナリオは、単純なマルチテナントのユーザー管理に加え、ユーザーの投稿を例に各ユーザーが独立した投稿データを持ち、それに対して効率的にアクセスしたい場合に適しています。

* 主キー (Partition Key): TenantId
* ソートキー (Sort Key): UserId_PostId

例:

TenantId|UserId_PostId|UserName|PostContent
--|--|--|--
Tenant1|U001#P001|Alice|Hello, World!
Tenant1|U001#P002|Alice|How are you?
Tenant1|U002#P001|Bob|Hi, there!
Tenant2|U001#P001|Charlie|Good morning!


#### ▶︎利点
<dl>
<dt>階層的なデータ構造の効率的な管理</dt>
<dd>TenantId、UserId、およびPostIdの組み合わせを用いることで、テナント、ユーザー、投稿の階層的な関係を効率的に表現できます。</dd>
<dt>柔軟なクエリ実行</dt>
<dd>Sort Keyを利用することで、特定のテナントの特定のユーザーの投稿に効率的にアクセスできます。また、範囲クエリを使って、特定のユーザーの特定の期間の投稿を一括で取得することも可能です。</dd>
</dl>


#### UserId_PostIdが{UserId}#{PostId}の形式の理由

この形式にする理由は、一つのパーティション（テナント）内でユーザーごとに投稿を効率的にクエリすることができるからです。

以下は、特定のテナントの特定のユーザーの全投稿をクエリするコード例です。

```
Copy code
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

const tenantId = 'Tenant1';
const userId = 'U001';

const params = {
    TableName: 'YourTableName',
    KeyConditionExpression: 'TenantId = :tenantId and begins_with(UserId_PostId, :userId)',
    ExpressionAttributeValues: {
        ':tenantId': tenantId,
        ':userId': `${userId}#`
    }
};

dynamodb.query(params, function(err, data) {
    if (err) {
        console.error('Error:', JSON.stringify(err, null, 2));
    } else {
        console.log('Query succeeded:', JSON.stringify(data, null, 2));
    }
});

```
このクエリは、TenantIdが'Tenant1'であり、ソートキーが'U001#'で始まるすべての項目（この例ではユーザー'U001'のすべての投稿）を効率的に取得します。


## セキュリティの強化: Fine-Grained Access Control (FGAC)
マルチテナントアプリケーションでは、セキュリティとデータの隔離が重要です。Fine-Grained Access Control (FGAC) は、DynamoDBの特定の項目または属性に対して実行できる操作を詳細に制御するための仕組みです。今回のようなマルチテナントサービスの場合には、ユーザーが属しているテナント以外のデータへのアクセスはできないようにします。

### FGACの概要
FGACは、AWS IAMポリシーのCondition要素を使用して実装されます。これにより、特定のユーザーやロールが、特定のDynamoDB項目のみにアクセスできるように制限できます。

### Amazon Cognitoとは
ユーザー認証と権限管理を提供するAWSのサービスです。この例では、Cognitoを使用してユーザーを認証し、そのユーザーの属性（ここではテナントID）を取得します。このテナントIDを基に、DynamoDBのデータへのアクセス権限を制御します。

### FGACの設定例
以下の例では、各テナントが自身のデータのみにアクセスできるように、IAMポリシーで制限を設けています。この例では、認証ユーザーのTenantIDがDynamoDBのパーティションキーと一致する場合のみ、データにアクセスできるようにしています。

#### ▶︎ポリシーの作成
まず、各テナントが自身のテナントIDに関連するデータにのみアクセスできるようにIAMポリシーを設定します。

この場合、Cognitoから取得したユーザー情報（例えば、カスタム属性として設定されたcustom:tenant_id）を使って、IAMポリシーでDynamoDBのアイテムへのアクセスを制限します。

以下はそのような設定の例です：


```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/TABLE_NAME",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:custom:tenant_id}"]
        }
      }
    }
  ]
}

```

このIAMポリシーを解説します。

* ユーザーに対して、特定のDynamoDBテーブル（TABLE_NAME）に対する GetItem, PutItem, UpdateItem, DeleteItem のアクションを許可しています。
* Conditionエレメント内で、dynamodb:LeadingKeysを使用しています。
これは、このポリシーが許可されるのは、パーティションキーがCognitoから取得したカスタム属性 custom:tenant_id と一致するアイテムの場合だけ、と指定しています。


この設定により、各ユーザー（それぞれが特定のテナントに所属していると想定）は、そのユーザーのcustom:tenant_idに対応するデータにのみアクセスが可能となります。これは重要なセキュリティの問題解決策であり、DynamoDBとIAMポリシー、そしてAmazon Cognitoを組み合わせた強力なセキュリティの実装を実現します。

:::note warn
この例ではcustom:tenant_idを使っていますが、これはCognitoのユーザープールで定義されたカスタム属性の一例です。実際には、独自の属性名に変更する必要があります。
:::

## おわりに

本記事では、マルチテナントサービスのためのDynamoDB設計について考察してみました。

シングルテーブルでのマルチテナント設計でのセキュリティ対策をどうするかを色々調べた結果、FGACの採用が良さそうでしたので具体例を交えて解説しました。

間違った点などがありましたらコメントで教えていただけると幸いです。