ゼロトラストRAG:AzureのTerraformで共有プライベートリンクのデッドロックを打ち破る

Dev.to / 2026/5/21

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • AzureのTerraformによるゼロトラストRAGのデプロイは成功しても、アプリ側では403エラーになることがあり、その原因はAzure AI Searchが必要とするOpenAIの共有プライベートリンクがPendingのままになっているためです。
  • 根本原因は、標準のazurerm Terraformプロバイダーがプライベートエンドポイント接続の「要求」はできる一方で「承認」はできず、Azureではターゲット(OpenAI)が明示的に受け入れる必要がある点にあります。
  • 記事ではこの回避策として、承認ステップではazurermを使わず、azapiプロバイダーでAzure Resource ManagerのREST APIを直接呼び出す方法を提案しています。
  • 具体的には、共有プライベートリンクの接続IDがランダムGUIDになるため、実行時にazapiで接続一覧を取得してIDを読み取り、Pendingの接続を絞り込んで承認します。
  • また、depends_onを入れることで、リンク要求の前に接続一覧を問い合わせてしまい「サイレントに失敗する承認」を防げると強調しています。

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 チェイニング)は私のブログにあります。

woitzik.dev の完全版記事
無料の GitHub リポジトリ