Zero-Trust RAG: Defeating the Shared Private Link Deadlock in Azure Terraform The article addresses a critical gap in Azure Terraform pipelines for Enterprise RAG applications, where the `azurerm` provider can request a Shared Private Link from AI Search to OpenAI but cannot approve it, causing a deployment deadlock that requires manual Portal intervention. The solution uses the `azapi` provider to directly call the Azure Resource Manager REST API, dynamically reading the pending connection's GUID at runtime and auto-approving it, eliminating the need for manual "ClickOps" approval. Additionally, the article recommends disabling local authentication in favor of System Managed Identities and configuring private DNS zones to prevent opaque 403 errors from Azure Firewall. Your Terraform pipeline is green. The deployment completes without errors. You grab a coffee. Ten minutes later, you test your new Enterprise RAG application. It throws a 403 Forbidden . You open the Azure Portal, check the OpenAI Networking tab, and there it is: your Shared Private Link from AI Search is sitting in Pending. Nobody told Terraform to approve it. Nobody told you it even needed approving. This is the CI/CD killer of Azure AI infrastructure. AI Search must call OpenAI to vectorize data. The azurerm provider can successfully request this Shared Private Link — but it cannot approve its own request. The target resource OpenAI must explicitly accept the inbound connection. The standard provider has no method for this. Pipeline deadlocked. Someone has to click "Approve" in the Portal manually. ClickOps in 2026. We bypass the azurerm provider entirely and talk directly to the Azure Resource Manager REST API using the azapi provider. Because Azure generates a random GUID for the incoming connection, we can't hardcode the ID — we read it at runtime: 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 } The depends on is critical — without it, Terraform queries the connection list before the link is even requested, returns empty, and the approval silently fails. Then we filter for the Pending connection and approve it: 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" } } } } The try wrapper is not optional. On terraform destroy , the Shared Private Link is deleted before this resource is evaluated. Without try , indexing 0 on an empty array crashes the destroy run and leaves orphaned resources in Azure. After terraform apply , the link goes from Pending to Approved in under 30 seconds. No Portal access required. Auto-approving the link lets AI Search reach OpenAI. But static API keys will fail your compliance audit. Keys leak. Keys get committed to Git. Disable local auth and use a System Managed Identity instead: 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 } Zero credential management. When the AI Search instance is deleted, its identity and permissions are destroyed automatically. If your-instance.openai.azure.com still resolves to a public IP, the Azure Firewall drops the traffic and you get another opaque 403 . Both services need their DNS zones linked to the 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 — always. Automatic registration conflicts with centralized Hub & Spoke DNS management. The free baseline AzAPI auto-approval + basic networking is on GitHub. The full article with complete VNet injection, Private DNS automation, and RBAC Identity Chaining is on my blog.