0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft Foundry を BYO VNet(閉域ネットワーク)で構築

0
Posted at

はじめに

本記事記述および実装内容は、多くの部分を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

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

image.png

modules/network-agent-vnet.bicep(抜粋)
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-agentMicrosoft.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 のネットワーク設定

networkAclsbypass: AzureServices

AI Services アカウントでは、ネットワークアクセスを 2 つの層 で制御しています。

modules/ai-account-identity.bicep(抜粋)
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 の核心)

networkInjectionsAI Services アカウント作成時にのみ設定可能 です。既存のアカウントに後から追加することはできません。VNet 構成を変更したい場合はアカウントの再作成が必要です。

image.png

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 の定義は以下の通りです。

modules/add-project-capability-host.bicep
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)を定義しておく必要があります。

modules/ai-project-identity.bicep(抜粋)
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 というリソースタイプを使用します。

modules/cosmos-container-role-assignments.bicep
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(属性ベースのアクセス制御)条件 を使って、エージェント用コンテナのみにアクセスを限定しています。

modules/blob-storage-container-role-assignments.bicep(抜粋)
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 が空の場合はスキップされます。

main.bicep(抜粋)
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

azdaz 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 を使います。

接続手順

  1. Azure Portal → Bastion → Jumpbox VM に RDP 接続
  2. Jumpbox VM 内で Python 環境をセットアップ
  3. az login で認証
  4. スクリプトを実行

📸 【画面キャプチャ⑧】 Azure Portal → Bastion → Jumpbox VM への接続画面

Python スクリプト

scripts/create-prompt-agent.py
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()
Jumpbox VM 内で実行
# 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 に解決されることを確認します。

Jumpbox VM 内で実行
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 も使わない時は 停止(割り当て解除) することでコンピューティング課金を止められます。

既知の制限事項・ハマりポイント

  1. networkInjections は後付け不可 — AI Services アカウント作成時に VNet 構成を決める必要がある。変更するにはアカウント再作成が必要
  2. エージェントサブネットは専用snet-agent は他のリソースと共有できない(Microsoft.App/environments 委任が必要なため)
  3. 全リソースは同一リージョン必須 — Cosmos DB, Storage, AI Search, Foundry, VNet すべて同じリージョンに配置
  4. 削除順序に注意 — Foundry リソース削除 → 完全消去(Purge)→ その後 VNet 削除の順序を守ること。逆順だと削除が失敗する
  5. AI Search の Free SKU は使えない — Private Endpoint に対応していないため、最低 Basic SKU が必要
  6. SPL は手動承認が必要 — Shared Private Link を Bicep で作成した後、対象リソース側のプライベートエンドポイント接続を手動承認する必要がある(Bicep では自動承認不可)
  7. Azure CLI の SPL バリデーションが古いaz search shared-private-link-resource createopenai_account の groupId を受け付けない(2026年4月時点)。REST API(az rest)を使うか、Bicep で直接定義する
  8. スキルセットの 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 を直接使用してください。

Shared Private Link の作成(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"
    }
  }'

作成後、対象リソースの「プライベートエンドポイント接続」から 手動で承認 が必要です。

PE接続の承認
# 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 では使用できません。

https://learn.microsoft.com/ja-jp/azure/search/search-indexer-howto-access-private?tabs=portal-create

スキルセットの認証:API Key ではなくマネージド ID を使う

Shared Private Link 経由で AI Services にアクセスする場合、API Key 認証は動作しません。マネージド ID 認証を使用する必要があります。

❌ 動作しない設定(API Key)
{
  "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
  "resourceUri": "https://{aiServicesName}.openai.azure.com",
  "apiKey": "<API Key>",
  "deploymentId": "text-embedding-3-small"
}
✅ 正しい設定(マネージド ID)
{
  "@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 が未作成。

対処:

  1. AI Services に対する Shared Private Link(groupId: openai_account)を作成・承認する
  2. スキルセットの apiKey を削除し、マネージド ID 認証に切り替える

エラー 3:ベクトル化で 401 Unauthorized(検索時)

Could not complete vectorization action. The vectorization endpoint returned status code '401' (Unauthorized).

原因: インデックスの ベクトライザー設定(クエリ時の統合ベクトル化)で、AI Search のマネージド ID に AI Services への権限がないか、ネットワーク的にアクセスできない。

対処:

  1. AI Search のシステム割り当てマネージド ID に Cognitive Services OpenAI User ロールを付与
  2. AI Services に対する Shared Private Link(groupId: openai_account)が承認済みであることを確認

Edge ブラウザの Private Network Access (PNA) 問題

事象

Jumpbox VM 内の Microsoft Edgehttps://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-requestsDisabled に設定する(検証用途のみ)

この問題は 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

Agent定義や会話履歴が入っています。
image.png

AI Search

Agent定義でファイルアップロードすると、Indexも裏で作られます。
image.png

直接検索を試す場合には、JSON クエリエディターを使います。

{
  "search": "*",
  "top": 5,
  "count": true,
  "select": "DocumentChunkId,Text,SourceName,Url,CreatedAt"
}

image.png

Storage Account(Blob)

EvaluationなどでファイルがBlobに保存されます。

image.png

Subnet

image.png

Foundry画面

Foundry Portal

モデルとChat PlaygroundとAPI

Portalから見えている。モデルも確認。
image.png

以下の手順で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

参照、更新、Playground使用可能。
image.png

Foundry IQ でのナレッジベース作成

Foundry Portalからナレッジベース作成(Blobから作成し、Reasoning Effort=minimal)した後に、以下を変更。

  • SkillSetのEmbedding SkillがAPI Key認証だったのでMI認証に変更(上述)
  • エラーは起きないが、Indexer をPrivate環境で実行するように変更(上述)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?