How to limit the use of Azure OpenAI Service via VNet
Azure OpenAI Service の利用を VNet 経由に限定する方法
本家 OpenAI ではなく Azure OpenAI Service を利用するメリットはいくつかありますが、その一つに プライベートネットワーク内に閉じて利用することができる という特徴があります。
すなわち、インターネットを経由しない経路で OpenAI を利用することができるということで、これによってセキュリティポリシーの厳しい企業においても利用条件をクリアできる (少なくともネットワークに関して) ものと思われます。
Azure OpenAI Service は Azure Cognitive Service の一員であり、その構築方法は他の Cognitive Service と同様です。
とはいえ、Azure OpenAI Service 特有の観点もあるかと思いますので、構築方法を以下に紹介したいと思います。
前提
- Azure OpenAI Service をデプロイ済み
- リージョンは
East US
とします。 - 動作確認用に少なくとも 1つのモデルをデプロイしておきます。
- 本記事では
gpt-4-0314
という名前でデプロイしたモデルを使用します。
- 本記事では
- リージョンは
- API クライアントとして Linux 仮想マシンを仮想ネットワーク内にデプロイ済み
- リージョンは
Japan East
とします。 - 仮想ネットワークにおけるサブネットの CIDR は
10.3.0.0/24
とします。 - 予め
pip install openai
を実行しておきます。
- リージョンは
免責
- 本記事では基本的に Azure Portal を使って操作します。CLI 等を使った操作の説明はありません。
- 本記事の内容は執筆時点のものです。将来的に変更が発生して、本記事での説明が適用できなくなる可能性があります。
- 本記事において、Azure OpenAI Service への API リクエストには REST ではなく OpenAI Python Library を用います。
構成
Before
Azure OpenAI Service の利用を仮想ネットワーク経由に限定する前の構成です。

IaaS である仮想マシンは仮想ネットワークとネットワーク セキュリティ グループで保護することができる一方、PaaS である Azure OpenAI Service をネットワークのレベルで保護するサービスはありません。(簡易なファイアウォール機能はあります。)
さらに、Azure OpenAI Service の API 認証方式は現状 API キー認証 と Azure Active Directory 認証の 2種類ですが、使用しない認証方式を無効化することはできません。
当然 API キーも十分な複雑さを持ってはいるものの (解析されるまでの時間: 数世紀〜10000+世紀)、これではセキュリティとして不十分であると考えるユーザーもいるでしょうし、企業のセキュリティポリシーに適合しないケースもあるでしょう。
After
こちらが、Azure OpenAI Service の利用を仮想ネットワーク経由に限定した構成です。

Azure OpenAI Service のネットワークアクセスを無効にした上で、任意の仮想ネットワークからのアクセスのみ受け付けられるよう、当該仮想ネットワーク向けにプライベート エンドポイントを提供します。
また、Azure OpenAI Service の API に対するインターネットアクセスをする場合と同じエンドポイントの FQDN が使えるよう、プライベート DNS ゾーンで名前解決を行います。
これであれば、セキュリティとして多くの企業が受け入れられるレベルなのではないでしょうか。
そしてこの構成は、簡単に構築することができます。以下に手順を説明します。
手順
1 | 1. Azure OpenAI Service のネットワークを無効にする |
※1, 2 は順不同ですが、構築にミスがあったときの切り分けのしやすさから、この順番を採用しています。
1. Azure OpenAI Service のネットワークを無効にする
まず、デプロイ済みとなっている Azure OpenAI Service のリソースを Azure Portal で開きます。

画面左のメニューから ネットワーク
を開きます。



特にネットワークの制限を設定していない場合、 Firewalls and virtual networks
タブにおいて、許可するアクセス元としては すべてのネットワーク
が選択されているはずです。
この状態で、仮想マシンからインターネット経由で ChatCompletion
API をコールします。
コードは以下の通りです。openai.api_base
と openai.api_key
は環境変数から取得するようにしています。
1 | #!/usr/bin/env python3 |
実行結果は以下の通りです。

正しく結果が返ってくることが確認できました。
ここから、ネットワークを無効にしていきます。
許可するアクセス元 無効
を選択し、 Save
をクリックします。

変更が適用されるまで、数分待ちます。

更新中……

変更が適用されました。
先ほどコールした API を再びコールします。

openai.error.PermissionError: Public access is disabled. Please configure private endpoint.
と返ってきました。これで、パブリックアクセスが無効となったことを確認できました。
2. プライベート エンドポイントを作成する
仮想ネットワーク側から Azure OpenAI Service を利用できるように、プライベート エンドポイントを作成します。
プライベート エンドポイントの作成手順は一つではありません。ここで示す方法は一例である旨、ご了承ください。
まず、Azure OpenAI Service のネットワーク (先の手順で使用した画面) を開き、 プライベート エンドポイント接続
タブに切り替えます。

ここで + プライベート エンドポイント
をクリックし、プライベート エンドポイント作成を開始します。

API クライアントとなる VM は Japan East
リージョンの仮想ネットワークにデプロイされているため、リージョンの設定はそれに合わせて Japan East
とします。

リソース
に関して、特に変更はありません。先に進めます。

仮想ネットワーク
の設定を行います。
プライベート エンドポイントをデプロイする仮想ネットワークを選択します。
本記事では API クライアントとなる VM が接続している仮想ネットワークおよびサブネットを使用します。
プライベート IP 構成
について、基本的に FQDN でアクセスするため、 IP アドレスを動的に割り当てる
で良いでしょう。
もし独自の DNS を構築・設定して利用する場合や、当該仮想ネットワークの IP アドレス管理を厳密に行っている場合は、 IP アドレスを静的に割り当てる
の方が都合が良いかもしれません。
アプリケーション セキュリティ グループ
は本記事では設定しません。

DNS
の設定は、デフォルト値のままで構いません。先に進めます。

タグ
は必要に応じて付与してください。先に進めます。


作成
をクリックして、プライベート エンドポイントのデプロイを開始します。
参考までに、ここで API クライアントとなる Linux 仮想マシン側のリソースグループの状況を確認しておきましょう。

デプロイを進めます。


ここで、再度 API クライアントとなる Linux 仮想マシン側のリソースグループの状況を確認します。

privatelink.openai.azure.com
というプライベート DNS ゾーンが作成されました。
既に当該 Azure OpenAI Service の FQDN への A レコードが作成されており、それがプライベート エンドポイントの IP アドレスに向けられています。

したがって、API クライアントとなる Linux 仮想マシンが Azure OpenAI Service のエンドポイントをコールした場合、インターネットではなくプライベート エンドポイントに対してコールする形になります。
Azure OpenAI Service 側のリソースグループも見てみましょう。

ネットワーク インターフェースとプライベート エンドポイントが作成されました。
このネットワーク インターフェースはプライベート エンドポイントにアタッチされており、指定された仮想ネットワークおよびサブネットにデプロイされており、プライベート エンドポイントの IPv4 アドレスが振られています。

また、プライベート エンドポイントの DNS の構成
から、当該ネットワーク インターフェースに対して、元々 Azure OpenAI Service に付与されていた FQDN が割り振られていることが確認できます。

元々 Azure OpenAI Service に付与されていた FQDN について、Linux 仮想マシンで nslookup
を実行してみましょう。

プライベート エンドポイントにアタッチされているネットワーク インターフェースに付与されている IP アドレスとなっていることが確認できます。canonical name
、つまり CNAME
が別で存在していて、先ほどの DNS の構成
ではこちらも同じ IP アドレスを指していました。
試しにこの CNAME
に対しても nslookup
を実行してみます。

同じ IP アドレス (プライベート エンドポイントにアタッチされているネットワーク インターフェースに振られている IPv4 アドレス) を指しています。
後述しますが、API クライアントが API をコールする際に使用するのは元々の FQDN の方です。 CNAME
の方は使いません。
3. 仮想ネットワーク内からプライベート エンドポイント経由で Azure OpenAI Service を使用する
API クライアントとなる Linux 仮想マシン上で作成した Python スクリプトを、一切書き換えることなく (openai.api_base
も openai.api_key
も変更せず)、再度実行します。

Azure OpenAI Service の ChatCompletion
を、プライベート エンドポイント経由で実行することができました。
補足
8 点補足を行います。
1 | 1. 仮想ネットワーク外から API をコールした場合 |
仮想ネットワーク外から API をコールした場合
この状態で、仮想ネットワーク外から API をコールした場合に、どのような挙動になるかを確認します。

ChatCompletion
はエラーとなりました。(NewConnectionError
)
これで、Azure OpenAI Service のエンドポイントがネットワーク的に保護されていることが確認できました。
プライベート エンドポイントの FQDN
プライベート エンドポイントが正しく構成されていれば、元々 Azure OpenAI Service に付与されていた FQDN を使って API コールをすることができるのは、説明した通りです。
CNAME
を使う必要はありません。もとい、CNAME
を使うと Azure OpenAI Service にアクセスできません。

SSLError
となります。
独自 DNS サーバーを利用する場合
オンプレミスのクライアントから ExpressRoute 等を経由して Azure OpenAI Service を使う場合などで、オンプレミスのクライアントが独自の DNS を使用するケースがあると思われます。
この場合、上記のプライベート DNS ゾーンを参考にして CNAME
を設定し、かつ、DNS フォワーダーまたは Azure DNS Private Resolver を使用することになります。
詳しくは以下のリンクを参照してください。
許可するアクセス元 は 無効 ではなく 選択したネットワークとプライベート エンドポイントでもよいのか
良いです。
簡易なファイアウォールが利用できますので、プライベート エンドポイントを経由せずに API コールさせたい、あるいは Azure OpenAI Studio を使わせたい端末がある場合は、こちらを選択し、ソース IP アドレスや CIDR を許可してください。
インターネット経由での Azure OpenAI Studio の操作
別の懸念事項として、インターネット経由で万一 Azure OpenAI Studio を不正利用されてしまった場合に、何ができて何ができないのかということが挙げられると思います。
以下、検証結果を紹介します。
TL;DR
- できないこと
- 微調整されたモデルの表示
- Playground でのプロンプト実行
- Completions playground, Chat playground (Preview) ともに不可
- できること
- Azure OpenAI Studio を開く
- 画面を遷移する
- モデル一覧の表示
- モデルのデプロイ
- 通知の表示
- デプロイしたモデルの一覧表示
- デプロイしたモデルの詳細表示
- デプロイしたモデルの削除
- 表示言語の切り替え
- 未確認
- モデルの微調整
- 使用している Azure サブスクリプション ではモデルの微調整が有効化されていないため
- Testing and training dataset (テストおよびトレーニングのデータセット)
- モデルの微調整が利用できないため, また jsonl ファイルを用意していないため
- モデルの微調整
検証

Azure OpenAI Studio を開きます。

画面左のメニューを展開します。

Models 画面へ遷移します。
このとき、通知が来たので開きます。「微調整されたモデルを取得できなかった」とのことでした。

モデルの一覧は表示できます。

モデルをデプロイします。




モデルのデプロイに成功しました。
デプロイされたモデルを確認するため、 Deployments 画面へ移動します。
まず、デプロイされたモデルのリストが表示されました。


デプロイされたモデルの詳細を確認することができました。
デプロイされたモデルを削除します。



デプロイされたモデルの削除に成功しました。
次に、デプロイ済みのモデルを実行してみたいと思います。
Completions playground 画面に移動します。

Completion を実行します。


Completions call failed:AccessDenied
と表示され、実行することができませんでした。
次に、Chat playground (Preview) に移動します。

ChatCompletion を実行します。


Public access is disabled. Please configure private endpoint.
とチャットで返ってきました。
エラーハンドリングの実装が独特なものになりそうです。
最後に、表示言語の設定変更を試します。





表示言語の設定変更ができました。
インターネット経由での API 操作
プライベート エンドポイント経由にアクセスを限定した後も、Azure OpenAI Studio ではモデルの一覧表示を含むいくつかの操作を、インターネット経由で行うことができました。
ここで、インターネット経由で同様の API 操作が可能なのかを検証します。
当該仮想ネットワークの外にある端末から、以下のコードを実行します。
1 | #!/usr/bin/env python3 |

少なくともモデルの一覧表示はできないことが分かりました。
API キー で細かな認可は可能か
できません。
Azure RBAC で細かな認可は可能か
できません。
まとめ
Azure OpenAI Service の利用を仮想ネットワーク経由に限定する方法について解説しました。
ユーザー企業のセキュリティポリシーに適合させるためにこのような構成にする必要があるケースもあるかと思いますが、簡単にできますので、必要に応じて構成し、Azure OpenAI Service を使い始めてみてください。