このブログ投稿シリーズでは、 AWSでマルチテナント サービスを構築するためのベスト プラクティスについて説明したいと思います。マルチテナントサービスの構築方法に関する既存の文献は、通常、数百の顧客を持つ SaaS アプリケーションを対象としています (例: AWS サーバーレス サービスを使用したマルチテナント SaaS ソリューションの構築)。
このシリーズの主な根拠は、すべて AWS アカウントにデプロイされるクライアント数が少ないユースケース向けのマルチテナント サービスの構築に焦点を当てることです。通常、これは内部使用のためにマルチテナント サービスを構築するシナリオに当てはまります。
一連のブログ投稿を、サービス間の統合の種類 (同期、非同期、バッチ統合) ごとに 3 つの部分に分割します。
パート 1 では、API Gateway と AppSync という 2 つの AWS サービスのマルチテナント アーキテクチャについて説明します。記事全体を通じて、Typescript と AWS CDK でこの記事用に構築されたサンプル アプリケーション アプリのコードを参照します: https://github.com/filletofish/aws-cdk-multi-tenant-api-example/tree/main 。
内部サービスのマルチテナント
1.1.テナントの分離
1.2.マルチテナント監視
1.3.スケーリング
内部サービスのマルチテナント
2.1.テナントの分離 - アクセス制御
2.2 テナントの分離 - 騒音の多い隣人の問題
2.3 マルチテナント監視
2.4 メトリクス、アラーム、ダッシュボード
2.5 API クライアントのオンボーディングとオフボーディング
AWS AppSync によるマルチテナンシー
結論
マルチテナンシーとは、ソフトウェアの単一インスタンスで複数の顧客またはテナントにサービスを提供するソフトウェアの機能です。
複数のチームがサービスAPIを呼び出すことを許可すると、サービスはマルチテナントになります。マルチテナント アーキテクチャでは、テナントの分離、テナント レベルの監視、スケーリングなど、サービスがさらに複雑になります。
一般に、テナントの分離では、テナントが別のテナントのリソースにアクセスできないようにすることで、セキュリティの問題に対処します。また、1 つのテナントによって引き起こされた障害がサービスの他のテナントに影響を与えないように、テナントの分離が実装されています。これは、騒々しい隣人問題とも呼ばれることがよくあります。詳細については、テナント分離戦略に関する AWS ホワイトペーパーhttps://d1.awsstatic.com/whitepapers/saas-tenant-isolation-strategies.pdfをご覧ください。
複数のテナントがインフラストラクチャ リソースの共有を開始したら、各テナントがシステムをどのように使用するかを監視する必要があります。これは通常、テナント名または識別子がログ、メトリクス、ダッシュボードに存在する必要があることを意味します。マルチテナント監視は、次のようないくつかの理由で役立ちます。
マルチテナント サービスは、シングルテナント サービスよりもスケーリングの課題にさらされる可能性が高くなります。ただし、スケーラビリティは大きなトピックなので、このブログ投稿では取り上げません。
AWS でREST 、HTTP、または WebSocket API を使用して AWS Web サービスを構築している場合は、API Gateway を使用している可能性が高くなります。
AWS では、サービスのリソースとデータを分離し、コスト管理を容易にし、テスト環境と本番環境を分離するために、各サービスを独自の AWS アカウントにデプロイすることをお勧めします (詳細については、 AWS ホワイトペーパー「複数のアカウントを使用した AWS 環境の構成」を参照してください)。
会社のサービスが AWS にデプロイされている場合、API ゲートウェイへのアクセスを管理するための最も明白なソリューションは AWS IAM です。 AWS Cognito は、マルチテナント API へのアクセスを管理するためのもう 1 つのオプションです ( 「API Gateway を使用した大規模な階層型マルチテナント REST API のスロットル」 、 「Amazon Cognito の事例」および「Amazon Cognito に対する事例」を参照)。
AWS IAM と AWS Cognito の比較については、別途詳しく説明する価値があります。ただし、この記事では、会社のサービスが AWS にある場合にアクセスを管理する最も簡単な方法である AWS IAM を使用することにします。
API ゲートウェイ メソッドの AWS IAM 認証を有効にすると ( 「CFN」を参照)、このメソッドのすべての API リクエストは、API ゲートウェイの呼び出しを許可された IAM ID の認証情報で署名される必要があります。
デフォルトでは、AWS アカウント間のアクセスは許可されません。たとえば、別の AWS アカウントの認証情報を使用して API Gateway を呼び出すと失敗します。顧客を API に統合するには、クロスアカウント アクセスを設定する必要があります。 API Gateway へのクロスアカウント アクセスを許可するには、リソースベースの認可 (API Gateway HTTP API では使用できません) と ID ベースの認可 (詳細はhttps://repost.aws/knowledge-center/を参照) の 2 つの方法を使用できます。アクセス API ゲートウェイ アカウント):
リソースベースの認証を使用したクライアントのオンボーディング。リソースベースのアクセスの場合、API ゲートウェイ リソース ポリシーを更新し、クライアントの AWS アカウントを追加する必要があります。この方法の主な欠点は、リソース ポリシーを更新した後、変更を有効にするために API Gateway ステージを再デプロイする必要があることです (AWS ドキュメント[1]および[2]を参照)。ただし、CDK を使用すると、新しいステージのデプロイを自動化できます ( 「API Gateway の AWS CDK ドキュメント」を参照)。もう 1 つの欠点は、リソース ポリシーの最大長の制限です。
ID ベースの認証を使用したクライアントのオンボーディング。 ID ベースのアクセス制御の場合、クライアントの IAM ロールを作成し、ロールのリソース ポリシー (信頼関係) を更新することでクライアントがそのロールを引き受けられるようにする必要があります。 IAM ユーザーを使用することもできますが、セキュリティの観点からは IAM ロールの方が優れています。ロールでは一時的な認証情報を使用した認証が可能であり、IAM ユーザー認証情報を保存する必要はありません。アカウントごとに 1,000 ロールという制限がありますが、この制限は調整可能です。さらに、API へのクロスアカウント アクセスを取得するためのロールベースの方法のもう 1 つの欠点は、新しい API クライアントごとに IAM ロールを作成する必要があることです。ただし、ロール管理は CDK を使用して自動化できます ( 提供されている 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 ゲートウェイ制限は 10,000 TPS ( API ゲートウェイの割り当てと制限) です。ただし、ダウンストリームの依存関係により、サービスではより低い TPS 制限が必要になる場合があります。システム全体の可用性に影響を与える、単一のテナントからの API リクエストの過負荷を回避するには、テナントごとの API レート制限 (「スロットル」または「アドミッション コントロール」とも呼ばれます) を実装する必要があります。
API Gateway API 使用プランとキーを使用して、各クライアントの制限を個別に設定できます (詳細については、AWS ドキュメント [1]、[2]、および [3] を参照してください)。
API Gateway には 2 種類のログがあります。
API ゲートウェイ実行ログ: リクエストまたはレスポンスのパラメータ値、必要な API キー、使用量プランが有効かどうかなどのデータが含まれます。デフォルトでは有効になっていませんが、構成することができます。
API ゲートウェイのアクセス ログ機能: API にアクセスしたユーザー、アクセス方法、アクセスされたエンドポイント、および API 呼び出しの結果をログに記録できます。ログ形式を指定し、コンテキスト変数を使用してログ内容を選択できます (CDK のドキュメントを参照)。
API クライアントのリクエストを監視するには、アクセス ログを有効にすることをお勧めします。少なくとも、呼び出し元の AWS IAM ARN ( $context.identity.userArn
)、リクエスト パス ( $context.path
)、サービス応答ステータス コード$context.status
、および API 呼び出しレイテンシー ( $context.responseLatency
) をログに記録できます。 。
個人的には、AWS IAM 認証とコンピューティングとしての Lambda 機能を備えたサービスの場合、次の API ゲートウェイ アクセス ログ設定が役立つことがわかりました。
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);
ログ記録を有効にすると、CloudWatch Insights を使用して、選択した API クライアントからの最新の呼び出しを次のように簡単に取得できます。
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.クライアントごとのメトリックを公開するためのメトリック ログ フィルターの使用
CloudWatch メトリクス ログ フィルタ (ドキュメントを参照) を使用すると、カスタム フィルタを提供し、API ゲートウェイ アクセス ログからメトリクス値を抽出できます (以下の例を参照)。メトリック ログ フィルターを使用すると、ログからカスタム メトリック ディメンションの値を抽出することもできます。マルチテナント監視の場合、ディメンション Client は呼び出し元の IAM ARN になる可能性があります。
メトリック ログ フィルターの主な利点は、(1) 管理するコンピューティングが不要 (2) シンプルで安価であることです。ただし、データの変更 (IAM ARN の代わりに、より読みやすいクライアント名を設定するなど) を行うことはできず、単一ログ グループあたりのメトリック フィルターの数は 100 個までという制限があります (ドキュメント)。
ディメンション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',},}); });
提供されているサンプル CDK アプリケーションで、4xx、5xx エラー、およびレイテンシー メトリックのすべてのメトリック フィルターを参照してください。
2.4.2. Lambda 関数を使用してクライアントごとのメトリクスを公開する
別のオプションは、ログを解析し、メトリクスを抽出して公開する Lambda 関数を作成することです。これにより、不明なクライアントをフィルタリングしたり、userArn からクライアント名を抽出したりするなど、より多くのカスタム作業を行うことができます。
わずか数行の CDK コードを使用して、Lambda 関数を API ゲートウェイ アクセス ログにサブスクライブします。
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(), });
コードの完全な例とLog Processor Lambda Functionの実装を参照してください。
クライアントごとに分割された API Gateway メトリクスの公開を開始すると、クライアントごとに CloudWatch ダッシュボードと CloudWatch アラームを個別に作成できるようになります。
CDK アプリは、クライアント名、その AWS アカウント、要求された TPS 制限、その他のメタデータを含む構成を保存するための簡単なソリューションとなる可能性があります。新しい API クライアントをオンボードするには、コードで管理されている構成にそれを追加する必要があります。
interface ApiClientConfig { name: string; awsAccounts: string[]; rateLimit: number; burstLimit: number; } const apiClients: ApiClientConfig[] = [ { name: 'payment-service', awsAccounts: ['111122223333','444455556666'], rateLimit: 10, burstLimit: 2, }, { name: 'order-service', awsAccounts: ['777788889999'], rateLimit: 1, burstLimit: 1, }, ];
この設定を使用して、CDK アプリは IAM ロール、API ゲートウェイ使用キーを作成し、アクセス ログを解析する Lambda 関数にクライアントの名前を渡すことができます (サンプル アプリケーション コードを参照)。
サービスにGraphQL API がある場合は、おそらく AppSync を使用するでしょう。 API Gateway と同様に、IAM 認証を使用して AppSync リクエストを承認できます。 AppSync にはリソース ポリシーがないため ( 「GH 問題」を参照)、AppSync API へのアクセス制御を設定するにはロールベースの承認のみを使用できます。 API Gateway と同様に、サービスの新しいテナントごとに個別の IAM ロールを作成します。
残念ながら、AppSync では、テナントの分離と監視に必要なクライアントごとの調整のサポートが制限されています。 WAF を使用して AppSync の TPS 制限を設定することはできますが、サービス テナントを分離するためにクライアントごとに個別の制限を作成することはできません。同様に、AppSync は API Gateway のようにアクセス ログを提供しません。
解決? API Gateway をプロキシとして AppSync に追加し、上記の API Gateway 機能をすべて使用して、テナントの分離や監視などのマルチテナント要件を実装できます。さらに、AppSync にはまだ存在しない Lambda オーソライザー、カスタム ドメイン、API ライフサイクル管理などの他の API ゲートウェイ機能を使用できます。欠点は、リクエストのレイテンシーが若干増加することです。
それでおしまい。ご質問やアイデアがございましたら、コメント欄でお知らせいただくか、直接私にご連絡ください。このシリーズの次のパートでは、AWS Event Bridge および AWS SQS / SNS との非同期内部統合のベストプラクティスを確認します。
AWS 上にマルチテナント サービスを構築するというトピックを深く掘り下げたい場合は、次のリソースが役立つことがわかりました。