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.
Why It Happens
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.
The AzAPI State Machine Fix
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.
Identity Chaining — Kill the API Keys
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.
Don't Forget Private DNS
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.
Top comments (0)