Azure API Management を使って Azure OpenAI Service の負荷分散構成を作る

※本記事は2024年7月末時点の情報を元に作成されています。
※2024年7月時点でPreview版として提供されている[こちら](https://techcommunity.microsoft.com/t5/fasttrack-for-azure/using-azure-api-management-circuit-breaker-and-load-balancing/ba-p/4041003)の機能を使った解説ではありませんので、予めご了承ください。今後はこちらのPreview版で提供されている機能を使った実装が主流になっていく可能性があります。

# はじめに
Azure OpenAI Serviceを使ったサービス開発をされている方が増えてきているかと思いますが、クォータの制限に悩まされている方も多いのではないかと思います。[こちら](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/quotas-limits) の公式ドキュメントに記載の通りリージョンごとに上限値が設定されているため、この数値を超える・超えそうな場合には都度申請をしなければならないことに煩わしさを感じられている方も多いのではないかと思います。

クォータ問題の最もシンプルな解決方法は冗長化することです。Azureには様々なロードバランシング用のサービスが提供されていますが、本記事ではAzure API Managementを使用した負荷分散構成の作成方法についてまとめています。

# 前提条件
Azureの操作に必要なアカウントやサブスクリプション、権限などが適切に付与されていることが前提になります。また、本手順に沿って作業をする場合には予めAzure OpenAI Serviceリソースを2つ作成し、それぞれでモデルデプロイを完了しておいてください。

# 構成イメージ
以下の構成を作ることができます。

![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/648a35c8-78a2-47a0-d75c-1ec48deee51a.png)

# サービスのデプロイ
1から作成を進めることもできますが、マイクロソフト公式のGitHubリポジトリに自動でサービスを作成してくれるテンプレートが用意されているので、こちらを利用するのが簡単です。

リポジトリのURL: https://github.com/microsoft/AzureOpenAI-with-APIM/tree/main

リポジトリ内をスクロールダウンすると最初に表示される「Deploy to Azure」のボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/fb99f03f-38b7-8fcf-435a-490a02a1997e.png)

サインインをするとAzureポータルにリダイレクトされます。以下の事項を入力して「確認と作成」に進みます。(※Azure API Managementは様々なサービスが組み合わさって構成されているため、デプロイの完了には4~50分程度かかることがあります。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/8611d29a-1fa4-b0c6-2329-bd006a7ae5fa.png)

作成が正常に完了後、展開されたAzure API ManagementのBackendsに、設定した2つのAzure OpenAI Serviceが登録されていれば準備は完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/b022f583-dd40-9b71-86fd-9dfafe6de76a.png)


# 通信の確認
全てのリソースの作成が完了した時点で一度通信の確認をしてみましょう。Azure API Managementのエンドポイントをコールすることで、バックエンドに配置されたAzure OpenAI Serviceを呼び出すことができます。

- メソッド: POST
- ヘッダー: 
 Content-type: application/json
 api-key: API Managementの「サブスクリプション」メニューに記載された主キー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/8684c251-d06f-db4b-545a-1d6ae4b2b4ed.png)

```API:URI
https://[API Managementのリソース名].azure-api.net/deployments/[AOAI上で作成したモデル名]/chat/completions?api-version=2024-02-15-preview
```

```JSON:Body
{
    "messages": [
        {"role": "system", "content": "Assistant is a large language model trained by OpenAI."},
        {"role": "user", "content": "質問内容をここに記載"}
    ],
  "max_tokens": 800,
  "temperature": 0.7,
  "frequency_penalty": 0,
  "presence_penalty": 0,
  "top_p": 0.95,
  "stop": [
      "
"
  ]
}
```

Postman等を使用し200 OKが返ってくれば成功です。

# 負荷分散構成の設定
Azure API Managementにおいて負荷分散の構成を作る場合にはポリシーを設定する必要があります。「API」メニューを開くとAPI全体やメソッドごとに細かくポリシーの設定がかけられるようになっていますが、今回は検証としてAPI全体のBackendにポリシーを割り当ててみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/9c1c079d-ea7e-96c3-febf-7cf7e3e9eaf7.png)

「retry」と表示されたボタンをクリックするとポリシーをXMLで編集できる画面が表示されます。今回はランダムに振り分けるため、以下のようなポリシーを設定します。
(※こちらはあくまでもサンプルのため、実運用においては必ず検証をお願いいたします。)

```XML:Policy
<policies>
    <inbound>
        <base />
        <authentication-managed-identity resource="https://cognitiveservices.azure.com" />
        <!-- Apply rate limiting -->
        <set-variable name="urlId" value="@(new Random(context.RequestId.GetHashCode()).Next(1, 101))" />
        <choose>
            <when condition="@(context.Variables.GetValueOrDefault<int>("urlId") < 51)">
                <set-backend-service backend-id="aoai-primary-backend" />
            </when>
            <when condition="@(context.Variables.GetValueOrDefault<int>("urlId") > 50)">
                <set-backend-service backend-id="aoai-secondary-backend" />
            </when>
            <otherwise>
                <return-response>
                    <set-status code="500" reason="InternalServerError" />
                    <set-header name="Microsoft-Azure-Api-Management-Correlation-Id" exists-action="override">
                        <value>@{return Guid.NewGuid().ToString();}</value>
                    </set-header>
                    <set-body>A gateway-related error occurred while processing the request.</set-body>
                </return-response>
            </otherwise>
        </choose>
    </inbound>
    <backend>
        <retry condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)" count="5" interval="1" delta="1" max-interval="8" first-fast-retry="false">
            <!-- Failover logic below - uncomment to retry on secondary backend -->
            <choose>
                <when condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)">
                    <set-backend-service backend-id="aoai-secondary-backend" />
                </when>
            </choose>
            <forward-request buffer-request-body="true" />
        </retry>
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

設定できたら画面下部の「Save」をクリックするとポリシーが瞬時に適用されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/b50ab11b-e645-4a6f-3891-8bec492aac97.png)

# 試してみる
先ほどと同じREST APIのコールを何度か実行してみます。Azure Log Analyticsで分析してみると、以下のようにコールごとにバックエンドが切り替わっていることが確認できます。

```KQL:Kusto Sample
ApiManagementGatewayLogs
| where TimeGenerated > ago(1d)
| summarize count() by BackendUrl
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267543/ccf3e0ef-6364-460f-2eba-4ec3fb684aae.png)


(※Log Analyticsでクエリをかける場合には、予めAzure API Managementの「分析」タブで有効化をしておく必要があります。)

# さいごに
Azure API Managementを活用することでアプリ側にソースコードを記載しなくても簡単に負荷分散の仕組みをマネージしてくれるので、特にAzure OpenAI Serviceのクォータ上限値で悩まれている方の参考になりましたら幸いです。
(※今回ご紹介した手法は一例ですので実際に本番運用される場合には必ず検証をお願いいたします。)

## 参考文献
・[マイクロソフト公式リポジトリ](https://github.com/microsoft/AzureOpenAI-with-APIM/tree/main)
・[Azure API Managementのより詳細な設定方法](https://github.com/Azure-Samples/jp-azureopenai-samples/tree/main/6.azureopenai-landing-zone-accelerator#api-management%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%83%AD%E3%83%93%E3%83%B3)
・[その他の負荷分散の手段](https://qiita.com/Isato-Hiyama/items/237cb5b0b420d4239a77)