はじめに
本記事記述および実装内容は、多くの部分をAIで処理しており、人間チェックは甘いです
Azure AI Foundry の Agent Service を完全閉域ネットワーク(BYO VNet) で構築する検証を行いました。
公式ドキュメントやサンプル Bicep は存在しますが、実際にデプロイして初めてわかるポイントが多くあります。本記事では、Bicep テンプレートの設計からデプロイ、動作確認までの一連の流れを、実環境のスクリーンショットとコードを交えて解説します。
この記事で扱うこと
- BYO VNet 構成の全体アーキテクチャと設計判断
-
networkInjections/capabilityHosts/bypass: AzureServicesの仕組み - Bicep + azd によるデプロイの実践
- Private Endpoint + Private DNS Zone の構成
- Bastion + Jumpbox 経由でのエージェント動作確認
- コスト最適化のポイント
対象読者
- Azure AI Foundry を閉域ネットワークで使いたいエンジニア
- Private Endpoint / Private DNS Zone の構成を実例で理解したい方
- Bicep による IaC でネットワークセキュリティを含む構成を管理したい方
参考資料
全体アーキテクチャ
構成概要
今回構築する構成は Standard Setup with Private Network (BYO VNet) です。
-
すべての Azure リソースは
publicNetworkAccess: Disabled(パブリックインターネットからアクセス不可) - リソース間通信は VNet 内の Private Endpoint 経由
- 外部からの管理アクセスは Azure Bastion + Jumpbox VM 経由のみ
- エージェントコンピューティングは VNet に委任されたサブネットで動作
唯一の例外は Azure Bastion の Public IP です(Bastion の仕様上必須)。
アーキテクチャ図
┌──────────────────────────────────────────────────────────────────────┐
│ ユーザーアクセス (Internet → Azure Bastion → Jumpbox VM) │
└───────────────────────────┬──────────────────────────────────────────┘
│ RDP/SSH via Bastion
┌─────────────▼──────────────────────────────────────┐
│ Private VNet (10.0.0.0/16) │
│ │
│ ┌─────────────────────┐ │
│ │ AzureBastionSubnet │ 10.0.3.0/26 │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ snet-jumpbox │ 10.0.2.0/24 │
│ │ ┌───────────────┐ │ │
│ │ │ Jumpbox VM │ │ Windows Server 2022 │
│ │ │ (Standard_B2s)│ │ → ブラウザで Foundry に │
│ │ └───────────────┘ │ アクセス │
│ └─────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ AI Services Account │ │
│ │ (publicNetworkAccess: Disabled) │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Agent Compute │ │ │
│ │ │ (networkInjections) │ │ │
│ │ └──────────┬───────────┘ │ │
│ └──────────────┼───────────────────────────────┘ │
│ │ │
│ ┌──────────────▼──────────┐ │
│ │ snet-agent │ 10.0.0.0/24 │
│ │ (Microsoft.App 委任) │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ snet-pe │ 10.0.1.0/24 │
│ │ ┌─────────┐ ┌───────┐ │ │
│ │ │AI Search│ │CosmosDB│ │ ← Private Endpoints │
│ │ └─────────┘ └───────┘ │ │
│ │ ┌─────────┐ ┌───────┐ │ │
│ │ │ Storage │ │Foundry│ │ │
│ │ └─────────┘ └───────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────┘
デプロイされるリソース一覧
| カテゴリ | リソース | 主な設定 |
|---|---|---|
| ネットワーク | VNet (10.0.0.0/16) + 4 サブネット | NSG 付き |
| 管理アクセス | Azure Bastion (Basic) + Jumpbox VM (B2s) | Windows Server 2022 |
| AI | AI Services Account (S0) | publicNetworkAccess: Disabled |
| AI | gpt-4o-mini / gpt-5-mini | GlobalStandard, 30K TPM |
| バックエンド | Cosmos DB (Serverless) | disableLocalAuth: true |
| バックエンド | AI Search (Basic) | publicNetworkAccess: disabled |
| バックエンド | Storage Account (Standard_LRS) | publicNetworkAccess: Disabled |
| ネットワーク | Private Endpoints × 4 | AI Services, Cosmos DB, AI Search, Storage |
| DNS | Private DNS Zones × 6 | VNet リンク付き |
ネットワーク設計の詳細
サブネット設計
4 つのサブネットを用途ごとに分離しています。
| サブネット | アドレス範囲 | 用途 | 備考 |
|---|---|---|---|
snet-agent |
10.0.0.0/24 | エージェントコンピューティング |
Microsoft.App/environments に委任 |
snet-pe |
10.0.1.0/24 | Private Endpoint 配置 | NSG で VNet 内通信のみ許可 |
snet-jumpbox |
10.0.2.0/24 | Jumpbox VM | Bastion からの RDP/SSH のみ許可 |
AzureBastionSubnet |
10.0.3.0/26 | Azure Bastion 専用 | 名前固定(Azure の要件)、最小 /26 |



snet-jumpboxはインターネット通信可能になっています。これで、GitHubやPyPIなどにアクセス可能(ここの設定とNSG)。

resource nsgJumpbox 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
name: 'nsg-${jumpboxSubnetName}'
location: location
tags: tags
properties: {
securityRules: [
{
name: 'AllowBastionInbound'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRanges: ['3389', '22']
sourceAddressPrefix: bastionSubnetPrefix // 10.0.3.0/26
destinationAddressPrefix: '*'
}
}
{
name: 'DenyAllInbound'
properties: {
priority: 4096
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
Jumpbox の NSG は Bastion サブネット (10.0.3.0/26) からの RDP/SSH のみ許可し、それ以外はすべて拒否する設計です。
snet-agent は Microsoft.App/environments に委任されるため、NSG の管理はプラットフォーム側に委ねられます。手動で NSG を付ける必要はありません。
Private Endpoint と Private DNS Zone
閉域環境で各サービスにアクセスするため、4 つの Private Endpoint を作成し、6 つの Private DNS Zone を VNet にリンクしています。
| 対象サービス | PE のサブリソース | Private DNS Zone |
|---|---|---|
| AI Services | account |
privatelink.cognitiveservices.azure.com / privatelink.openai.azure.com / privatelink.services.ai.azure.com
|
| AI Search | searchService |
privatelink.search.windows.net |
| Cosmos DB | Sql |
privatelink.documents.azure.com |
| Storage | blob |
privatelink.blob.core.windows.net |
AI Services は 1 つの Private Endpoint に対して 3 つの DNS ゾーン が必要です。Cognitive Services / OpenAI / AI Services のそれぞれのエンドポイント FQDN を解決するためです。これを忘れると VNet 内から名前解決できずにタイムアウトします。
📸 【画面キャプチャ①】 Azure Portal →
rg-foundry-byo→ Private Endpoint 一覧画面(4つの PE が Approved 状態で表示されていること)
📸 【画面キャプチャ②】 Azure Portal → Private DNS Zone →
privatelink.cognitiveservices.azure.com→ レコードセット画面(A レコードがプライベート IP に解決されていること)
AI Services Account のネットワーク設定
networkAcls と bypass: AzureServices
AI Services アカウントでは、ネットワークアクセスを 2 つの層 で制御しています。
resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
name: accountName
location: location
sku: { name: 'S0' }
kind: 'AIServices'
identity: { type: 'SystemAssigned' }
properties: {
allowProjectManagement: true
customSubDomainName: accountName
networkAcls: {
defaultAction: 'Deny'
virtualNetworkRules: []
ipRules: []
bypass: 'AzureServices'
}
publicNetworkAccess: 'Disabled'
networkInjections: [
{
scenario: 'agent'
subnetArmId: agentSubnetId
useMicrosoftManagedNetwork: false
}
]
}
}
それぞれの設定の意味を整理します。
publicNetworkAccess: 'Disabled'
パブリックインターネットからのアクセスを 根本的に無効化 します。Azure Portal の「ネットワーク」画面で「無効」と表示される部分です。この設定により、IP ルールやサービスエンドポイントの許可設定に関係なく、パブリックエンドポイント経由のアクセスがブロックされます。
networkAcls.defaultAction: 'Deny'
ファイアウォールのデフォルトルールです。virtualNetworkRules(サービスエンドポイント)や ipRules(IP アドレス許可リスト)に明示的に登録されていない通信を すべて拒否 します。今回は両方とも空配列なので、事実上すべてのネットワーク経由アクセスを拒否しています。
bypass: 'AzureServices'
defaultAction: Deny の 例外規定 です。Azure の信頼されたサービス(Trusted Services) からのアクセスのみを許可します。
具体的には以下のような Azure 基盤サービスからのアクセスが該当します。
- Azure Monitor(診断ログの収集)
- Azure Policy(コンプライアンスチェック)
- Azure Backup
- その他 Microsoft が管理する信頼済みサービス
publicNetworkAccess: Disabled ← インターネットからの門を完全に閉める
↓
networkAcls.defaultAction: Deny ← ファイアウォールも全拒否(二重防御)
↓
bypass: AzureServices ← ただし Azure 信頼サービスは例外で通す
↓
Private Endpoint ← 正規のアクセスは PE 経由のみ
bypass: 'None' にすると Azure サービスからの通信も遮断されます。診断ログや監視が動作しなくなるリスクがあるため、通常は 'AzureServices' を設定します。
networkInjections:エージェントコンピューティングの VNet 統合
networkInjections: [
{
scenario: 'agent'
subnetArmId: agentSubnetId
useMicrosoftManagedNetwork: false
}
]
networkInjections は Foundry の エージェントコンピューティングを指定したサブネットに注入する 設定です。
-
scenario: 'agent':Agent Service のコンピュート(内部的に Azure Container Apps を使用)を対象 -
subnetArmId:委任済みのsnet-agentサブネットの ARM リソース ID -
useMicrosoftManagedNetwork: false:Microsoft 管理ネットワークを使わず、ユーザーの VNet を使う(これが BYO VNet の核心)
networkInjections は AI Services アカウント作成時にのみ設定可能 です。既存のアカウントに後から追加することはできません。VNet 構成を変更したい場合はアカウントの再作成が必要です。
Capability Host(capabilityHosts)の仕組み
Capability Host とは
Capability Host は、Foundry Project の Agent Service がバックエンドリソース(Cosmos DB / Storage / AI Search)にアクセスするための接続定義 です。
Agent が会話のスレッドを保存したり、ファイルを検索したりするには、これらのバックエンドリソースへの接続が必要です。Capability Host がその「橋渡し」の役割を担います。
Foundry Project
└── Capability Host (capabilityHostKind: 'Agents')
├── threadStorageConnections → Cosmos DB (会話スレッドの保存先)
├── storageConnections → Storage (ファイルアップロード先)
└── vectorStoreConnections → AI Search (ベクトル検索用)
Bicep での定義
Capability Host の定義は以下の通りです。
resource projectCapabilityHost 'Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview' = {
name: projectCapHost
parent: project
properties: {
capabilityHostKind: 'Agents'
vectorStoreConnections: [aiSearchConnection]
storageConnections: [azureStorageConnection]
threadStorageConnections: [cosmosDBConnection]
}
}
ここで渡している aiSearchConnection / azureStorageConnection / cosmosDBConnection は、Project の connections リソースの名前 です。Project 側で事前に接続(Connection)を定義しておく必要があります。
resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
parent: account
name: projectName
identity: { type: 'SystemAssigned' }
// Cosmos DB への接続定義
resource cosmosConnection 'connections@2025-04-01-preview' = {
name: cosmosDBName
properties: {
category: 'CosmosDB'
target: cosmosDBAccount.properties.documentEndpoint
authType: 'AAD' // ← マネージド ID で認証
metadata: { ApiType: 'Azure', ResourceId: cosmosDBAccount.id }
}
}
// Storage への接続定義
resource storageConnection 'connections@2025-04-01-preview' = {
name: azureStorageName
properties: {
category: 'AzureStorageAccount'
target: storageAccount.properties.primaryEndpoints.blob
authType: 'AAD'
}
}
// AI Search への接続定義
resource searchConnection 'connections@2025-04-01-preview' = {
name: aiSearchName
properties: {
category: 'CognitiveSearch'
target: 'https://${aiSearchName}.search.windows.net'
authType: 'AAD'
}
}
}
すべての接続は authType: 'AAD'(Microsoft Entra ID 認証)を使用しています。キーベース認証は使いません。これにより、マネージド ID + RBAC のみでアクセス制御を行う、よりセキュアな構成になります。
デプロイ順序が重要
Capability Host は ロール割り当てが完了してから 作成する必要があります。順序を間違えると、Capability Host 作成時にバックエンドリソースへのアクセスに失敗します。
1. Foundry Project 作成(SMI が発行される)
↓
2. ロール割り当て(Pre-CapHost)
- Storage: Storage Blob Data Contributor
- Cosmos DB: Cosmos DB Operator
- AI Search: Search Index Data Contributor + Search Service Contributor
↓
3. ★ Capability Host 作成 ★
↓
4. ロール割り当て(Post-CapHost)
- Storage Blob Containers: Storage Blob Data Owner(条件付き)
- Cosmos DB Containers: Cosmos DB Built-in Data Contributor
Pre-CapHost と Post-CapHost でロールが分かれる理由は、Capability Host 作成時に Cosmos DB のデータベースや Storage のコンテナが自動作成される ためです。作成後に、それらのコンテナレベルのアクセス権を追加で付与します。
RBAC(ロール割り当て)設計
Project のマネージド ID に付与するロール
Foundry Project のシステム割り当てマネージド ID (SMI) が、各バックエンドリソースにアクセスするためのロール一覧です。
| タイミング | 対象リソース | ロール |
|---|---|---|
| CapHost 前 | Storage Account | Storage Blob Data Contributor |
| CapHost 前 | Cosmos DB Account | Cosmos DB Operator |
| CapHost 前 | AI Search | Search Index Data Contributor |
| CapHost 前 | AI Search | Search Service Contributor |
| CapHost 後 | Storage Blob Containers | Storage Blob Data Owner(条件付き) |
| CapHost 後 | Cosmos DB Containers | Cosmos DB Built-in Data Contributor |
Cosmos DB のロール割り当ての特殊性
Cosmos DB のデータプレーンロールは ARM の RBAC とは別の仕組み です。Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments というリソースタイプを使用します。
var roleDefinitionId = resourceId(
'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions',
cosmosAccountName,
'00000000-0000-0000-0000-000000000002' // Built-in Data Contributor
)
resource containerRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = {
parent: cosmosAccount
name: guid(projectWorkspaceId, cosmosAccountName, roleDefinitionId, projectPrincipalId)
properties: {
principalId: projectPrincipalId
roleDefinitionId: roleDefinitionId
scope: accountScope
}
}
00000000-0000-0000-0000-000000000002 は Cosmos DB の Built-in Data Contributor ロールの固定 GUID です。ARM RBAC の Cosmos DB Account Reader Role 等とは別物なので注意してください。
Storage の条件付きロール割り当て
Post-CapHost の Storage ロール割り当てでは、ABAC(属性ベースのアクセス制御)条件 を使って、エージェント用コンテナのみにアクセスを限定しています。
var conditionStr = '(... @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase \'${workspaceId}\' AND @Resource[...containers:name] StringLikeIgnoreCase \'*-azureml-agent\')'
resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: storage
properties: {
principalId: aiProjectPrincipalId
roleDefinitionId: storageBlobDataOwner.id
principalType: 'ServicePrincipal'
conditionVersion: '2.0'
condition: conditionStr // ← エージェント用コンテナのみに制限
}
}
workspaceId で始まり *-azureml-agent にマッチするコンテナのみに Storage Blob Data Owner を付与する、最小権限の設計です。
デプロイ実行ユーザーへの権限付与
検証時にデータの確認ができるよう、デプロイ実行ユーザーにも権限を付与しています。deployerUserPrincipalId が空の場合はスキップされます。
module deployerUserRoleAssignments 'modules/deployer-user-role-assignments.bicep' = if (!empty(deployerUserPrincipalId)) {
name: 'deployer-ra-${uniqueSuffix}-deployment'
params: {
userPrincipalId: deployerUserPrincipalId
azureStorageName: aiDependencies.outputs.azureStorageName
cosmosAccountName: aiDependencies.outputs.cosmosDBName
accountName: aiAccount.outputs.accountName
projectName: aiProject.outputs.projectName
}
}
付与されるロール:
| 対象 | ロール | 用途 |
|---|---|---|
| Storage Account | Storage Blob Data Contributor | Blob データの検証 |
| Cosmos DB | Cosmos DB Built-in Data Contributor | データの読み書き検証 |
| Foundry Project | Azure AI Developer | Agent の作成・実行 |
デプロイ手順
前提条件
- Azure CLI + Azure Developer CLI (
azd) がインストール済み - 対象サブスクリプションで 所有者 または Role Based Access Administrator 以上の権限
- 必要なリソースプロバイダーが登録済み
リソースプロバイダーの登録コマンド
az provider register --namespace 'Microsoft.CognitiveServices'
az provider register --namespace 'Microsoft.Storage'
az provider register --namespace 'Microsoft.Search'
az provider register --namespace 'Microsoft.Network'
az provider register --namespace 'Microsoft.App'
az provider register --namespace 'Microsoft.ContainerService'
az provider register --namespace 'Microsoft.KeyVault'
az provider register --namespace 'Microsoft.MachineLearningServices'
azd によるデプロイ
cd foundry-byo-vnet-basic
# 初期化(初回のみ)
azd init
# 環境変数の設定
azd env set AZURE_LOCATION japaneast
azd env set JUMPBOX_ADMIN_USERNAME azureadmin
azd env set JUMPBOX_ADMIN_PASSWORD "<強力なパスワード>"
azd env set DEPLOYER_USER_PRINCIPAL_ID "$(az ad signed-in-user show --query id -o tsv)"
azd env set UNIQUE_SUFFIX fyr5
# デプロイ実行
azd provision
azd と az CLI は認証が独立しています。事前に azd auth login も実行してください。main.bicepparam 内の readEnvironmentVariable() が azd env set で設定した値を自動的に読み取ります。
デプロイ順序(Bicep が自動制御)
1. VNet + Subnets + NSG
2. Azure Bastion + Jumpbox VM
3. AI Services Account + モデルデプロイ (gpt-4o-mini, gpt-5-mini)
4. バックエンドリソース (Cosmos DB, AI Search, Storage)
5. Private Endpoints + Private DNS Zones
6. Foundry Project + Connections
7. ロール割り当て(Pre-CapHost)
8. Capability Host
9. ロール割り当て(Post-CapHost)
--- ここまで Bicep 自動デプロイ ---
10. [手動] Bastion → Jumpbox VM に接続
11. [手動] Prompt Agent 作成 (Python SDK)
Prompt Agent の作成(Jumpbox VM 内で実行)
閉域環境のため、Agent の作成は Jumpbox VM に Bastion 経由で接続して手動で実行 します。Foundry Agent は Bicep のネイティブリソースとして定義できないため、Python SDK を使います。
接続手順
- Azure Portal → Bastion → Jumpbox VM に RDP 接続
- Jumpbox VM 内で Python 環境をセットアップ
-
az loginで認証 - スクリプトを実行
📸 【画面キャプチャ⑧】 Azure Portal → Bastion → Jumpbox VM への接続画面
Python スクリプト
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.agents.models import PromptAgentDefinition
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--endpoint", required=True)
parser.add_argument("--model", default="gpt-5-mini")
parser.add_argument("--agent-name", default="prompt-agent")
parser.add_argument(
"--instructions",
default="あなたは役立つAIアシスタントです。日本語で回答してください。",
)
args = parser.parse_args()
credential = DefaultAzureCredential()
client = AIProjectClient(endpoint=args.endpoint, credential=credential)
agent = client.agents.create_agent(
model=args.model,
name=args.agent_name,
instructions=args.instructions,
agent_definition=PromptAgentDefinition(),
)
print(f"Agent created successfully!")
print(f" Agent ID: {agent.id}")
print(f" Agent Name: {agent.name}")
print(f" Model: {agent.model}")
if __name__ == "__main__":
main()
# Python パッケージのインストール
pip install azure-identity azure-ai-projects azure-ai-agents
# Azure CLI で認証
az login
# プロジェクトエンドポイントは azd provision 完了後に取得
# az deployment group show で確認するか、azd の出力から取得
python create-prompt-agent.py --endpoint "https://aiservicesfyr5.cognitiveservices.azure.com/api/projects/projectfyr5"
デプロイ後の確認ポイント
DNS 解決の確認
Jumpbox VM 内から nslookup を実行し、Private Endpoint のプライベート IP に解決されることを確認します。
nslookup aiservicesfyr5.cognitiveservices.azure.com
プライベート IP(例: 10.0.1.x)が返ってくれば、Private DNS Zone が正しく動作しています。パブリック IP が返る場合は DNS Zone の VNet リンクを確認してください。
Foundry ポータルでの動作確認
Jumpbox VM 内のブラウザで https://ai.azure.com にアクセスし、作成した Prompt Agent で会話ができることを確認します。
コスト最適化
SKU 選定の考え方
検証目的のため、各リソースで最小限の SKU を選定しています。
| リソース | 選定 SKU | 理由 |
|---|---|---|
| AI Search | Basic (~$70/月) | Free は Private Endpoint 非対応のため最小の有料 SKU |
| Cosmos DB | Serverless (~$1–5/月) | 従量課金でアイドル時のコストを最小化 |
| Storage | Standard_LRS | 最小冗長性で十分 |
| Bastion | Basic (~$140/月) | 最小 SKU。最大のコスト要因 |
| Jumpbox VM | Standard_B2s (~$30/月) | 2 vCPU / 4 GiB RAM のバースト対応 VM |
月額コスト概算
| 項目 | 概算月額 (USD) |
|---|---|
| Azure Bastion (Basic) | ~$140 |
| AI Search (Basic) | ~$70 |
| Jumpbox VM (B2s) | ~$30 |
| Private Endpoints × 4 | ~$28 |
| その他 (DNS, Storage, Cosmos DB, Public IP) | ~$10 |
| 合計(固定費ベース) | ~$275–280 |
Bastion が月額コストの約半分 を占めます。検証で常時使わない場合は Bastion を削除し、必要な時だけ再作成する運用がおすすめです(約 $0.19/時間)。Jumpbox VM も使わない時は 停止(割り当て解除) することでコンピューティング課金を止められます。
既知の制限事項・ハマりポイント
-
networkInjectionsは後付け不可 — AI Services アカウント作成時に VNet 構成を決める必要がある。変更するにはアカウント再作成が必要 -
エージェントサブネットは専用 —
snet-agentは他のリソースと共有できない(Microsoft.App/environments委任が必要なため) - 全リソースは同一リージョン必須 — Cosmos DB, Storage, AI Search, Foundry, VNet すべて同じリージョンに配置
- 削除順序に注意 — Foundry リソース削除 → 完全消去(Purge)→ その後 VNet 削除の順序を守ること。逆順だと削除が失敗する
- AI Search の Free SKU は使えない — Private Endpoint に対応していないため、最低 Basic SKU が必要
- SPL は手動承認が必要 — Shared Private Link を Bicep で作成した後、対象リソース側のプライベートエンドポイント接続を手動承認する必要がある(Bicep では自動承認不可)
-
Azure CLI の SPL バリデーションが古い —
az search shared-private-link-resource createはopenai_accountの groupId を受け付けない(2026年4月時点)。REST API(az rest)を使うか、Bicep で直接定義する - スキルセットの API Key 認証は SPL 経由で動作しない — マネージド ID 認証に切り替える必要がある
AI Search ナレッジベース(インデクサー)を閉域環境で動かす
BYO VNet 構成の Foundry 環境で ナレッジベース(Knowledge Index) を作成し、AI Search のインデクサーでドキュメントをインデックスする場合、Private Endpoint だけでは不十分 です。追加で Shared Private Link(共有プライベートリンク) の構成が必要になります。
Private Endpoint と Shared Private Link の違い
この 2 つは根本的に異なるネットワーク経路です。
| 概念 | 経路 | 用途 |
|---|---|---|
| Private Endpoint (PE) | VNet 内のリソース → サービスのプライベート IP | Jumpbox やエージェントコンピュートからのアクセス |
| Shared Private Link (SPL) | AI Search の内部マルチテナントバックエンド → サービスのプライベート接続 | インデクサー/スキルセット実行時のデータソース・AI Services アクセス |
┌─────────────────────────────────────────────────────────────────────┐
│ AI Search のインデクサー実行環境 │
│ (Microsoft マルチテナントバックエンド) │
│ │
│ インデクサー ──── Shared Private Link ──→ Storage Account (blob) │
│ スキルセット ──── Shared Private Link ──→ AI Services (openai) │
│ │
│ ※ VNet の Private Endpoint 経由ではない! │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ お客様の VNet │
│ │
│ Jumpbox ───── Private Endpoint ──→ Storage Account │
│ Agent ───── Private Endpoint ──→ AI Services │
│ │
│ ※ こちらは VNet 内のリソースから直接アクセスする経路 │
└─────────────────────────────────────────────────────────────────────┘
既存の Private Endpoint が VNet 内にあっても、AI Search のインデクサーはそれを使えません。AI Search のバックエンドは Microsoft のマルチテナント環境で動作するため、個別に Shared Private Link を作成する必要があります。
必要な Shared Private Link
ナレッジベースを閉域環境で動作させるには、以下の 2 つの SPL が必要です。
| SPL 名 | 対象リソース | groupId | 用途 |
|---|---|---|---|
spl-storage-blob |
Storage Account | blob |
インデクサーが Blob からドキュメントを読み取る |
spl-openai |
AI Services | openai_account |
スキルセットがベクトル化(Embedding)を実行する |
AI Services には複数のエンドポイントがあり、groupId を正しく選ぶ必要があります。
| エンドポイント FQDN | groupId |
|---|---|
*.cognitiveservices.azure.com |
cognitiveservices_account |
*.openai.azure.com |
openai_account |
*.services.ai.azure.com |
foundry_account |
スキルセットの resourceUri に設定している FQDN と一致する groupId を選択してください。AzureOpenAIEmbeddingSkill で *.openai.azure.com を使っているなら openai_account です。
Shared Private Link の作成方法
Azure CLI の az search shared-private-link-resource create は、2026年4月時点で openai_account の groupId に対するバリデーションが未対応でエラーになります。REST API を直接使用してください。
# Storage Account 用
az rest --method PUT \
--url "https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Search/searchServices/{searchName}/sharedPrivateLinkResources/spl-storage-blob?api-version=2024-06-01-preview" \
--body '{
"properties": {
"privateLinkResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{storageName}",
"groupId": "blob",
"requestMessage": "Please approve for AI Search indexer access"
}
}'
# AI Services (OpenAI) 用
az rest --method PUT \
--url "https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Search/searchServices/{searchName}/sharedPrivateLinkResources/spl-openai?api-version=2024-06-01-preview" \
--body '{
"properties": {
"privateLinkResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{aiServicesName}",
"groupId": "openai_account",
"requestMessage": "Please approve for AI Search vectorization"
}
}'
作成後、対象リソースの「プライベートエンドポイント接続」から 手動で承認 が必要です。
# Storage の PE 接続を承認
az network private-endpoint-connection approve \
--resource-group {rg} \
--resource-name {storageName} \
--type Microsoft.Storage/storageAccounts \
--name {connectionName} \
--description "Approved for AI Search"
# AI Services の PE 接続を承認
az network private-endpoint-connection approve \
--resource-group {rg} \
--resource-name {aiServicesName} \
--type Microsoft.CognitiveServices/accounts \
--name {connectionName} \
--description "Approved for AI Search"
AI Search の Basic SKU 以上 で Shared Private Link がサポートされています。Free SKU では使用できません。
スキルセットの認証:API Key ではなくマネージド ID を使う
Shared Private Link 経由で AI Services にアクセスする場合、API Key 認証は動作しません。マネージド ID 認証を使用する必要があります。
{
"@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
"resourceUri": "https://{aiServicesName}.openai.azure.com",
"apiKey": "<API Key>",
"deploymentId": "text-embedding-3-small"
}
{
"@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
"resourceUri": "https://{aiServicesName}.openai.azure.com",
"apiKey": "",
"deploymentId": "text-embedding-3-small"
}
apiKey を空文字列または null にすることで、AI Search のシステム割り当てマネージド ID が使用されます。
事前に AI Search のマネージド ID に対して、AI Services アカウント上で Cognitive Services OpenAI User ロールを付与しておく必要があります。
az role assignment create \
--assignee-object-id {aiSearchManagedIdentityObjectId} \
--assignee-principal-type ServicePrincipal \
--role "Cognitive Services OpenAI User" \
--scope /subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{aiServicesName}
インデクサーの実行環境設定
Shared Private Link を使用する場合、公式ドキュメントではインデクサーの executionEnvironment を "private" に設定することが推奨されています。
{
"name": "my-indexer",
"dataSourceName": "my-datasource",
"targetIndexName": "my-index",
"skillsetName": "my-skillset",
"parameters": {
"configuration": {
"executionEnvironment": "private"
}
}
}
executionEnvironment を明示的に設定しない場合でも動作する場合がありますが、AI Search が自動選択するため、負荷状況やサービス更新によりマルチテナント環境にフォールバックする可能性があります。その場合、SPL 経由の DNS 解決ができず接続に失敗します。
明示的に "private" を設定しておくことで、常にプライベート環境で実行されることが保証されます。
よくあるエラーと対処法
エラー 1:/document/content が空
Could not execute skill because one or more skill input was invalid.
Required skill input is missing or empty. Name: 'text', Source: '/document/content'.
原因: AI Search のインデクサーが Storage Account の Blob コンテンツを読み取れていない。Storage の publicNetworkAccess: Disabled が原因で、SPL(groupId: blob)が未作成。
対処: Storage Account に対する Shared Private Link(groupId: blob)を作成・承認する。
エラー 2:スキル実行で 403 Forbidden
Web Api response status: 'Forbidden', Web Api response details:
'{"error":{"code":"403","message": "Public access is disabled. Please configure private endpoint."}}'
原因: スキルセットが AI Services にアクセスする際、API Key 認証を使っているか、SPL が未作成。
対処:
- AI Services に対する Shared Private Link(
groupId: openai_account)を作成・承認する - スキルセットの
apiKeyを削除し、マネージド ID 認証に切り替える
エラー 3:ベクトル化で 401 Unauthorized(検索時)
Could not complete vectorization action. The vectorization endpoint returned status code '401' (Unauthorized).
原因: インデックスの ベクトライザー設定(クエリ時の統合ベクトル化)で、AI Search のマネージド ID に AI Services への権限がないか、ネットワーク的にアクセスできない。
対処:
- AI Search のシステム割り当てマネージド ID に Cognitive Services OpenAI User ロールを付与
- AI Services に対する Shared Private Link(
groupId: openai_account)が承認済みであることを確認
Edge ブラウザの Private Network Access (PNA) 問題
事象
Jumpbox VM 内の Microsoft Edge で https://ai.azure.com(Foundry ポータル)にアクセスし、Agent を作成しようとするとブロックされる現象が発生しました。
原因
Chromium ベースのブラウザ(Edge / Chrome)には Private Network Access (PNA) というセキュリティ機能があります。
ブラウザの判断ロジック:
ai.azure.com のパブリック IP を DNS 解決 → 「パブリックサイト」と認識
↓
ページ内で Private Endpoint のプライベート IP (10.x.x.x) へリクエスト
↓
PNA: 「パブリックサイトからプライベートネットワークへのアクセスはブロック」
Private DNS Zone により *.cognitiveservices.azure.com がプライベート IP に解決されるため、Foundry ポータル (ai.azure.com) からの API 呼び出しが PNA によりブロックされます。
解決
2026年4月末時点で、Microsoft がサーバーサイドで修正を適用し、新しく作成した Jumpbox では Edge でも問題なくアクセスできるようになりました。
もし同様の問題に遭遇した場合の回避策:
- Chrome を使用する(PNA の適用ポリシーが異なる場合がある)
-
edge://flags/#block-insecure-private-network-requestsを Disabled に設定する(検証用途のみ)
この問題は Microsoft 側のサーバー対応(PNA preflight ヘッダーの付与やポータル BFF パターンの変更)により解消される過渡的な問題です。Jumpbox を再作成した場合に解消する場合があります。
Bicep テンプレート構成
foundry-byo-vnet-basic/
├── azure.yaml ← azd プロジェクト定義
├── main.bicep ← エントリポイント(全モジュールのオーケストレーション)
├── main.bicepparam ← パラメータファイル(azd 環境変数連携)
├── modules/
│ ├── network-agent-vnet.bicep ← VNet / サブネット / NSG
│ ├── bastion.bicep ← Azure Bastion + Public IP
│ ├── jumpbox-vm.bicep ← Jumpbox VM + NIC
│ ├── ai-account-identity.bicep ← AI Services Account + モデルデプロイ
│ ├── standard-dependent-resources.bicep ← Cosmos DB / AI Search / Storage
│ ├── private-endpoint-and-dns.bicep ← Private Endpoint + DNS Zone
│ ├── ai-project-identity.bicep ← Foundry Project + Connections
│ ├── add-project-capability-host.bicep ← ★ Capability Host
│ ├── ai-search-shared-private-links.bicep ← ★ Shared Private Link (SPL)
│ ├── ai-search-openai-role-assignment.bicep ← AI Search MI → OpenAI User ロール
│ ├── azure-storage-account-role-assignment.bicep
│ ├── cosmosdb-account-role-assignment.bicep
│ ├── ai-search-role-assignments.bicep
│ ├── blob-storage-container-role-assignments.bicep ← 条件付き ABAC ロール
│ ├── cosmos-container-role-assignments.bicep
│ └── deployer-user-role-assignments.bicep
└── scripts/
├── create-prompt-agent.py ← Prompt Agent 作成スクリプト
└── requirements.txt
まとめ
Azure AI Foundry を BYO VNet(完全閉域)で構築する検証を行いました。主な学びは以下の通りです。
-
ネットワーク設計が最重要:
publicNetworkAccess: Disabled+networkAcls+ Private Endpoint + Private DNS Zone の多層防御で閉域環境を実現する -
bypass: AzureServices: ファイアウォールを Deny にしつつ、Azure 信頼サービス(監視・診断等)のアクセスは例外で許可する実用的な設定 - Capability Host はバックエンド接続の橋渡し: Project の Connections を通じて Cosmos DB / Storage / AI Search を Agent に接続する。デプロイ順序(ロール割り当て → CapHost → Post-CapHost ロール)が重要
-
networkInjectionsは後付け不可: BYO VNet 構成はアカウント作成時に決定する必要がある - Private Endpoint ≠ Shared Private Link: AI Search のインデクサー/スキルセットは VNet の PE を使えない。別途 SPL の作成が必要
- SPL 経由では API Key 認証が不可: スキルセットの認証はマネージド ID + RBAC に切り替える必要がある
-
executionEnvironment: "private"を明示設定: SPL 利用時はインデクサーのプライベート実行環境を明示的に指定するべき - コスト構造: Bastion (~$140) + AI Search Basic (~$70) が固定費の大半。検証用途なら使わない時間帯に Bastion を削除する運用が有効
おまけ
Foundry Agent に依存したリソースの中身
Cosmos DB
AI Search
Agent定義でファイルアップロードすると、Indexも裏で作られます。

直接検索を試す場合には、JSON クエリエディターを使います。
{
"search": "*",
"top": 5,
"count": true,
"select": "DocumentChunkId,Text,SourceName,Url,CreatedAt"
}
Storage Account(Blob)
EvaluationなどでファイルがBlobに保存されます。
Subnet
Foundry画面
Foundry Portal
モデルとChat PlaygroundとAPI
以下の手順でPowerShellからAPI接続。
# Azure CLI インストール (約2-3分)
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindowsx64 -OutFile .\AzureCLI.msi
Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
Remove-Item .\AzureCLI.msi
# PowerShell を閉じて再度開く(PATHを反映)
az login --use-device-code
$token = az account get-access-token --resource https://cognitiveservices.azure.com --query accessToken -o tsv
$headers = @{ "Authorization" = "Bearer $token" }
$uri = "https://<resource>.openai.azure.com/openai/deployments/gpt-41-mini/chat/completions?api-version=2024-10-21"
$body = '{"messages":[{"role":"user","content":"Hello"}],"max_tokens":50}'
Invoke-RestMethod -Uri $uri -Headers $headers -Method Post -Body $body -ContentType "application/json"
choices : {@{content_filter_results=; finish_reason=stop; index=0; logprobs=; message=}}
created : 1776935282
id : chatcmpl-DXkKQ4AHDwuHILptc3J3pGvlwgmYQ
model : gpt-4.1-mini-2025-04-14
object : chat.completion
prompt_filter_results : {@{prompt_index=0; content_filter_results=}}
service_tier : default
system_fingerprint : fp_b6f445fc1c
usage : @{completion_tokens=10; completion_tokens_details=; prompt_tokens=8; prompt_tokens_details=;
total_tokens=18}
Agent
Foundry IQ でのナレッジベース作成
Foundry Portalからナレッジベース作成(Blobから作成し、Reasoning Effort=minimal)した後に、以下を変更。
- SkillSetのEmbedding SkillがAPI Key認証だったのでMI認証に変更(上述)
- エラーは起きないが、Indexer をPrivate環境で実行するように変更(上述)







