このブログ投稿シリーズでは、 AWSでマルチテナント サービスを構築するためのベスト プラクティスについて説明したいと思います。マルチテナントサービスの構築方法に関する既存の文献は、通常、数百の顧客を持つ SaaS アプリケーションを対象としています (例: )。
一連のブログ投稿を、サービス間の統合の種類 (同期、非同期、バッチ統合) ごとに 3 つの部分に分割します。
内部サービスのマルチテナント
1.1.テナントの分離1.2.マルチテナント監視1.3.スケーリング内部サービスのマルチテナント
2.1.テナントの分離 - アクセス制御2.2 テナントの分離 - 騒音の多い隣人の問題2.3 マルチテナント監視2.4 メトリクス、アラーム、ダッシュボード2.5 API クライアントのオンボーディングとオフボーディングAWS AppSync によるマルチテナンシー
結論
マルチテナンシーとは、ソフトウェアの単一インスタンスで複数の顧客またはテナントにサービスを提供するソフトウェアの機能です。
複数のチームがサービスAPIを呼び出すことを許可すると、サービスはマルチテナントになります。マルチテナント アーキテクチャでは、テナントの分離、テナント レベルの監視、スケーリングなど、サービスがさらに複雑になります。
AWS でREST 、HTTP、または WebSocket API を使用して AWS Web サービスを構築している場合は、API Gateway を使用している可能性が高くなります。
リソースベースの認証を使用したクライアントのオンボーディング。リソースベースのアクセスの場合、API ゲートウェイ リソース ポリシーを更新し、クライアントの AWS アカウントを追加する必要があります。この方法の主な欠点は、リソース ポリシーを更新した後、変更を有効にするために API Gateway ステージを再デプロイする必要があることです (AWS ドキュメントおよびを参照)。ただし、CDK を使用すると、新しいステージのデプロイを自動化できます ( を参照)。もう 1 つの欠点は、リソース ポリシーの最大長の制限です。
ID ベースの認証を使用したクライアントのオンボーディング。 ID ベースのアクセス制御の場合、クライアントの IAM ロールを作成し、ロールのリソース ポリシー (信頼関係) を更新することでクライアントがそのロールを引き受けられるようにする必要があります。 IAM ユーザーを使用することもできますが、セキュリティの観点からは IAM ロールの方が優れています。ロールでは一時的な認証情報を使用した認証が可能であり、IAM ユーザー認証情報を保存する必要はありません。アカウントごとに 1,000 ロールという制限がありますが、この制限は調整可能です。さらに、API へのクロスアカウント アクセスを取得するためのロールベースの方法のもう 1 つの欠点は、新しい API クライアントごとに IAM ロールを作成する必要があることです。ただし、ロール管理は CDK を使用して自動化できます ( を参照)。
AWS IAM 認証では、API ゲートウェイへのアクセスのみを制御できます (IAM ポリシーを使用して、どの AWS アカウントがどの API ゲートウェイ エンドポイントを呼び出すことができるかを指定できます)。データおよびサービスのその他の基盤となるリソースへのアクセス制御を実装するのはあなたの責任です。サービス内では、API ゲートウェイ リクエストで渡される呼び出し元の AWS IAM ARN を使用して、さらにアクセス制御を行うことができます。
export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => { // IAM Principal ARN of the api caller const callerArn = event.requestContext.identity.userArn!; // .. business logic based on caller return { statusCode: 200, body: JSON.stringify({ message: `Received API Call from ${callerArn}`, }) }; };
API Gateway には 2 種類のログがあります。
API クライアントのリクエストを監視するには、アクセス ログを有効にすることをお勧めします。少なくとも、呼び出し元の AWS IAM ARN ( $context.identity.userArn
)、リクエスト パス ( $context.path
)、サービス応答ステータス コード$context.status
、および API 呼び出しレイテンシー ( $context.responseLatency
) をログに記録できます。 。
const formatObject = { requestId: '$context.requestId', extendedRequestId: '$context.extendedRequestId', apiId: '$context.apiId', resourceId: '$context.resourceId', domainName: '$context.domainName', stage: '$context.stage', path: '$context.path', resourcePath: '$context.resourcePath', httpMethod: '$context.httpMethod', protocol: '$context.protocol', accountId: '$context.identity.accountId', sourceIp: '$context.identity.sourceIp', user: '$context.identity.user', userAgent: '$context.identity.userAgent', userArn: '$context.identity.userArn', caller: '$context.identity.caller', cognitoIdentityId: '$context.identity.cognitoIdentityId', status: '$context.status', integration: { // The status code returned from an integration. For Lambda proxy integrations, this is the status code that your Lambda function code returns. status: '$context.integration.status', // For Lambda proxy integration, the status code returned from AWS Lambda, not from the backend Lambda function code. integrationStatus: '$context.integration.integrationStatus', // The error message returned from an integration // A string that contains an integration error message. error: '$context.integration.error', latency: '$context.integration.latency', }, error: { responseType: '$context.error.responseType', message: '$context.error.message', }, requestTime: '$context.requestTime', responseLength: '$context.responseLength', responseLatency: '$context.responseLatency', }; const accessLogFormatString = JSON.stringify(formatObject); const accessLogFormat = apigw.AccessLogFormat.custom(accessLogFormatString);
fields @timestamp, path, status, responseLatency, userArn | sort @timestamp desc | filter userArn like 'payment-service' | limit 20
API Gateway によってデフォルトでサポートされている CloudWatch メトリクスは、すべてのリクエストに対して集約されます。ただし、API Gateway のアクセス ログを解析して、クライアント名の追加ディメンションを使用してカスタム CloudWatch メトリクスを公開し、クライアント (テナント) の API の使用状況を監視することができます。少なくとも、クライアントごとの CloudWatch メトリクス Count、4xx、5xx、 Dimension=${Client}
で分割された Latency を公開することをお勧めします。ステータス コードや API パスなどのディメンションを追加することもできます。
2.4.1.クライアントごとのメトリックを公開するためのメトリック ログ フィルターの使用
ディメンションClient
とPath
を使用してCount
を公開するための CloudWatch メトリクスログフィルターの例
new logs.MetricFilter(this, 'MultiTenantApiCountMetricFilter', { logGroup: accessLogsGroup, filterPattern: logs.FilterPattern.exists('$.userArn'), metricNamespace: metricNamespace, metricName: 'Count', metricValue: '1', unit: cloudwatch.Unit.COUNT, dimensions: { client: '$.userArn', method: '$.httpMethod', path: '$.path',},}); });
2.4.2. Lambda 関数を使用してクライアントごとのメトリクスを公開する
別のオプションは、ログを解密し、メトリクスを提取して公開する Lambda 関数を作为することです。これにより、不知道なクライアントをフィルタリングしたり、userArn からクライアント名を提取したりするなど、より多くのカスタム作業を行うことができます。
const logProcessingFunction = new lambda.NodejsFunction( this, 'log-processor-function', { functionName: 'multi-tenant-api-log-processor-function', } ); new logs.SubscriptionFilter(this, 'MultiTenantApiLogSubscriptionFilter', { logGroup: accessLogsGroup, destination: new logsd.LambdaDestination(logProcessingFunction), filterPattern: logs.FilterPattern.allEvents(), });
interface ApiClientConfig { name: string; awsAccounts: string[]; rateLimit: number; burstLimit: number; } const apiClients: ApiClientConfig[] = [ { name: 'payment-service', awsAccounts: ['3','444455556666'], rateLimit: 10, burstLimit: 2, }, { name: 'order-service', awsAccounts: ['777788889999'], rateLimit: 1, burstLimit: 1, }, ];
サービスにGraphQL API がある場合は、おそらく AppSync を使用するでしょう。 API Gateway と同様に、IAM 認証を使用して AppSync リクエストを承認できます。 AppSync にはリソース ポリシーがないため ( を参照)、AppSync API へのアクセス制御を設定するにはロールベースの承認のみを使用できます。 API Gateway と同様に、サービスの新しいテナントごとに個別の IAM ロールを作成します。