Terraform パイプラインはグリーンです。デプロイはエラーなしで完了します。あなたはコーヒーを取りに行きます。
10分後、新しい Enterprise RAG アプリケーションをテストします。403 Forbidden が返ってきます。Azure Portal を開き、OpenAI の Networking タブを確認すると、そこにあるのはこれです。AI Search からの Shared Private Link が Pending のままになっています。
Terraform にそれを承認するよう指示した人はいません。そもそも、承認が必要だということすら誰もあなたに言っていません。
これは Azure AI インフラの CI/CD キラーです。
Why It Happens
AI Search はデータをベクタライズするために OpenAI を呼び出さなければなりません。azurerm プロバイダーはこの Shared Private Link を 要求 することには成功しますが、自分自身の要求を 承認 することはできません。対象リソース(OpenAI)は、着信接続を明示的に受け入れる必要があります。標準のプロバイダーにはこの方法がありません。パイプラインはデッドロックします。誰かが Portal で手動で「Approve」をクリックしなければなりません。2026年の ClickOps。
The AzAPI State Machine Fix
私たちは azurerm プロバイダーを完全に迂回し、azapi プロバイダーを使って Azure Resource Manager の REST API に直接話します。
Azure は着信接続にランダムな GUID を生成するため、ID をハードコードできません。そこで実行時に読み取ります:
data "azapi_resource_list" "pe_connections" {
type = "Microsoft.CognitiveServices/accounts/privateEndpointConnections@2023-05-01"
parent_id = azurerm_cognitive_account.openai.id
response_export_values = ["value"]
depends_on = [azurerm_search_shared_private_link_service.openai_link]
}
depends_on は重要です。これがないと Terraform はリンクがそもそも要求される前に接続リストを問い合わせて空を返し、承認が黙って失敗します。
次に、Pending の接続だけをフィルタして承認します:
resource "azapi_update_resource" "approve_shared_link" {
type = "Microsoft.CognitiveServices/accounts/privateEndpointConnections@2023-05-01"
resource_id = try(
[for conn in jsondecode(data.azapi_resource_list.pe_connections.output).value :
conn.id
if conn.properties.privateLinkServiceConnectionState.status == "Pending"
][0],
""
)
body = jsonencode({
properties = {
privateLinkServiceConnectionState = {
status = "Approved"
description = "Approved via Terraform AzAPI Pipeline"
}
}
})
}
try() のラッパーは必須です。terraform destroy では Shared Private Link がこのリソースの評価前に削除されます。try() がないと、空の配列に対して [0] のインデックス参照を行うことで destroy の実行がクラッシュし、Azure に孤立(オーファン)したリソースが残ります。
terraform apply の後、このリンクは 30秒以内に Pending から Approved に切り替わります。Portal アクセスは不要です。
Identity Chaining — Kill the API Keys
リンクの自動承認により、AI Search は OpenAI に到達できるようになります。しかし固定の API キーでは、コンプライアンス監査が失敗します。キーは漏えいします。キーは Git にコミットされます。
ローカル認証を無効化し、代わりに System Managed Identity を使います:
resource "azurerm_search_service" "search" {
name = var.search_service_name
sku = "standard"
public_network_access_enabled = false
local_authentication_enabled = false
identity {
type = "SystemAssigned"
}
}
resource "azurerm_role_assignment" "search_to_openai" {
scope = azurerm_cognitive_account.openai.id
role_definition_name = "Cognitive Services OpenAI User"
principal_id = azurerm_search_service.search.identity[0].principal_id
}
資格情報(クレデンシャル)の管理はゼロです。AI Search インスタンスが削除されると、その ID と権限も自動的に破棄されます。
Don't Forget Private DNS
your-instance.openai.azure.com がまだパブリック IP に解決される場合、Azure Firewall がトラフィックを遮断し、別の判別しにくい 403 が返ってきます。両サービスの DNS ゾーンを VNet にリンクする必要があります:
resource "azurerm_private_dns_zone" "openai_dns" {
name = "privatelink.openai.azure.com"
resource_group_name = var.resource_group_name
}
resource "azurerm_private_dns_zone_virtual_network_link" "openai_vnet_link" {
name = "link-openai-vnet"
resource_group_name = var.resource_group_name
private_dns_zone_name = azurerm_private_dns_zone.openai_dns.name
virtual_network_id = azurerm_virtual_network.vnet.id
registration_enabled = false
}
registration_enabled = false — 常に。集中型の Hub & Spoke による DNS 管理とは自動登録が競合します。
無料のベースライン(AzAPI の自動承認 + 基本的なネットワーキング)は GitHub にあります。完全版の記事(VNet 注入の全内容、Private DNS の自動化、RBAC Identity チェイニング)は私のブログにあります。



