マルチテナントパターン
このガイドでは、適切なデータ分離、共有リソース、テナント間操作を備えたマルチテナントアプリケーションの実装パターンについて説明します。
このガイドを使用するタイミング
以下の場合にこのガイドを使用してください:
- テナント(顧客/組織)間でデータを分離する
- すべてのテナントで共通データを共有する
- ユーザーが複数のテナントに所属できるようにする
- テナント固有の設定を実装する
- テナント間でデータを同期する
マルチテナントアーキテクチャ
┌─────────────────────────── ──────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Tenant A │ │ Tenant B │ │ Common │ │
│ │ PK: X#A │ │ PK: X#B │ │ PK: X#common│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──── ─────────────────────────────────────────────────────┐ │
│ │ DynamoDB Table │ │
│ │ PK: ENTITY#tenantCode | SK: identifier │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
テナントコンテキスト
テナントコンテキストの抽出
呼び出しコンテキストからテナント情報を抽出するヘルパーを作成します:
// helpers/context.ts
import { IInvoke, getUserContext } from '@mbc-cqrs-serverless/core';
export interface CustomUserContext {
tenantCode: string;
userCode: string;
userId: string;
email?: string;
role?: string;
}
/**
* Get custom user context from invoke context
* 呼び出しコンテキストからカスタムユーザーコンテキストを取得
*/
export function getCustomUserContext(invokeContext: IInvoke): CustomUserContext {
const userContext = getUserContext(invokeContext);
return {
tenantCode: userContext.tenantCode || DEFAULT_TENANT_CODE,
userCode: userContext.userCode || '',
userId: userContext.userId || '',
email: userContext.email,
role: userContext['custom:role'],
};
}
/**
* Default tenant code for shared data
* 共有データ用のデフォルトテナントコード
*/
export const DEFAULT_TENANT_CODE = 'common';
/**
* Check if user has access to tenant
* ユーザーがテナントへのアクセス権を持っているか確認
*/
export function hasTenanctAccess(
userContext: CustomUserContext,
targetTenantCode: string,
): boolean {
// System admin can access all tenants / システム管理者は全テナントにアクセス可能
if (userContext.role === 'SYSTEM_ADMIN') {
return true;
}
// User can only access their own tenant / ユーザーは自分のテナントのみアクセス可能
return userContext.tenantCode === targetTenantCode;
}
テナントガード
テナントアクセスを強制するガードを実装します:
// guards/tenant.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { getCustomUserContext, hasTenanctAccess } from '../helpers/context';
@Injectable()
export class TenantGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const invokeContext = request.invokeContext;
const userContext = getCustomUserContext(invokeContext);
// Get target tenant from path or body
const targetTenant = request.params.tenantCode ||
request.body?.tenantCode ||
this.extractTenantFromPk(request.body?.pk);
if (!targetTenant) {
return true; // No tenant specified, will use user's tenant
}
if (!hasTenanctAccess(userContext, targetTenant)) {
throw new ForbiddenException(
`Access denied to tenant: ${targetTenant}`,
);
}
return true;
}
private extractTenantFromPk(pk: string | undefined): string | undefined {
if (!pk) return undefined;
const parts = pk.split('#');
return parts.length >= 2 ? parts[1] : undefined;
}
}
データ分離パターン
パターン1: パーティションキーにテナントを含める
完全な分離のためにパーティションキーにテナントコードを含めます:
// Standard tenant isolation pattern
// 標準的なテナント分離パターン
const PRODUCT_PK_PREFIX = 'PRODUCT';
function generateProductPk(tenantCode: string): string {
return `${PRODUCT_PK_PREFIX}${KEY_SEPARATOR}${tenantCode}`;
}
// Example keys:
// PK: PRODUCT#tenant-a
// SK: 01HX7MBJK3V9WQBZ7XNDK5ZT2M
// Query all products for a tenant
async function listProductsByTenant(tenantCode: string) {
const pk = generateProductPk(tenantCode);
return dataService.listItemsByPk(pk);
}
パターン2: 共有データ用の共通テナント
すべてのテナントで共有されるデータには共通のテナントコードを使用します:
// Shared data pattern (master data, configurations)
// 共有データパターン(マスターデータ、設定)
const COMMON_TENANT = 'common';
// System-wide settings
const settingsPk = `SETTINGS${KEY_SEPARATOR}${COMMON_TENANT}`;
// User data (users can belong to multiple tenants)
const userPk = `USER${KEY_SEPARATOR}${COMMON_TENANT}`;
// Example: Get system-wide email templates
async function getEmailTemplates() {
return dataService.listItemsByPk(`TEMPLATE${KEY_SEPARATOR}${COMMON_TENANT}`);
}