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?

Azure AI Searchを完全Entra化してキーゼロへ

0
Last updated at Posted at 2026-05-14

本記事記述および実装内容は、多くの部分を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=AIServices 1 つに寄せ、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 で明示取得する方が確実だった

アーキテクチャ

image.png

リソース 用途 認証方針
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 ReaderStorage 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 を無効化する

infra/modules/storage.bicep
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=falsedefaultToOAuthAuthentication=true の組み合わせで、Blob 操作を Entra ID 前提に寄せます。

AI Search: System MI と local auth disabled

infra/modules/search.bicep
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'
  }
}

disableLocalAuthMicrosoft.Search/searchServices@2024-03-01-preview で設定しました。これにより admin key / query key 前提の Search REST 操作を封じます。

Foundry / AI Services: kind=AIServices でまとめる

infra/modules/foundry.bicep
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 側の subdomainUrlhttps://<foundry>.cognitiveservices.azure.com 形式にそろえます。

RBAC の Bicep 抜粋
infra/modules/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 の cognitiveServicesAIServicesByIdentity にします。

skillset.cognitiveServices.json
{
  "@odata.type": "#Microsoft.Azure.Search.AIServicesByIdentity",
  "subdomainUrl": "https://<foundry>.cognitiveservices.azure.com"
}

これで Document Intelligence Layout の呼び出しも AI Search の System MI 経由になります。

Embedding Skill / Vectorizer は key フィールドを送らない

scripts/create_resources.py
"azureOpenAIParameters": {
    "resourceUri": foundry_endpoint.rstrip("/"),
    "deploymentId": embedding_deployment,
    "modelName": embedding_deployment,
    "authIdentity": None,
}
scripts/create_resources.py
{
    "@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 を指定します。

scripts/create_resources.py
{
    "@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 なしで叩ける想定でした。

http/ai-search-entra.http
@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 を取る運用に寄せました。

get-search-token.sh
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 の変数に貼る場合は次の形です。

http/ai-search-entra.http
@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 を使っているためです。

check-foundry-capabilities.sh
az cognitiveservices account show \
  --resource-group rg-<prefix>-dev-japaneast \
  --name <foundry> \
  --query "{kind:kind,endpoints:properties.endpoints}" \
  -o json

期待する見え方は次のようなイメージです。

foundry-endpoints.example.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 化できています。

search-result-snippet.html
<figure>
<figcaption>Entra ID Auth Flow</figcaption>
...
</figure>

今回の検証では、検索結果に text chunk と image chunk が両方入り、images コンテナにも JPEG blob が生成されました。

key を送っていないことの確認

REST / スクリプト側は次を確認しました。

no-api-key-check.sh
grep -R "api-key" scripts http || true
grep -R "AccountKey=\|SharedAccessSignature=" scripts http || true

Search object の GET レスポンスでは、DataSource の connectionStringnull またはマスクされます。これは失敗ではなく、秘匿値が返らない正常な挙動です。

japaneast のモデル fallback

japaneast では、検証時点で gpt-5-minigpt-4o-mini の Standard SKU が通りませんでした。最終的に gpt-4.1-mini / version 2025-04-14 にフォールバックして preflight / deploy を通しました。

model-fallback.sh
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 は次の形にします。

datasource-connection-string.txt
ResourceId=/subscriptions/<subscription-id>/resourceGroups/rg-<prefix>-dev-japaneast/providers/Microsoft.Storage/storageAccounts/<storage>;

末尾の ; を忘れないこと、AccountKey=SharedAccessSignature= を含めないことがポイントです。

検証結果サマリ

10 シナリオすべて PASS でした。

ID シナリオ 結果 メモ
VS-01 Bicep デプロイ成功 ✅ PASS リソース状態は Succeededazd 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 の稼働時間を短くするのが効きます。

再現時の最小コマンド

preflight.sh
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
smoke-test.sh
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 まで付けます。

cleanup.sh
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 戦略を検証したいです。

参考リンク

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?