本記事記述および実装内容は、多くの部分をAIで処理しており、人間チェックは甘いです
Azure AI Searchを完全Entra化してキーゼロへ
はじめに
以前書いた Key 認証版の記事のフォローアップです。
元記事では、Azure AI Search、Document Intelligence、Azure OpenAI、Storage を組み合わせて、画像を含む PDF をインデックス化しました。一方で PoC らしく、AI Search admin key、Storage connection string、Azure OpenAI key、Document Intelligence key を使う構成でした。
この記事では、同等の PDF 取り込み・画像抽出・ベクトル検索・画像キャプション生成を維持しつつ、認証経路を Entra ID / Managed Identity に置き換えた検証結果をまとめます。
ゴールは「アプリコードから key を消す」だけではありません。Azure リソース側でも Search / Storage / Foundry のローカル認証を無効化して、key 前提の逃げ道を閉じることを狙いました。
結論
- ✅ AI Search REST、Storage、Document Intelligence Layout、Azure OpenAI Embedding / Vectorizer / Chat の 6 経路を Entra ID 化できた
- ✅ AI Search の
disableLocalAuth=trueと Storage のallowSharedKeyAccess=falseで「キーゼロ」構成にできた - ✅ Document Intelligence と Azure OpenAI は Foundry account
kind=AIServices1 つに寄せ、AIServicesByIdentityでまとめて Managed Identity 化できた - ✅
ChatCompletionSkillは WebApiSkill 派生なので、authResourceIdに Foundry リソース ID を指定するのがポイントだった - ⚠️
japaneastではgpt-5-mini/gpt-4o-miniの Standard SKU が通らず、検証ではgpt-4.1-miniへフォールバックした - ⚠️
.httpの$aadV2Token自動取得は別テナントを掴むことがあり、最終的には az CLI で明示取得する方が確実だった
アーキテクチャ
| リソース | 用途 | 認証方針 |
|---|---|---|
| Azure AI Search | Index / DataSource / Skillset / Indexer / Vector search | System-assigned Managed Identity + local auth disabled |
| ADLS Gen2 Storage | PDF 入力 docs、画像出力 images
|
Shared Key disabled + OAuth default |
| Foundry / AI Services | Document Intelligence Layout、Embedding、Chat | System-assigned Managed Identity + local auth disabled |
| 開発者クライアント | Search REST、PDF upload、検索確認 | Entra ID token + RBAC |
Entra 化した 6 経路
| # | 経路 | Key 認証版 | Entra ID 化後 |
|---|---|---|---|
| 1 | Client → AI Search REST |
api-key ヘッダー |
Authorization: Bearer <token> |
| 2 | AI Search → Storage read | Storage connection string | DataSource ResourceId=<storage-resource-id>;
|
| 3 | AI Search → Storage write | KnowledgeStore の Storage key | storageConnectionString=ResourceId=...; |
| 4 | AI Search → Document Intelligence Layout | AIServicesByKey |
AIServicesByIdentity |
| 5 | AI Search → AOAI Embedding / Vectorizer | apiKey |
resourceUri + deployment ID + Search System MI |
| 6 | AI Search → AOAI Chat | AOAI key |
ChatCompletionSkill + authResourceId
|
特に 2 と 3 は似ていますが、DataSource の読み取りと KnowledgeStore / Image Projection の書き込みで別々に確認しました。
キーゼロ 3 点セット
今回の肝は「呼び出し側で key を使わない」だけでなく、「サービス側でも key 認証を受け付けない」ようにしたことです。
| 対象 | 設定 | 効果 |
|---|---|---|
| Storage |
allowSharedKeyAccess=false, defaultToOAuthAuthentication=true
|
Account Key / Shared Key 前提のアクセスを排除 |
| AI Search | disableLocalAuth=true |
admin key / query key を無効化 |
| Foundry / AI Services | disableLocalAuth=true |
AOAI / Document Intelligence の key 利用を排除 |
disableLocalAuth=true にすると、Azure Portal の Search Explorer なども key ではなく Entra ログイン前提になります。「あとで key で手早く確認する」運用はできません。
RBAC マトリクス
AI Search Managed Identity に必要な 3 ロール
| Role | Scope | 目的 |
|---|---|---|
Storage Blob Data Contributor |
<storage> |
DataSource read と Image Projection write |
Cognitive Services User |
<foundry> |
Document Intelligence Layout / AIServicesByIdentity
|
Cognitive Services OpenAI User |
<foundry> |
Embedding Skill、Vectorizer、ChatCompletionSkill |
Storage Blob Data Reader と Storage Blob Data Contributor を分ける設計もできますが、今回は Image Projection の write が必要なため Contributor 1 本に集約しました。
検証ユーザに必要な 4 ロール
| Role | Scope | 目的 |
|---|---|---|
Search Index Data Contributor |
<search> |
Index / document / search data plane 操作 |
Search Service Contributor |
<search> |
DataSource / Skillset / Indexer 管理 |
Storage Blob Data Contributor |
<storage> |
テスト PDF upload、Blob list |
Cognitive Services OpenAI User |
<foundry> |
AOAI 呼び出し確認 |
検証ユーザは <検証ユーザ> として扱い、実ユーザ名やテナント固有情報は記事から除外しています。
Bicep の核心
Storage: Shared Key を無効化する
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
tags: tags
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
properties: {
isHnsEnabled: true
allowBlobPublicAccess: false
allowSharedKeyAccess: false
minimumTlsVersion: 'TLS1_2'
publicNetworkAccess: 'Enabled'
supportsHttpsTrafficOnly: true
defaultToOAuthAuthentication: true
}
}
allowSharedKeyAccess=false と defaultToOAuthAuthentication=true の組み合わせで、Blob 操作を Entra ID 前提に寄せます。
AI Search: System MI と local auth disabled
resource search 'Microsoft.Search/searchServices@2024-03-01-preview' = {
name: searchName
location: location
tags: tags
sku: {
name: 'basic'
}
identity: {
type: 'SystemAssigned'
}
properties: {
replicaCount: 1
partitionCount: 1
hostingMode: 'default'
publicNetworkAccess: 'enabled'
disableLocalAuth: true
authOptions: null
semanticSearch: 'free'
}
}
disableLocalAuth は Microsoft.Search/searchServices@2024-03-01-preview で設定しました。これにより admin key / query key 前提の Search REST 操作を封じます。
Foundry / AI Services: kind=AIServices でまとめる
resource foundry 'Microsoft.CognitiveServices/accounts@2024-10-01' = {
name: foundryName
location: location
tags: tags
kind: 'AIServices'
sku: {
name: 'S0'
}
identity: {
type: 'SystemAssigned'
}
properties: {
customSubDomainName: foundryName
disableLocalAuth: true
publicNetworkAccess: 'Enabled'
}
}
customSubDomainName は Entra ID 認証で重要です。Skillset 側の subdomainUrl も https://<foundry>.cognitiveservices.azure.com 形式にそろえます。
RBAC の Bicep 抜粋
resource searchMiBlobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(storage.id, searchPrincipalId, storageBlobDataContributor)
scope: storage
properties: {
principalId: searchPrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributor)
principalType: 'ServicePrincipal'
}
}
resource searchMiOpenAIUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(foundry.id, searchPrincipalId, cognitiveServicesOpenAIUser)
scope: foundry
properties: {
principalId: searchPrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUser)
principalType: 'ServicePrincipal'
}
}
resource searchMiCogServicesUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(foundry.id, searchPrincipalId, cognitiveServicesUser)
scope: foundry
properties: {
principalId: searchPrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUser)
principalType: 'ServicePrincipal'
}
}
スキルセット定義の核心
AIServicesByIdentity
Document Intelligence Layout Skill を key なしで動かすには、Skillset の cognitiveServices を AIServicesByIdentity にします。
{
"@odata.type": "#Microsoft.Azure.Search.AIServicesByIdentity",
"subdomainUrl": "https://<foundry>.cognitiveservices.azure.com"
}
これで Document Intelligence Layout の呼び出しも AI Search の System MI 経由になります。
Embedding Skill / Vectorizer は key フィールドを送らない
"azureOpenAIParameters": {
"resourceUri": foundry_endpoint.rstrip("/"),
"deploymentId": embedding_deployment,
"modelName": embedding_deployment,
"authIdentity": None,
}
{
"@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
"name": "embedding",
"resourceUri": foundry_endpoint_clean,
"deploymentId": embedding_deployment,
"modelName": embedding_deployment,
"authIdentity": None,
}
レスポンス上は schema の都合で apiKey: null が返る場合がありますが、リクエスト body には key 値を送っていません。
ChatCompletionSkill は authResourceId がポイント
ChatCompletionSkill は WebApiSkill 派生です。そのため、Managed Identity で呼ぶには authResourceId に Foundry リソース ID を指定します。
{
"@odata.type": "#Microsoft.Skills.Custom.ChatCompletionSkill",
"name": "image-captioner",
"uri": "https://<foundry>.cognitiveservices.azure.com/openai/deployments/gpt-4.1-mini/chat/completions?api-version=2024-10-21",
"authResourceId": "/subscriptions/<subscription-id>/resourceGroups/rg-<prefix>-dev-japaneast/providers/Microsoft.CognitiveServices/accounts/<foundry>",
"authIdentity": None,
}
authIdentity: None は「System-assigned Managed Identity を使う」という意図です。User-assigned MI を使う場合は別途 identity の指定方法を検討します。
.http ファイルで AAD トークンを自動化したかった話
VS Code REST Client では $aadV2Token を使うと AAD トークンを自動取得できます。最初は次のように書けば Search REST を key なしで叩ける想定でした。
@search_endpoint = https://<search>.search.windows.net
@aad_token = {{$aadV2Token new public <tenant-id> scopes:https://search.azure.com/.default}}
GET {{search_endpoint}}/indexes?api-version=2025-09-01
Authorization: Bearer {{aad_token}}
ところが、VS Code 側の Microsoft 認証プロバイダが別テナントのアカウントを掴み、AADSTS1001010 になることがありました。<tenant-id> を書いていても、ローカルに複数テナントのログイン状態があると再現しやすいです。
.http の $aadV2Token は便利ですが、複数テナントを行き来する開発環境では「意図したテナントの token か」を毎回確認した方が安全です。
検証では、最終的に az CLI で tenant / subscription を明示して token を取る運用に寄せました。
az account set --subscription <subscription-id>
az account get-access-token \
--tenant <tenant-id> \
--resource https://search.azure.com \
--query accessToken \
-o tsv
REST Client の変数に貼る場合は次の形です。
@aad_token = <paste-az-cli-token-here>
PUT {{search_endpoint}}/indexes/{{index_name}}?api-version={{api_version}}
Authorization: Bearer {{aad_token}}
Content-Type: application/json
アクセストークンの有効期限は短いので、自動化としては Python の DefaultAzureCredential().get_token("https://search.azure.com/.default") を使う方が扱いやすかったです。
Document Intelligence は Foundry に同梱される
今回の構成では、Portal 上に「Document Intelligence」という単独リソースは出てきません。これは異常ではなく、Foundry / Cognitive Services account を kind=AIServices で作成し、そのアカウント内の capability として Document Intelligence Layout を使っているためです。
az cognitiveservices account show \
--resource-group rg-<prefix>-dev-japaneast \
--name <foundry> \
--query "{kind:kind,endpoints:properties.endpoints}" \
-o json
期待する見え方は次のようなイメージです。
{
"kind": "AIServices",
"endpoints": {
"FormRecognizer": "https://<foundry>.cognitiveservices.azure.com/"
}
}
FormRecognizer endpoint が返っていれば、Document Intelligence Layout Skill で使う capability があると判断できます。
動作確認のコツ
LayoutSkill が呼ばれた証拠
検索結果の content に次のようなタグが残っていれば、Document Intelligence Layout Skill が PDF 内の図を Markdown 化できています。
<figure>
<figcaption>Entra ID Auth Flow</figcaption>
...
</figure>
今回の検証では、検索結果に text chunk と image chunk が両方入り、images コンテナにも JPEG blob が生成されました。
key を送っていないことの確認
REST / スクリプト側は次を確認しました。
grep -R "api-key" scripts http || true
grep -R "AccountKey=\|SharedAccessSignature=" scripts http || true
Search object の GET レスポンスでは、DataSource の connectionString が null またはマスクされます。これは失敗ではなく、秘匿値が返らない正常な挙動です。
japaneast のモデル fallback
japaneast では、検証時点で gpt-5-mini と gpt-4o-mini の Standard SKU が通りませんでした。最終的に gpt-4.1-mini / version 2025-04-14 にフォールバックして preflight / deploy を通しました。
azd env set CHAT_MODEL_NAME gpt-4.1-mini
azd env set CHAT_MODEL_VERSION 2025-04-14
azd provision --preview --environment dev
モデル提供状況はリージョンと時点で変わります。Bicep にモデル名を固定する場合でも、preflight で provider validation を通し、失敗時の fallback を明示しておくのが安全です。
その他のハマりどころ
azd 1.24 の azure.ai.agents extension が false-negative になった
Foundry Project ではなく Foundry Account (kind=AIServices) のみを作る構成では、azd up の postdeploy hook が AZURE_AI_PROJECT_ENDPOINT を期待して失敗しました。
Azure リソース自体は Succeeded だったため、CI では exit code だけで失敗判定せず、必要に応じて resource state を確認するのがよさそうです。
AI Search Basic の作成が遅い
今回の検証では、AI Search Basic の作成が全体のリードタイムを大きく支配しました。短時間で何度も作り直す検証では、Search 作成に 10 分程度を見込んだ方がよさそうです。
DataSource の ResourceId=...; は末尾セミコロン必須
ADLS Gen2 DataSource を Entra ID 化する場合、connection string は次の形にします。
ResourceId=/subscriptions/<subscription-id>/resourceGroups/rg-<prefix>-dev-japaneast/providers/Microsoft.Storage/storageAccounts/<storage>;
末尾の ; を忘れないこと、AccountKey= や SharedAccessSignature= を含めないことがポイントです。
検証結果サマリ
10 シナリオすべて PASS でした。
| ID | シナリオ | 結果 | メモ |
|---|---|---|---|
| VS-01 | Bicep デプロイ成功 | ✅ PASS | リソース状態は Succeeded。azd up は postdeploy false-negative |
| VS-02 | AI Search MI RBAC | ✅ PASS | Search MI の必須 3 ロールを確認 |
| VS-03 | 検証ユーザ RBAC | ✅ PASS | ユーザの必須 4 ロールを確認 |
| VS-04 | Bearer Token で AI Search REST PUT | ✅ PASS |
api-key なしで PUT 成功 |
| VS-05 | ADLS Gen2 DataSource Entra 作成 | ✅ PASS |
AccountKey / SAS なし |
| VS-06 | Skillset 作成 | ✅ PASS |
AIServicesByIdentity を確認 |
| VS-07 | Indexer 実行で PDF 処理 | ✅ PASS |
itemsProcessed=1, itemsFailed=0
|
| VS-08 | Bearer Token 検索 | ✅ PASS | text chunk と image chunk を確認 |
| VS-09 | Image Projection 書き込み | ✅ PASS |
images コンテナに JPEG 生成 |
| VS-10 | 削除手順確認 | ✅ PASS |
azd down --force --purge 手順を確認 |
パフォーマンスメモ
| メトリクス | 結果 |
|---|---|
| Indexer API 上の実行時間 | 約 15 秒 |
| Indexer wall clock | 約 1 分 |
| items processed / failed | 1 / 0 |
| text chunks / image chunks | 4 / 1 |
| text-only search p50 | 1 秒未満 |
| hybrid search | 1 秒台 |
少量データかつ初回検証のため、上記は傾向値です。Basic SKU、初回 token 取得、Vectorizer / ChatCompletionSkill の呼び出しが絡むため、本番評価では p50 / p95 を複数回測る必要があります。
コストについては実請求値を載せません。傾向としては、アイドル時でも AI Search Basic の固定費が支配的になりやすいです。短時間検証では azd down --force --purge で Search の稼働時間を短くするのが効きます。
再現時の最小コマンド
az bicep build --file infra/main.bicep --stdout > /dev/null
azd env new dev --location japaneast --subscription <subscription-id>
azd env set AZURE_PRINCIPAL_ID <principal-id>
azd env set CHAT_MODEL_NAME gpt-4.1-mini
azd env set CHAT_MODEL_VERSION 2025-04-14
azd provision --preview --environment dev
cd scripts
uv sync
uv run python generate_test_pdf.py
uv run python upload_pdf.py
uv run python create_resources.py
uv run python run_indexer.py
uv run python search_test.py
削除する場合は、Foundry / Cognitive Services 系の soft-delete による名前衝突を避けるため --purge まで付けます。
azd down --force --purge
まとめ
Key 認証版の AI Search × Document Intelligence × AOAI 構成を、Entra ID / Managed Identity ベースへ置き換えられることを確認しました。
特に重要だったのは次の 6 点です。
- Storage は
allowSharedKeyAccess=false - AI Search と Foundry / AI Services は
disableLocalAuth=true - Document Intelligence は
AIServicesByIdentity - ChatCompletionSkill は
authResourceId=<foundry-resource-id> - RBAC は Search MI 用 3 ロール、開発者用 4 ロールを明示する
-
.httpの AAD token 自動化は便利だが、複数テナント環境では az CLI fallback を用意する
次にやるなら、Private Endpoint / firewall を含めた閉域化、User-assigned Managed Identity 化、継続的な性能測定、リージョン別モデル fallback 戦略を検証したいです。
