Azure OpenAI をネットワーク機能で負荷分散する

Azure OpenAI をそれなりの規模の環境で利用する場合、[リソースの上限](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/quotas-limits)があるため、負荷分散を行いたいといった要望があります。Azure の機能で実現する場合、以下の方法が考えられます。実際に利用可能であるかを検証して確認してみましたので、ご利用の際はご一読いただければと思います。

- API Management ([Azure API Management を使用して Azure OpenAI for Python をスケーリングす](https://learn.microsoft.com/ja-jp/azure/developer/python/get-started-app-chat-scaling-with-azure-api-management?tabs=github-codespaces%2Cinitial-deployment) ※本記事では詳細は記載してません)
- Application Gateway
- Front Door
- Traffic Manager

なお、**Azure Load Balancer で PaaS や Private Endpoint を負荷分散することはサポートされてません**ので、このシナリオでは利用できません。

## 負荷分散の機能のおさらい

[こちら](https://learn.microsoft.com/ja-JP/azure/architecture/guide/technology-choices/load-balancing-overview)に詳細はまとまっておりますが、本件に関連しそうなところとして、それぞれのネットワーク機能は以下のような違いがありますので、要件に応じて選択する必要があります。

| サービス | 範囲 | ルーティング | 分散方式 | 対応プロトコル | WAFの有無 |URL,ヘッダー書き換え|正常性プローブの応答|閉域利用|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Application Gateway  | リージョン内 | ラウンドロビンのみ | L7 ロードバランサー | HTTP, HTTPS, HTTP/2, WebSocket| 〇 | 〇 |Status コードの修正可能|〇|
| Front Door  | グローバル | 優先順位/レイテンシ/重み | L7 ロードバランサー | HTTP, HTTPS, HTTP/2 |〇| 〇 | Status 200 のみ| × |
| Traffic Manager  | グローバル  |  優先順位/レイテンシ/重み/地理/サブネット | DNS ベースのロードバランサー | TCP, UDP | × | × |Status コードの修正可能| × |

## Azure OpenAI の認証方式

API キーまたは AAD 認証の 2 種類がサポートされています。

https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference

:::note info
- API キー認証: この種類の認証の場合、すべての API 要求で、api-key HTTP ヘッダーに API キーを含める必要があります。 クイック スタートに、この種類の認証を使用して呼び出しを行う方法に関するガイダンスが用意されています。

- Azure Active Directory 認証: Azure Active Directory トークンを使用して API 呼び出しを認証できます。 認証トークンは、Authorization ヘッダーとして要求に含まれます。 指定するトークンの前に Bearer を付ける必要があります (例: Bearer YOUR_AUTH_TOKEN)。 Azure Active Directory での認証に関する攻略ガイドをお読みください。
:::

## 構成検討
Azure OpenAI にアクセスするためには認証が必須という点と、リソースごとに異なる API キーを持っているという点を考慮して構成を検討する必要があります。
また、各サービスには可用性を担保する機能も備わっているため、最低限、以下の点を考慮して、構成したほうが良いと思います。

- 各負荷分散サービスからの正常性プローブへの応答をどうするか。(※認証がないため、Status 404 等を許容するか) **※2023/11/27 追記。** [こちらに正常性プローブの参考情報がありました。](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-architecture-patterns-and-implementation-steps/ba-p/3979934) モデルによって動作が異なる可能性があるため事前検証をお勧めします。
- Azure OpenAI への認証方式は API Key, AAD どちらを利用するか。
- アクセスする URL を統一するため、各 Azure OpenAI のデプロイ名は合わせる。

## 各機能の検証
以下のような構成で各機能で Azure OpenAI の負荷分散を実施します。
クライアントは検証しやすかったため、curl コマンドで実施しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/cfdd60bf-b949-dc10-1251-ee1c790d1aa7.png)

### Application Gateway の場合

#### 正常性プローブの構成
Application Gateway の正常性プローブでヘッダーを付与する機能はないため、Status 404 を許容するといった対応が必要となります。Status 200 ではないため、API へのリクエストが成功するような状態であるか？までは監視できませんが、ネットワークレベル (http リクエストが届き、status 404 が返る) での監視は可能です。**※2023/11/27 追記。** [こちらに正常性プローブの参考情報がありました。"/status-0123456789abcdef" のパスを指定することで status 200 にできます。](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-architecture-patterns-and-implementation-steps/ba-p/3979934)

この例ではバックエンドプールのホスト名に対してパス / の HTTP リクエスト `https://xxx.openai.azure.com/`  を投げる構成としています。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/e2d783b5-0a81-47c1-fbc7-728cd1f36c08.png)

正常性プローブが健全となっていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/23ea2ce7-e01d-d5dc-d6b2-2730c9ffed6c.png)

#### 認証方式1 (API Key 認証)
Application Gateway では振り分け先のバックエンドに応じてヘッダーを付与するような機能はありません。HTTP リクエストのサーバー変数 (IP, Port 等) やヘッダーを元に振り分け先を指定し、ヘッダーを付与することはできます。**バックエンドの冗長化はできません**が、負荷分散のみの要件であれば以下のような構成は可能です。
ただし、テンプレート等で平文で API Key が参照可能となってしまいますので、一時的な動作確認程度で本番環境での利用はお勧めしません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/c6aef792-c207-2f40-4c52-7f9a9043b692.png)

どのようにヘッダー書き換えを設定すればよいかは以下が参考になると思います。

https://qiita.com/Isato-Hiyama/items/4369ea506cfbf4c2e9c8

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/debeb999-41dd-e8fa-d7d6-b1af8ce220dc.png)


接続元ポートが 0-4 であれば URL の先頭に /path1 を付与し、api-key (West Europe) も付与します。また、パスマップの再評価も行います。(変更後の URL パスで再度ルーティングを行う)

一致させるパターンは `^\d*[01234]$` の正規表現を利用。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/704cdb81-ff97-917f-592e-1462656c0a21.png)

接続元ポートが 5-9 であれば URL の先頭に /path2 を付与し、api-key (France Central) も付与します。また、パスマップの再評価も行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/5611cff0-9833-527c-063a-094ae4e1afd5.png)

以下のようなコマンドで Application Gateway に割り当てた FQDN に対して POST リクエストを投げます。ヒアドキュメントで JSON を読み込んで、jq で見やすくフォーマットしています。
```
curl -v https://xxxx.hiyamanet2.org/openai/deployments/gpt35model/chat/completions?api-version=2023-03-15-preview \
-H "Content-Type: application/json" \
-d @- <<EOM | jq
{
    "messages": [
        {
            "role": "user",
            "content": "Hi OpenAI"
        }
    ]
}
EOM

```

以下の様に Azure OpenAI から応答があることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/f2d50c0a-982c-ffc5-3585-56348862e45a.png)

診断ログのアクセスログを見てみると clientPort の番号により、想定通り West Europe (weu), France Central (crf) に負荷分散されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/f3a0f69f-725a-767d-6e76-b9891d480cb5.png)

ただし、前述のとおりこの構成は 1 バックエンドプール、1 エンドポイントとなっているため、バックエンドの冗長化はできません。どちらかの Azure OpenAI が異常となった時に、そのエンドポイントに振り分けられたリクエストのレスポンスは 502 となります。

#### 認証方式2 (AAD 認証)

書き換え規則を削除して、2 つの Azure OpenAI リソースを同じバックエンドプールに登録し、Basic ルールと関連付けを行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/f19895ed-73c1-7b16-84c1-081f280a7b3a.png)


以下のドキュメントを参考に実施します。

https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/how-to/managed-identity

curl の実行端末で Azure CLI を使えるようにしておきます。また、jq コマンドも利用できるようにしておきます。
検証で利用してる端末は CentOS 8.5 です。

https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-linux?pivots=dnf

Azure AD のユーザーで Token を取得し、リクエストを投げるため、各 Azure OpenAI のリソースに Cognitive Service ユーザーロールを割り当てておきます。(ユーザーはなくサービスプリンシパルの場合も似たような手順になるはずです)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/e6f75f9c-61a8-6e39-314d-af3bc78aa820.png)

az login コマンドを実行し、RBAC を割り当てたユーザーでログインし、token を取得します。
```
az login

export accessToken=$(az account get-access-token --resource https://cognitiveservices.azure.com -o json | jq -r .accessToken)
```

`-H "Authorization: Bearer $accessToken"` を付け足してコマンドを実行します。
```
curl -v https://xxxx.hiyamanet2.org/openai/deployments/gpt35model/chat/completions?api-version=2023-03-15-preview -H "Content-Type: application/json" -H "Authorization: Bearer $accessToken" -d @- <<EOM | jq
{
    "messages": [
        {
            "role": "user",
            "content": "Hi OpenAI"
        }
    ]
}
EOM
```

何回か実行し、レスポンスに含まれるリージョンが想定通りに切り替わっていることを確認できました。こちらのレスポンスヘッダーはリソースのリージョンと同じになるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/44d41fa6-acc2-1767-cd68-c5b97fadc8f8.png)

また、試しに疑似的な障害として France Central のプライベートエンドポイントを削除してみましたが、想定通りに正常性プローブが失敗し、West Europe のみにリクエストが振り分けられました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/3467c862-5756-243f-d113-f387c55922b2.png)

#### OpenAI サービスのファイアウォール
Application Gateway からプライベートエンドにアクセス可能ですので、ファイアウォールを無効化してしまい、プライベートエンドのみが有効化されていれば問題ありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/fb1ce878-fdac-6cd4-cc24-f06cd8d5c17c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/af40e4c2-0ea8-2757-0dc9-d969a9e7f89a.png)

### Front Door の場合
Azure Front Door (Standard または Premium) を利用して負荷分散を行います。

#### 正常性プローブの構成
Front Door の正常性プローブは Status 200 のみが成功とみなされます。そのため、認証が必須となる Azure OpenAI のようなリソースの場合、**正常性プローブを使ってバックエンドを切り替えることができません。**
~~Azure OpenAI の負荷分散のために、Azure Front Door を使う場合はバックエンドの冗長化ができないことになります。~~ **※2023/11/27 追記。** こちらの "/status-0123456789abcdef" のパスを指定する方法で正常性を確認できるようです。[正常性プローブの参考情報](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-architecture-patterns-and-implementation-steps/ba-p/3979934)


https://learn.microsoft.com/ja-jp/azure/frontdoor/health-probes#health-probe-responses

:::note warn
200 OK 状態コードは、配信元が正常であることを示します。 その他の状態コードは失敗と見なされます。
:::

各配信グループの設定で正常性プローブを無効化しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/aaa079b0-5eb0-ce78-c7d8-89818642b50f.png)

#### 認証方式1 (API Key 認証)
Application Gateway と同じような方法で、クライアントポートが 1-5 で終わる場合、api-key (West Europe) を付与して、West Europe の配信グループに振り分けし、クライアントポートが 1-5 で終わらない場合、api-key (France Central) を付与して、France Central の配信グループに振り分けするルールセットを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/b89a1018-fd5d-0119-987d-b896ebcce250.png)

作成したルールセットをルールに関連付けします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/369e935d-ca5f-d8f8-3536-7f89b08240e5.png)

Application Gateway と同じように curl コマンドを実行したところ、想定通りに振り分けられ応答が得られることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/afaef08a-e86f-7d9c-856c-69c8d173bb14.png)

ただし、こちらもテンプレート等で平文で API Key が参照可能となってしまいますので、一時的な動作確認程度で本番環境での利用はお勧めしません。

#### 認証方式2 (AAD 認証)
ルールセットから API Key を削除しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/aef7df2f-1cc7-27c1-ccbe-17f49948b1b4.png)

想定通りに送信元ポートに応じて負荷分散されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/33b0a678-0b93-ea37-0e81-dfb242d03624.png)

**※2023/11/27 追記。こちらの "/status-0123456789abcdef" のパスを指定する方法でも問題なく負荷分散可能でした。** [正常性プローブの参考情報](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-architecture-patterns-and-implementation-steps/ba-p/3979934)

#### OpenAI サービスのファイアウォール
Azure OpenAI へのアクセスを "AzureFrontDoor.Backend" タグに限定します。サービスタグの機能はないのでアドレスを記載する必要があります。
様々なやり方があると思いますが、サービスタグの一覧ファイルから AzureFrontDoor.Backend タグの IPv4 アドレスを取得し、テキストエディタ等で { "value": "xxx" } のような JSON 形式に変更後、az rest コマンドでヒアドキュメントで JSON を読み込んでいくのが簡単でした。/32 がある場合は /32 部分の削除が必要だったりしますので、JSON の中身は適宜修正してください。

サービスタグの一覧は更新がありますので定期的なメンテが必要となります。

https://www.microsoft.com/en-us/download/details.aspx?id=56519

https://learn.microsoft.com/en-us/cli/azure/reference-index?view=azure-cli-latest#az-rest


```
az rest --method PATCH --url "/subscriptions/ad982283-xxxx-xxxx-xxxx-8043c3586b04/resourceGroups/rg-testxxxx/providers/Microsoft.CognitiveServices/accounts/cog-xxxxxx?api-version=2022-10-01" --body @- << EOM
{
    "properties": {
        "networkAcls": {
            "defaultAction": "Deny",
                "ipRules": [
                    { "value": "4.232.98.120/29" },
                                  ※省略
                    { "value": "158.23.108.56/29" },
                    { "value": "191.233.9.120/29" },
                    { "value": "191.235.225.128/29" }
                ],
                    "virtualNetworkRules": []
        }
    }
}
EOM
```

### Traffic Manager の場合

#### 正常性プローブの構成
Status 404 を許可します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/0f61eb44-b262-e182-6b80-6eb0978a35bb.png)

#### 認証方式1 (API Key 認証)
Traffic Manager にはエンドポイントごとに API Key 付与するような機能がないため実施できません。

#### 認証方式2 (AAD 認証)
AAD 認証の場合は実施可能だが、Azure OpenAI にはカスタムドメインを割り当てる機能はないため、Traffic Manager の既定のホストでアクセスしてみます。
証明書の警告は出てしまうため、-k オプションで証明書の検証を無視してアクセスしてみます。

```
curl -kv https://testoaixxxx.trafficmanager.net/openai/deployments/gpt35model/chat/completions?api-version=2023-03-15-preview -H "Content-Type: application/json" -H "Authorization: Bearer $accessToken" -d @- <<EOM | jq
{
    "messages": [
        {
            "role": "user",
            "content": "Hi OpenAI"
        }
    ]
}
EOM
```

以下のようなエラーが出て、Status 404 でアクセスすることはできませんでした。**このことから Azure OpenAI サービスは異なるホスト名でのアクセスは受け付けないようです。** Traffic Manager での負荷分散は難しいことがわかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266293/5697fc9a-9414-52f9-4a9c-c2f5d2e93670.png)

## まとめ
検証結果をふまえると以下の様になります。

| サービス名| 負荷分散 | バックエンド冗長化 | ファイアウォール | 備考 |
|:-:|:-:|:-:|:-:|:-:|
| Application Gateway | 〇 | 〇 |  〇 | |
| FrontDoor  | 〇 | 〇  | △ | |
| Traffic Manager  | ×  |  ×   | - | 動作せず |

※2023/11/27 追記。こちらの "/status-0123456789abcdef" のパスを指定する方法にて Azure Front Door でも負荷分散、バックエンドの冗長化が可能でした。[正常性プローブの参考情報](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-architecture-patterns-and-implementation-steps/ba-p/3979934)