マルチテナント SaaS のノイジーネイバー対策を想定して Azure API Management のポリシーでテナントコンテキストをカウンターキーにしてみる

2024.01.04

いわさです。

先日、Azure と AWS の SaaS テナント分離について紹介しました。 雑に分けると複数テナントでリソースを共用するか分離するかの考えがあるのですが、その方針を決める過程で「ノイジーネイバー」と呼ばれる問題について考える必要があります。

ノイジーネイバー問題は Azure に関わらずマルチテナント SaaS を設計・構築する際には必ず意識しなければならないポイントです。
複数のテナントで同一のシステムを利用する場合、特定のテナントがシステムリソースを逼迫するような使い方をしている場合に、その他のテナントが通常の使い方をしていても影響を受ける場合があります。

引用元:Noisy Neighbor antipattern - Azure Architecture Center | Microsoft Learn

この問題を回避するためにテナント間でリソース分離を行うシングルテナント型を採用すれば、ノイジーネイバーのリスクをかなり低くすることが出来ます。
一方で複数テナントでリソースを共用するマルチテナント型はノイジーネイバーの問題が起きやすいのですが、どういった対策があるでしょうか。

ノイジーネイバー対策

完璧に防ぐことは出来ないのですが、いくつかの対策を組み合わせることで軽減することが可能です。
次のドキュメントに詳しく書いてあるのでぜひ一読してください。

ざくっとまとめる次のようなアプローチがあります。

  • ノイジーネイバーの検出:テナントごとのリソース使用量をトレース出来るようにしておく
  • リソース効率化:非同期化が可能なものはオフピーク時に実行したり、高負荷が発生しないようにアプリケーション上の作りを行う(タイムアウトや処理件数の上限設定)
  • テナントに制限:スロットリングやレートリミットを導入

監視に関してはテナントコンテキストを実装する必要があって、これはこれでおもしろいトピックなのでまた別の機会に取り上げたいと思います。

本日は 3 つめののスロットリング&レートリミットについて検証してみたので紹介します。
このあたりを実装することで、要は 100 あるキャパシティに対して各テナント 10 までしかバーストさせないとか、そういった制限を設定します。
制限というとネガティブな感じもしますが、SaaS 戦略上はよりリミットが緩和されてたり分離環境を使えるような上位プランへの訴求効果になったりもします。

Azure API Management のポリシーで実現してみる

レートリミットの実装方法は様々です。
最近だと ASP.NET Core のミドルウェアでも実装出来たり、キューサービスと組み合わせて実装する場合もありますが、今回は Azure API Management のポリシー機能でこのあたりが簡単にコントロール出来るので使ってみたいと思います。

API Management 構築

Azure API Management を作成します。
今回は上記ドキュメントに記載されているようにrate-limit-by-keyあるいはquota-by-keyを使います。
これらは Consumption プランだと使うことが出来ないので今回は Developer プランで作成しました。

今回はポリシーの評価を行いたいので、検証用の API 自体の手順は割愛しますが以下をベースに作成します。
モックだとポリシーを設定しても無効(非活性)になるのでバックエンドサービスを何かしら設定しましょう。ここでは Azure Functions を指定しました。

ポリシーを作成する

API のベースが作成出来たら、Inbound processing の Policies でポリシーの追加を行います。

Limit call rateがレートリミットでSet usage quota by keyが使用量クォータです。
どちらも似たような挙動をさせれるのですが、一般的にレートリミットは短期間の制限、使用量クォータは長期間の制限という使い方が想定されているようです。
制限に達すると前者は429 TOo Many Requestsエラーとなり、後者は403 Forbiddenエラーとなります。

ちなみに、ここで言っているキーなのですが、AWS を使っていた型だと「API Gateway の API キーのことかな?」と思うのではないでしょうか。
次のようにポリシーの中でCounter keyを設定出来るのですが、キーには API subscription や IP address が指定でき、さらにカスタムという形でキーのパスを任意で指定することが出来ます。

API subscription は Amazon API Gateway でいう API キーみたいなものです。
さらに IP ベースや任意のカスタムキーが設定出来るのはかなり自由度が高くて驚きました。

今回ですが、先日 Azure AD B2C テナントにテナント ID のカスタム属性を追加しているので、こちらの JWT を使ってテナント ID をキーにしてみます。
これが出来ると、テナントごとにリミットを設定出来るというわけです。
今回は認可処理までは特に行いません。Authorization ヘッダーにトークンを渡して、カスタムポリシーでキーを取得します。

ここでは JWT のextension_tenant_idというクレームにテナント ID が設定される前提とします。
次のページはポリシー式のリファレンスです。

例だとSubjectにアクセスしていますが、今回アクセスしたいカスタム属性は JWT オブジェクトのプロパティには存在していません。 少し見てみるとstring Jwt.Claims.GetValueOrDefault(claimName: string, defaultValue: string)を使うとクレーム名を指定してアクセス出来る感じがしますね。これを使ってみます。

今回は次のようなポリシーを作成してみました。
カスタム属性はtenant_idという名称で作成しましたが、Azure AD B2C のクレーム上はextension_tenant_idとして出力されるのでこちらを指定しています。

<policies>
    <inbound>
        <base />
        <mock-response status-code="200" content-type="application/json" />
        <rate-limit-by-key calls="10" renewal-period="300" counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Claims.GetValueOrDefault("extension_tenant_id","default"))" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

テスト

Azure AD B2C テナントにはユーザーを 3 名作成しました。

ユーザー テナント
user1 tenant1
user2 tenant1
user3 tenant2

各テナント 300 秒間に 10 回までしかアクセス出来ないようにします。
なので user1 が 10 回リクエストを送信すると、tenant1 の user1 と user2 は一定期間はリクエストに失敗するはずです。
一方でその間にも別のテナントである tenant2 の user3 はアクセス出来るはずです。

今回リクエストテストは API Management のテスト機能を使いました。
各ユーザーの JWT は事前に払い出しておき、テストパネルから Authorization ヘッダーに設定します。

試しに user1 でリクエストを送信してみると、次のようにポリシーが動作している様子が確認出来ました。
API Management のトレース実行機能を使ってるのですが、これめちゃくちゃ良いな...!

rate-limit-by-keyのところでtenant1が抽出されており、1 回カウントされている様子がわかりますね。良さそうだ。

続いて同じようにリクエストを 10 回繰り返すと、次のように429 Too Many Requestsが発生しました。
レスポンスボディのエラーメッセージにはレートリミットに達したという内容と、時間についても表示されています。

続いて同一テナントの user2 でアクセスしてみるとこちらも期待どおりエラーとなりました。
同一テナント内でレートが共有されています。

一方で、別のテナントの user3 でリクエストを送信すると、こちらは正常に処理されました。
良いですね!

さいごに

本日はマルチテナント SaaS のノイジーネイバー対策を想定して Azure API Management のポリシーを使ってみました。

Azure API Management のポリシー機能すごいです。カスタムキー設定出来るの便利ですね。色々なアプローチ出来そう。
Azure Functions あたりでテナントコンテキスト識別してサブスクリプション引き渡すとかしないといけないのかな?とか最初思ったのですが、ポリシー一発で実装出来るとは。

ちなみにアーキテクチャセンターの App Service & Functions では、前段に API Management を配置して、認証やルーティングなどマルチテナントソリューションに有効な機能を使いましょうと記載されています。こちらには記述がなかったのですが、今回試したポリシーによるレートリミットやスロットリングも併せて使えそうですね。