diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/TESTING-GUIDE.md b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/TESTING-GUIDE.md new file mode 100644 index 00000000..2f8e1587 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/TESTING-GUIDE.md @@ -0,0 +1,483 @@ +# Private Network Setup Testing Guide + +This guide walks through testing the Azure AI Foundry Agent Service with private network isolation, including integration with Azure AI Search, Microsoft Fabric, and MCP Remote Servers behind a VNet. + +--- + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Step 1: Create Resource Group](#step-1-create-resource-group) +3. [Step 2: Deploy the Bicep Template](#step-2-deploy-the-bicep-template) +4. [Step 3: Verify Deployment](#step-3-verify-deployment) +5. [Step 4: Test Azure AI Search Integration](#step-4-test-azure-ai-search-integration) +6. [Step 5: Test Microsoft Fabric Integration](#step-5-test-microsoft-fabric-integration) +7. [Step 6: Test MCP Remote Servers on VNet](#step-6-test-mcp-remote-servers-on-vnet) +8. [Cleanup](#cleanup) +9. [Troubleshooting](#troubleshooting) + +--- + +## Prerequisites + +### Azure Access Requirements + +- **Azure Subscription** with the following permissions: + - **Azure AI Account Owner**: Create cognitive services account and project + - **Owner or Role Based Access Administrator**: Assign RBAC to resources (Cosmos DB, Azure AI Search, Storage) + - **Azure AI User**: Create and edit agents + +### Register Resource Providers + +Ensure the following providers are registered in your subscription: + +```bash +az provider register --namespace 'Microsoft.KeyVault' +az provider register --namespace 'Microsoft.CognitiveServices' +az provider register --namespace 'Microsoft.Storage' +az provider register --namespace 'Microsoft.Search' +az provider register --namespace 'Microsoft.Network' +az provider register --namespace 'Microsoft.App' +az provider register --namespace 'Microsoft.ContainerService' +az provider register --namespace 'Microsoft.Fabric' # If testing Fabric integration +``` + +### Tools Required + +- Azure CLI (latest version) +- Access to a VM, VPN, or ExpressRoute for secure access to the VNet (required for testing) + +--- + +## Step 1: Create Resource Group + +Create a new resource group for your test deployment: + +```bash +# Login to Azure (if needed) +az login + +# Set your subscription +az account set --subscription "" + +# Create resource group +az group create --name "rg-private-network-test" --location "norwayeast" +``` + +**Supported Regions:** +- Class A subnet support (GA): Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3 +- Class B and C subnet support (GA): All regions supported by Azure AI Foundry Agent Service + +--- + +## Step 2: Deploy the Bicep Template + +### Option A: Deploy with New Resources (Simplest) + +This creates all resources (VNet, Cosmos DB, AI Search, Storage) automatically: + +```bash +cd infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup + +az deployment group create \ + --resource-group "rg-private-network-test" \ + --template-file main.bicep \ + --parameters location="norwayeast" +``` + +### Option B: Deploy with Existing VNet + +If you have an existing VNet with pre-configured subnets: + +```bash +az deployment group create \ + --resource-group "rg-private-network-test" \ + --template-file main.bicep \ + --parameters location="norwayeast" \ + --parameters existingVnetResourceId="/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/" \ + --parameters agentSubnetName="agent-subnet" \ + --parameters agentSubnetPrefix="192.168.0.0/24" \ + --parameters peSubnetName="pe-subnet" \ + --parameters peSubnetPrefix="192.168.1.0/24" +``` + +### Option C: Deploy with Existing Resources + Fabric + +For testing with existing Azure AI Search, Storage, Cosmos DB, and Microsoft Fabric: + +```bash +az deployment group create \ + --resource-group "rg-private-network-test" \ + --template-file main.bicep \ + --parameters location="norwayeast" \ + --parameters aiSearchResourceId="/subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/" \ + --parameters azureStorageAccountResourceId="/subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts/" \ + --parameters azureCosmosDBAccountResourceId="/subscriptions//resourceGroups//providers/Microsoft.DocumentDB/databaseAccounts/" \ + --parameters fabricWorkspaceResourceId="/subscriptions//resourceGroups//providers/Microsoft.Fabric/capacities/" +``` + +### Deployment Parameters Reference + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `location` | Azure region | `eastus2` | +| `aiServices` | Base name for AI Services | `aiservices` | +| `modelName` | Model to deploy | `gpt-4o` | +| `modelCapacity` | TPM capacity | `30` | +| `vnetName` | VNet name | `agent-vnet-test` | +| `agentSubnetName` | Agent subnet name (reserved for AI Foundry) | `agent-subnet` | +| `peSubnetName` | Private endpoint subnet | `pe-subnet` | +| `mcpSubnetName` | MCP subnet for user Container Apps | `mcp-subnet` | +| `existingVnetResourceId` | Existing VNet resource ID | `""` (creates new) | +| `aiSearchResourceId` | Existing AI Search resource ID | `""` (creates new) | +| `azureStorageAccountResourceId` | Existing Storage resource ID | `""` (creates new) | +| `azureCosmosDBAccountResourceId` | Existing Cosmos DB resource ID | `""` (creates new) | +| `fabricWorkspaceResourceId` | Existing Fabric workspace ID | `""` (skips Fabric) | + +--- + +## Step 3: Verify Deployment + +### 3.1 Check Deployment Status + +```bash +az deployment group show \ + --resource-group "rg-private-network-test" \ + --name "main" \ + --query "properties.provisioningState" +``` + +### 3.2 List Created Resources + +```bash +az resource list \ + --resource-group "rg-private-network-test" \ + --output table +``` + +### 3.3 Verify Private Endpoints + +```bash +az network private-endpoint list \ + --resource-group "rg-private-network-test" \ + --output table +``` + +Expected private endpoints: +- AI Services Account (`*-private-endpoint`) +- AI Search (`*search-private-endpoint`) +- Storage Account (`*storage-private-endpoint`) +- Cosmos DB (`*cosmosdb-private-endpoint`) +- Fabric (if configured) (`*-fabric-private-endpoint`) + +### 3.4 Verify Private DNS Zones + +```bash +az network private-dns zone list \ + --resource-group "rg-private-network-test" \ + --output table +``` + +Expected DNS zones: +- `privatelink.services.ai.azure.com` +- `privatelink.openai.azure.com` +- `privatelink.cognitiveservices.azure.com` +- `privatelink.search.windows.net` +- `privatelink.blob.core.windows.net` +- `privatelink.documents.azure.com` +- `privatelink.analysis.windows.net` (if Fabric configured) + +--- + +## Step 4: Test Azure AI Search Integration + +### 4.1 Access the Environment + +Since all resources are behind private endpoints, you must access from within the VNet: + +**Option 1: Deploy a Jump Box VM** +```bash +az vm create \ + --resource-group "rg-private-network-test" \ + --name "jumpbox-vm" \ + --image Ubuntu2204 \ + --vnet-name "" \ + --subnet "" \ + --admin-username azureuser \ + --generate-ssh-keys +``` + +**Option 2: Use Azure Bastion** (recommended for production) + +**Option 3: VPN/ExpressRoute** (enterprise setup) + +### 4.2 Test AI Search Connectivity + +From the jump box or VPN-connected machine: + +```bash +# Get AI Search endpoint +AI_SEARCH_NAME=$(az search service list -g "rg-private-network-test" --query "[0].name" -o tsv) + +# Test DNS resolution (should resolve to private IP) +nslookup ${AI_SEARCH_NAME}.search.windows.net + +# Test connectivity +curl -I https://${AI_SEARCH_NAME}.search.windows.net +``` + +### 4.3 Create a Test Index + +```bash +# Get admin key +ADMIN_KEY=$(az search admin-key show \ + --resource-group "rg-private-network-test" \ + --service-name $AI_SEARCH_NAME \ + --query "primaryKey" -o tsv) + +# Create a simple index +curl -X POST "https://${AI_SEARCH_NAME}.search.windows.net/indexes?api-version=2023-11-01" \ + -H "Content-Type: application/json" \ + -H "api-key: ${ADMIN_KEY}" \ + -d '{ + "name": "test-index", + "fields": [ + {"name": "id", "type": "Edm.String", "key": true}, + {"name": "content", "type": "Edm.String", "searchable": true} + ] + }' +``` + +--- + +## Step 5: Test Microsoft Fabric Integration + +### 5.1 Prerequisites for Fabric Testing + +> **Note:** Contact the Fabric team (Piotr Karpala) to obtain a test Fabric resource. + +To test Fabric integration, you need: +1. An existing Microsoft Fabric capacity +2. The Fabric capacity must support private link connectivity +3. The Fabric workspace resource ID + +### 5.2 Deploy with Fabric + +```bash +# Deploy with Fabric private endpoint +az deployment group create \ + --resource-group "rg-private-network-test" \ + --template-file main.bicep \ + --parameters location="norwayeast" \ + --parameters fabricWorkspaceResourceId="/subscriptions//resourceGroups//providers/Microsoft.Fabric/capacities/" +``` + +### 5.3 Verify Fabric Private Endpoint + +```bash +# Check Fabric private endpoint +az network private-endpoint show \ + --resource-group "rg-private-network-test" \ + --name "*-fabric-private-endpoint" \ + --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState" +``` + +### 5.4 Test Fabric Connectivity + +From within the VNet: + +```bash +# Test DNS resolution for Fabric +nslookup .analysis.windows.net + +# The IP should be a private IP (10.x.x.x or 192.168.x.x) +``` + +--- + +## Step 6: Test MCP Remote Servers on VNet + +### 6.1 Overview + +MCP (Model Context Protocol) Remote Servers can be deployed on the VNet to provide additional tools to AI agents while maintaining network isolation. + +> **Important:** The `agent-subnet` is reserved for Azure AI Foundry's internal Container Apps environment and cannot be shared with user-deployed containers. The Bicep template automatically creates a separate `mcp-subnet` for your MCP servers and other user-deployed Container Apps. + +### 6.2 Deploy MCP Everything Server + +Deploy an MCP server container in the VNet using Azure Container Apps: + +```bash +# Step 1: Create a Container Apps Environment connected to the MCP subnet +az containerapp env create \ + --resource-group "rg-private-network-test" \ + --name "mcp-env" \ + --location "norwayeast" \ + --infrastructure-subnet-resource-id "/subscriptions//resourceGroups/rg-private-network-test/providers/Microsoft.Network/virtualNetworks//subnets/mcp-subnet" \ + --internal-only true + +# Step 2: Deploy the MCP server as a Container App +az containerapp create \ + --resource-group "rg-private-network-test" \ + --name "mcp-everything-server" \ + --environment "mcp-env" \ + --image "mcpeverything/server:latest" \ + --target-port 8080 \ + --ingress internal \ + --cpu 1.0 \ + --memory 2.0Gi + +# Step 3: Get the internal FQDN of the MCP server +az containerapp show \ + --resource-group "rg-private-network-test" \ + --name "mcp-everything-server" \ + --query "properties.configuration.ingress.fqdn" -o tsv +``` + +### 6.3 Verify MCP Server is Only Accessible from VNet + +**From within the VNet (should work):** +```bash +# Use the FQDN from the previous step +curl http:///health +``` + +**From outside the VNet (should fail):** +```bash +# This should timeout or fail - confirming network isolation +curl --connect-timeout 5 http:///health +``` + +### 6.4 Configure Agent to Use MCP Server + +Once the MCP server is running on the VNet, configure your AI Foundry agent to use it as a tool provider: + +1. Navigate to Azure AI Foundry portal +2. Access your project (created by the deployment) +3. Create or edit an agent +4. Add MCP server endpoint as a tool source: `http://:8080` + +--- + +## Cleanup + +### Delete All Resources + +```bash +# Delete the resource group and all resources +az group delete --name "rg-private-network-test" --yes --no-wait +``` + +### Partial Cleanup (Keep VNet) + +If you want to keep the VNet for future testing: + +1. First delete the project capability host: + ```bash + ./deleteCapHost.sh + ``` + +2. Wait ~20 minutes for resources to unlink + +3. Purge the AI Services account: + ```bash + az cognitiveservices account purge \ + --resource-group "rg-private-network-test" \ + --name "" \ + --location "norwayeast" + ``` + +--- + +## Troubleshooting + +### Common Issues + +#### 1. "Subnet already in use" Error + +**Cause:** Previous capability host wasn't properly deleted. + +**Solution:** +1. Delete and purge the AI Services account +2. Wait 20 minutes for subnet to be released +3. Redeploy + +#### 2. DNS Resolution Fails + +**Cause:** Private DNS zones not linked to VNet. + +**Solution:** +```bash +# Check DNS zone links +az network private-dns link vnet list \ + --resource-group "rg-private-network-test" \ + --zone-name "privatelink.search.windows.net" +``` + +#### 3. Cannot Access Resources from Jump Box + +**Cause:** Jump box in wrong subnet or NSG blocking traffic. + +**Solution:** +- Ensure jump box is in the PE subnet or has route to it +- Check NSG rules allow outbound HTTPS (443) + +#### 4. Fabric Private Endpoint Not Created + +**Cause:** `fabricWorkspaceResourceId` parameter was empty or invalid. + +**Solution:** +- Verify the Fabric resource ID format +- Ensure Fabric capacity supports private link + +#### 5. "ManagedEnvironmentSubnetInUse" Error When Deploying MCP Server + +**Cause:** You attempted to create a Container Apps environment in the `agent-subnet`, which is already reserved for Azure AI Foundry's internal Container Apps environment. + +**Error message:** +``` +The subnet '.../subnets/agent-subnet' is already used by environment '.../Microsoft.App/managedEnvironments/...' +``` + +**Solution:** +- Use the `mcp-subnet` instead of `agent-subnet` when creating your Container Apps environment +- The Bicep template automatically creates three subnets: + - `agent-subnet` — Reserved for Azure AI Foundry (do not use) + - `pe-subnet` — For private endpoints + - `mcp-subnet` — For user-deployed Container Apps (MCP servers, etc.) + +### Useful Diagnostic Commands + +```bash +# Check private endpoint connection status +az network private-endpoint show \ + --resource-group "rg-private-network-test" \ + --name "" \ + --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState" + +# Test DNS resolution +nslookup .privatelink..azure.com + +# List all private DNS records +az network private-dns record-set list \ + --resource-group "rg-private-network-test" \ + --zone-name "privatelink.search.windows.net" +``` + +--- + +## Next Steps + +1. **Production Deployment**: Use existing enterprise VNet with VPN/ExpressRoute +2. **Security Hardening**: Add NSG rules, Azure Firewall, and Azure Policy +3. **Monitoring**: Enable diagnostic logs and Azure Monitor +4. **CI/CD Integration**: Automate deployments with Azure DevOps or GitHub Actions + +--- + +## Contact + +For questions about: +- **Fabric integration**: Contact Matt Luker or Anand Raman +- **MCP Remote Servers**: Check MCP documentation (for networking - contact Piotr Karpala) +- **Azure AI Foundry**: See [Azure AI Foundry documentation](https://learn.microsoft.com/azure/ai-foundry/) diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep index 5f431231..50b87a29 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep @@ -71,6 +71,9 @@ param agentSubnetName string = 'agent-subnet' @description('The name of Private Endpoint subnet to create new or existing subnet for private endpoints') param peSubnetName string = 'pe-subnet' +@description('The name of MCP subnet for user-deployed Container Apps (e.g., MCP servers)') +param mcpSubnetName string = 'mcp-subnet' + //Existing standard Agent required resources @description('Existing Virtual Network name Resource ID') param existingVnetResourceId string = '' @@ -84,6 +87,9 @@ param agentSubnetPrefix string = '' @description('Address prefix for the private endpoint subnet') param peSubnetPrefix string = '' +@description('Address prefix for the MCP subnet. The default value is 192.168.2.0/24.') +param mcpSubnetPrefix string = '' + @description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') param aiSearchResourceId string = '' @description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') @@ -91,6 +97,9 @@ param azureStorageAccountResourceId string = '' @description('The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') param azureCosmosDBAccountResourceId string = '' +@description('The Microsoft Fabric Workspace full ARM Resource ID. This is an optional field for Fabric private link connectivity.') +param fabricWorkspaceResourceId string = '' + //New Param for resource group of Private DNS zones //@description('Optional: Resource group containing existing private DNS zones. If specified, DNS zones will not be created.') //param existingDnsZonesResourceGroup string = '' @@ -99,10 +108,11 @@ param azureCosmosDBAccountResourceId string = '' param existingDnsZones object = { 'privatelink.services.ai.azure.com': '' 'privatelink.openai.azure.com': '' - 'privatelink.cognitiveservices.azure.com': '' - 'privatelink.search.windows.net': '' - 'privatelink.blob.core.windows.net': '' - 'privatelink.documents.azure.com': '' + 'privatelink.cognitiveservices.azure.com': '' + 'privatelink.search.windows.net': '' + 'privatelink.blob.core.windows.net': '' + 'privatelink.documents.azure.com': '' + 'privatelink.analysis.windows.net': '' } @description('Zone Names for Validation of existing Private Dns Zones') @@ -113,9 +123,9 @@ param dnsZoneNames array = [ 'privatelink.search.windows.net' 'privatelink.blob.core.windows.net' 'privatelink.documents.azure.com' + 'privatelink.analysis.windows.net' ] - var projectName = toLower('${firstProjectName}${uniqueSuffix}') var cosmosDBName = toLower('${aiServices}${uniqueSuffix}cosmosdb') var aiSearchName = toLower('${aiServices}${uniqueSuffix}search') @@ -127,7 +137,6 @@ var searchPassedIn = aiSearchResourceId != '' var cosmosPassedIn = azureCosmosDBAccountResourceId != '' var existingVnetPassedIn = existingVnetResourceId != '' - var acsParts = split(aiSearchResourceId, '/') var aiSearchServiceSubscriptionId = searchPassedIn ? acsParts[2] : subscription().subscriptionId var aiSearchServiceResourceGroupName = searchPassedIn ? acsParts[4] : resourceGroup().name @@ -159,9 +168,11 @@ module vnet 'modules-network-secured/network-agent-vnet.bicep' = { existingVnetResourceGroupName: vnetResourceGroupName agentSubnetName: agentSubnetName peSubnetName: peSubnetName + mcpSubnetName: mcpSubnetName vnetAddressPrefix: vnetAddressPrefix agentSubnetPrefix: agentSubnetPrefix peSubnetPrefix: peSubnetPrefix + mcpSubnetPrefix: mcpSubnetPrefix existingVnetSubscriptionId: vnetSubscriptionId } } @@ -220,7 +231,7 @@ module aiDependencies 'modules-network-secured/standard-dependent-resources.bice // Cosmos DB Account cosmosDBResourceId: azureCosmosDBAccountResourceId cosmosDBExists: validateExistingResources.outputs.cosmosDBExists - } + } } resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { @@ -228,10 +239,12 @@ resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { scope: resourceGroup(azureStorageSubscriptionId, azureStorageResourceGroupName) } - resource aiSearch 'Microsoft.Search/searchServices@2023-11-01' existing = { name: aiDependencies.outputs.aiSearchName - scope: resourceGroup(aiDependencies.outputs.aiSearchServiceSubscriptionId, aiDependencies.outputs.aiSearchServiceResourceGroupName) + scope: resourceGroup( + aiDependencies.outputs.aiSearchServiceSubscriptionId, + aiDependencies.outputs.aiSearchServiceResourceGroupName + ) } resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = { @@ -246,31 +259,32 @@ resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = // 3. Links private DNS zones to the VNet for name resolution // 4. Configures network policies to restrict access to private endpoints only module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.bicep' = { - name: '${uniqueSuffix}-private-endpoint' - params: { - aiAccountName: aiAccount.outputs.accountName // AI Services to secure - aiSearchName: aiDependencies.outputs.aiSearchName // AI Search to secure - storageName: aiDependencies.outputs.azureStorageName // Storage to secure - cosmosDBName:aiDependencies.outputs.cosmosDBName - vnetName: vnet.outputs.virtualNetworkName // VNet containing subnets - peSubnetName: vnet.outputs.peSubnetName // Subnet for private endpoints - suffix: uniqueSuffix // Unique identifier - vnetResourceGroupName: vnet.outputs.virtualNetworkResourceGroup - vnetSubscriptionId: vnet.outputs.virtualNetworkSubscriptionId // Subscription ID for the VNet - cosmosDBSubscriptionId: cosmosDBSubscriptionId // Subscription ID for Cosmos DB - cosmosDBResourceGroupName: cosmosDBResourceGroupName // Resource Group for Cosmos DB - aiSearchSubscriptionId: aiSearchServiceSubscriptionId // Subscription ID for AI Search Service - aiSearchResourceGroupName: aiSearchServiceResourceGroupName // Resource Group for AI Search Service - storageAccountResourceGroupName: azureStorageResourceGroupName // Resource Group for Storage Account - storageAccountSubscriptionId: azureStorageSubscriptionId // Subscription ID for Storage Account - existingDnsZones: existingDnsZones - } - dependsOn: [ - aiSearch // Ensure AI Search exists - storage // Ensure Storage exists - cosmosDB // Ensure Cosmos DB exists - ] + name: '${uniqueSuffix}-private-endpoint' + params: { + aiAccountName: aiAccount.outputs.accountName // AI Services to secure + aiSearchName: aiDependencies.outputs.aiSearchName // AI Search to secure + storageName: aiDependencies.outputs.azureStorageName // Storage to secure + cosmosDBName: aiDependencies.outputs.cosmosDBName + fabricWorkspaceResourceId: fabricWorkspaceResourceId // Microsoft Fabric workspace (optional) + vnetName: vnet.outputs.virtualNetworkName // VNet containing subnets + peSubnetName: vnet.outputs.peSubnetName // Subnet for private endpoints + suffix: uniqueSuffix // Unique identifier + vnetResourceGroupName: vnet.outputs.virtualNetworkResourceGroup + vnetSubscriptionId: vnet.outputs.virtualNetworkSubscriptionId // Subscription ID for the VNet + cosmosDBSubscriptionId: cosmosDBSubscriptionId // Subscription ID for Cosmos DB + cosmosDBResourceGroupName: cosmosDBResourceGroupName // Resource Group for Cosmos DB + aiSearchSubscriptionId: aiSearchServiceSubscriptionId // Subscription ID for AI Search Service + aiSearchResourceGroupName: aiSearchServiceResourceGroupName // Resource Group for AI Search Service + storageAccountResourceGroupName: azureStorageResourceGroupName // Resource Group for Storage Account + storageAccountSubscriptionId: azureStorageSubscriptionId // Subscription ID for Storage Account + existingDnsZones: existingDnsZones } + dependsOn: [ + aiSearch // Ensure AI Search exists + storage // Ensure Storage exists + cosmosDB // Ensure Cosmos DB exists + ] +} /* Creates a new project (sub-resource of the AI Services account) @@ -299,10 +313,10 @@ module aiProject 'modules-network-secured/ai-project-identity.bicep' = { accountName: aiAccount.outputs.accountName } dependsOn: [ - privateEndpointAndDNS - cosmosDB - aiSearch - storage + privateEndpointAndDNS + cosmosDB + aiSearch + storage ] } @@ -324,8 +338,8 @@ module storageAccountRoleAssignment 'modules-network-secured/azure-storage-accou projectPrincipalId: aiProject.outputs.projectPrincipalId } dependsOn: [ - storage - privateEndpointAndDNS + storage + privateEndpointAndDNS ] } @@ -369,13 +383,13 @@ module addProjectCapabilityHost 'modules-network-secured/add-project-capability- projectCapHost: projectCapHost } dependsOn: [ - aiSearch // Ensure AI Search exists - storage // Ensure Storage exists - cosmosDB - privateEndpointAndDNS - cosmosAccountRoleAssignments - storageAccountRoleAssignment - aiSearchRoleAssignments + aiSearch // Ensure AI Search exists + storage // Ensure Storage exists + cosmosDB + privateEndpointAndDNS + cosmosAccountRoleAssignments + storageAccountRoleAssignment + aiSearchRoleAssignments ] } @@ -401,10 +415,9 @@ module cosmosContainerRoleAssignments 'modules-network-secured/cosmos-container- cosmosAccountName: aiDependencies.outputs.cosmosDBName projectWorkspaceId: formatProjectWorkspaceId.outputs.projectWorkspaceIdGuid projectPrincipalId: aiProject.outputs.projectPrincipalId - } -dependsOn: [ - addProjectCapabilityHost - storageContainersRoleAssignment + dependsOn: [ + addProjectCapabilityHost + storageContainersRoleAssignment ] } diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicepparam b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicepparam index 43097a36..ce798511 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicepparam +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicepparam @@ -1,12 +1,12 @@ using './main.bicep' -param location = 'eastus2' -param aiServices = 'aiservices' +param location = 'norwayeast' +param aiServices = 'djetchev' param modelName = 'gpt-4o' param modelFormat = 'OpenAI' param modelVersion = '2024-11-20' -param modelSkuName = 'GlobalStandard' -param modelCapacity = 30 +param modelSkuName = 'Standard' +param modelCapacity = 1 param firstProjectName = 'project' param projectDescription = 'A project for the AI Foundry account with network secured deployed Agent' param displayName = 'project' @@ -25,10 +25,10 @@ param azureCosmosDBAccountResourceId = '' param existingDnsZones = { 'privatelink.services.ai.azure.com': '' 'privatelink.openai.azure.com': '' - 'privatelink.cognitiveservices.azure.com': '' - 'privatelink.search.windows.net': '' - 'privatelink.blob.core.windows.net': '' - 'privatelink.documents.azure.com': '' + 'privatelink.cognitiveservices.azure.com': '' + 'privatelink.search.windows.net': '' + 'privatelink.blob.core.windows.net': '' + 'privatelink.documents.azure.com': '' } //DNSZones names for validating if they exist @@ -41,7 +41,6 @@ param dnsZoneNames = [ 'privatelink.documents.azure.com' ] - // Network configuration (behavior depends on `existingVnetResourceId`) // // - NEW VNet (existingVnetResourceId is empty): @@ -65,4 +64,3 @@ param dnsZoneNames = [ param vnetAddressPrefix = '' param agentSubnetPrefix = '' param peSubnetPrefix = '' - diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.json b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.json new file mode 100644 index 00000000..d7dd2247 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.json @@ -0,0 +1,2772 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "789904159633670276" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "eastus2", + "allowedValues": [ + "westus", + "eastus", + "eastus2", + "japaneast", + "francecentral", + "spaincentral", + "uaenorth", + "southcentralus", + "italynorth", + "germanywestcentral", + "brazilsouth", + "southafricanorth", + "australiaeast", + "swedencentral", + "canadaeast", + "westeurope", + "westus3", + "uksouth", + "southindia", + "koreacentral", + "polandcentral", + "switzerlandnorth", + "norwayeast" + ], + "metadata": { + "description": "Location for all resources." + } + }, + "aiServices": { + "type": "string", + "defaultValue": "aiservices", + "metadata": { + "description": "Name for your AI Services resource." + } + }, + "modelName": { + "type": "string", + "defaultValue": "gpt-4o", + "metadata": { + "description": "The name of the model you want to deploy" + } + }, + "modelFormat": { + "type": "string", + "defaultValue": "OpenAI", + "metadata": { + "description": "The provider of your model" + } + }, + "modelVersion": { + "type": "string", + "defaultValue": "2024-11-20", + "metadata": { + "description": "The version of your model" + } + }, + "modelSkuName": { + "type": "string", + "defaultValue": "GlobalStandard", + "metadata": { + "description": "The sku of your model deployment" + } + }, + "modelCapacity": { + "type": "int", + "defaultValue": 30, + "metadata": { + "description": "The tokens per minute (TPM) of your model deployment" + } + }, + "deploymentTimestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddHHmmss')]" + }, + "firstProjectName": { + "type": "string", + "defaultValue": "project", + "metadata": { + "description": "Name for your project resource." + } + }, + "projectDescription": { + "type": "string", + "defaultValue": "A project for the AI Foundry account with network secured deployed Agent", + "metadata": { + "description": "This project will be a sub-resource of your account" + } + }, + "displayName": { + "type": "string", + "defaultValue": "network secured agent project", + "metadata": { + "description": "The display name of the project" + } + }, + "vnetName": { + "type": "string", + "defaultValue": "agent-vnet-test", + "metadata": { + "description": "Virtual Network name for the Agent to create new or existing virtual network" + } + }, + "agentSubnetName": { + "type": "string", + "defaultValue": "agent-subnet", + "metadata": { + "description": "The name of Agents Subnet to create new or existing subnet for agents" + } + }, + "peSubnetName": { + "type": "string", + "defaultValue": "pe-subnet", + "metadata": { + "description": "The name of Private Endpoint subnet to create new or existing subnet for private endpoints" + } + }, + "existingVnetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Existing Virtual Network name Resource ID" + } + }, + "vnetAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address space for the VNet (only used for new VNet)" + } + }, + "agentSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the agent subnet. The default value is 192.168.0.0/24 but you can choose any size /26 or any class like 10.0.0.0 or 172.168.0.0" + } + }, + "peSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the private endpoint subnet" + } + }, + "aiSearchResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "azureStorageAccountResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "azureCosmosDBAccountResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "fabricWorkspaceResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The Microsoft Fabric Workspace full ARM Resource ID. This is an optional field for Fabric private link connectivity." + } + }, + "existingDnsZones": { + "type": "object", + "defaultValue": { + "privatelink.services.ai.azure.com": "", + "privatelink.openai.azure.com": "", + "privatelink.cognitiveservices.azure.com": "", + "privatelink.search.windows.net": "", + "privatelink.blob.core.windows.net": "", + "privatelink.documents.azure.com": "", + "privatelink.analysis.windows.net": "" + }, + "metadata": { + "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" + } + }, + "dnsZoneNames": { + "type": "array", + "defaultValue": [ + "privatelink.services.ai.azure.com", + "privatelink.openai.azure.com", + "privatelink.cognitiveservices.azure.com", + "privatelink.search.windows.net", + "privatelink.blob.core.windows.net", + "privatelink.documents.azure.com", + "privatelink.analysis.windows.net" + ], + "metadata": { + "description": "Zone Names for Validation of existing Private Dns Zones" + } + }, + "projectCapHost": { + "type": "string", + "defaultValue": "caphostproj", + "metadata": { + "description": "The name of the project capability host to be created" + } + } + }, + "variables": { + "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", + "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", + "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", + "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]", + "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]", + "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", + "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]", + "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]", + "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]", + "acsParts": "[split(parameters('aiSearchResourceId'), '/')]", + "aiSearchServiceSubscriptionId": "[if(variables('searchPassedIn'), variables('acsParts')[2], subscription().subscriptionId)]", + "aiSearchServiceResourceGroupName": "[if(variables('searchPassedIn'), variables('acsParts')[4], resourceGroup().name)]", + "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]", + "cosmosDBSubscriptionId": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[2], subscription().subscriptionId)]", + "cosmosDBResourceGroupName": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[4], resourceGroup().name)]", + "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]", + "azureStorageSubscriptionId": "[if(variables('storagePassedIn'), variables('storageParts')[2], subscription().subscriptionId)]", + "azureStorageResourceGroupName": "[if(variables('storagePassedIn'), variables('storageParts')[4], resourceGroup().name)]", + "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]", + "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]", + "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]", + "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]", + "trimVnetName": "[trim(variables('existingVnetName'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "vnetName": { + "value": "[variables('trimVnetName')]" + }, + "useExistingVnet": { + "value": "[variables('existingVnetPassedIn')]" + }, + "existingVnetResourceGroupName": { + "value": "[variables('vnetResourceGroupName')]" + }, + "agentSubnetName": { + "value": "[parameters('agentSubnetName')]" + }, + "peSubnetName": { + "value": "[parameters('peSubnetName')]" + }, + "vnetAddressPrefix": { + "value": "[parameters('vnetAddressPrefix')]" + }, + "agentSubnetPrefix": { + "value": "[parameters('agentSubnetPrefix')]" + }, + "peSubnetPrefix": { + "value": "[parameters('peSubnetPrefix')]" + }, + "existingVnetSubscriptionId": { + "value": "[variables('vnetSubscriptionId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8505298823279202405" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the deployment" + } + }, + "vnetName": { + "type": "string", + "metadata": { + "description": "The name of the virtual network" + } + }, + "useExistingVnet": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Indicates if an existing VNet should be used" + } + }, + "existingVnetSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID of the existing VNet (if different from current subscription)" + } + }, + "existingVnetResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource Group name of the existing VNet (if different from current resource group)" + } + }, + "agentSubnetName": { + "type": "string", + "defaultValue": "agent-subnet", + "metadata": { + "description": "The name of Agents Subnet" + } + }, + "peSubnetName": { + "type": "string", + "defaultValue": "pe-subnet", + "metadata": { + "description": "The name of Private Endpoint subnet" + } + }, + "vnetAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address space for the VNet (only used for new VNet)" + } + }, + "agentSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the agent subnet" + } + }, + "peSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the private endpoint subnet" + } + } + }, + "resources": [ + { + "condition": "[not(parameters('useExistingVnet'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "vnet-deployment", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "vnetName": { + "value": "[parameters('vnetName')]" + }, + "agentSubnetName": { + "value": "[parameters('agentSubnetName')]" + }, + "peSubnetName": { + "value": "[parameters('peSubnetName')]" + }, + "vnetAddressPrefix": { + "value": "[parameters('vnetAddressPrefix')]" + }, + "agentSubnetPrefix": { + "value": "[parameters('agentSubnetPrefix')]" + }, + "peSubnetPrefix": { + "value": "[parameters('peSubnetPrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4954184648131521061" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the deployment" + } + }, + "vnetName": { + "type": "string", + "defaultValue": "agents-vnet-test", + "metadata": { + "description": "The name of the virtual network" + } + }, + "agentSubnetName": { + "type": "string", + "defaultValue": "agent-subnet", + "metadata": { + "description": "The name of Agents Subnet" + } + }, + "peSubnetName": { + "type": "string", + "defaultValue": "pe-subnet", + "metadata": { + "description": "The name of Hub subnet" + } + }, + "vnetAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address space for the VNet" + } + }, + "agentSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the agent subnet" + } + }, + "peSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the private endpoint subnet" + } + } + }, + "variables": { + "defaultVnetAddressPrefix": "192.168.0.0/16", + "vnetAddress": "[if(empty(parameters('vnetAddressPrefix')), variables('defaultVnetAddressPrefix'), parameters('vnetAddressPrefix'))]", + "agentSubnet": "[if(empty(parameters('agentSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 0), parameters('agentSubnetPrefix'))]", + "peSubnet": "[if(empty(parameters('peSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 1), parameters('peSubnetPrefix'))]" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2024-05-01", + "name": "[parameters('vnetName')]", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('vnetAddress')]" + ] + }, + "subnets": [ + { + "name": "[parameters('agentSubnetName')]", + "properties": { + "addressPrefix": "[variables('agentSubnet')]", + "delegations": [ + { + "name": "Microsoft.app/environments", + "properties": { + "serviceName": "Microsoft.App/environments" + } + } + ] + } + }, + { + "name": "[parameters('peSubnetName')]", + "properties": { + "addressPrefix": "[variables('peSubnet')]" + } + } + ] + } + } + ], + "outputs": { + "peSubnetName": { + "type": "string", + "value": "[parameters('peSubnetName')]" + }, + "agentSubnetName": { + "type": "string", + "value": "[parameters('agentSubnetName')]" + }, + "agentSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + }, + "peSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + }, + "virtualNetworkName": { + "type": "string", + "value": "[parameters('vnetName')]" + }, + "virtualNetworkId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "virtualNetworkResourceGroup": { + "type": "string", + "value": "[resourceGroup().name]" + }, + "virtualNetworkSubscriptionId": { + "type": "string", + "value": "[subscription().subscriptionId]" + } + } + } + } + }, + { + "condition": "[parameters('useExistingVnet')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "existing-vnet-deployment", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "vnetName": { + "value": "[parameters('vnetName')]" + }, + "vnetResourceGroupName": { + "value": "[parameters('existingVnetResourceGroupName')]" + }, + "vnetSubscriptionId": { + "value": "[parameters('existingVnetSubscriptionId')]" + }, + "agentSubnetName": { + "value": "[parameters('agentSubnetName')]" + }, + "peSubnetName": { + "value": "[parameters('peSubnetName')]" + }, + "agentSubnetPrefix": { + "value": "[parameters('agentSubnetPrefix')]" + }, + "peSubnetPrefix": { + "value": "[parameters('peSubnetPrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "3152324712046183852" + } + }, + "parameters": { + "vnetName": { + "type": "string", + "metadata": { + "description": "The name of the existing virtual network" + } + }, + "vnetSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID of virtual network (if different from current subscription)" + } + }, + "vnetResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource Group name of the existing VNet (if different from current resource group)" + } + }, + "agentSubnetName": { + "type": "string", + "defaultValue": "agent-subnet", + "metadata": { + "description": "The name of Agents Subnet" + } + }, + "peSubnetName": { + "type": "string", + "defaultValue": "pe-subnet", + "metadata": { + "description": "The name of Private Endpoint subnet" + } + }, + "agentSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the agent subnet (only needed if creating new subnet)" + } + }, + "peSubnetPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Address prefix for the private endpoint subnet (only needed if creating new subnet)" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))]", + "resourceGroup": "[parameters('vnetResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "vnetName": { + "value": "[parameters('vnetName')]" + }, + "subnetName": { + "value": "[parameters('agentSubnetName')]" + }, + "addressPrefix": "[if(empty(parameters('agentSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 0)), createObject('value', parameters('agentSubnetPrefix')))]", + "delegations": { + "value": [ + { + "name": "Microsoft.App/environments", + "properties": { + "serviceName": "Microsoft.App/environments" + } + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "17043822047386586435" + } + }, + "parameters": { + "vnetName": { + "type": "string", + "metadata": { + "description": "Name of the virtual network" + } + }, + "subnetName": { + "type": "string", + "metadata": { + "description": "Name of the subnet" + } + }, + "addressPrefix": { + "type": "string", + "metadata": { + "description": "Address prefix for the subnet" + } + }, + "delegations": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of subnet delegations" + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]", + "properties": { + "addressPrefix": "[parameters('addressPrefix')]", + "delegations": "[parameters('delegations')]" + } + } + ], + "outputs": { + "subnetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]" + }, + "subnetName": { + "type": "string", + "value": "[parameters('subnetName')]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))]", + "resourceGroup": "[parameters('vnetResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "vnetName": { + "value": "[parameters('vnetName')]" + }, + "subnetName": { + "value": "[parameters('peSubnetName')]" + }, + "addressPrefix": "[if(empty(parameters('peSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 1)), createObject('value', parameters('peSubnetPrefix')))]", + "delegations": { + "value": [] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "17043822047386586435" + } + }, + "parameters": { + "vnetName": { + "type": "string", + "metadata": { + "description": "Name of the virtual network" + } + }, + "subnetName": { + "type": "string", + "metadata": { + "description": "Name of the subnet" + } + }, + "addressPrefix": { + "type": "string", + "metadata": { + "description": "Address prefix for the subnet" + } + }, + "delegations": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of subnet delegations" + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]", + "properties": { + "addressPrefix": "[parameters('addressPrefix')]", + "delegations": "[parameters('delegations')]" + } + } + ], + "outputs": { + "subnetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]" + }, + "subnetName": { + "type": "string", + "value": "[parameters('subnetName')]" + } + } + } + } + } + ], + "outputs": { + "peSubnetName": { + "type": "string", + "value": "[parameters('peSubnetName')]" + }, + "agentSubnetName": { + "type": "string", + "value": "[parameters('agentSubnetName')]" + }, + "agentSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + }, + "peSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + }, + "virtualNetworkName": { + "type": "string", + "value": "[parameters('vnetName')]" + }, + "virtualNetworkId": { + "type": "string", + "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "virtualNetworkResourceGroup": { + "type": "string", + "value": "[parameters('vnetResourceGroupName')]" + }, + "virtualNetworkSubscriptionId": { + "type": "string", + "value": "[parameters('vnetSubscriptionId')]" + } + } + } + } + } + ], + "outputs": { + "virtualNetworkName": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value)]" + }, + "virtualNetworkId": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value)]" + }, + "virtualNetworkSubscriptionId": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value)]" + }, + "virtualNetworkResourceGroup": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value)]" + }, + "agentSubnetName": { + "type": "string", + "value": "[parameters('agentSubnetName')]" + }, + "peSubnetName": { + "type": "string", + "value": "[parameters('peSubnetName')]" + }, + "agentSubnetId": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value)]" + }, + "peSubnetId": { + "type": "string", + "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.peSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.peSubnetId.value)]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "accountName": { + "value": "[variables('accountName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "modelName": { + "value": "[parameters('modelName')]" + }, + "modelFormat": { + "value": "[parameters('modelFormat')]" + }, + "modelVersion": { + "value": "[parameters('modelVersion')]" + }, + "modelSkuName": { + "value": "[parameters('modelSkuName')]" + }, + "modelCapacity": { + "value": "[parameters('modelCapacity')]" + }, + "agentSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "854097619778148359" + } + }, + "parameters": { + "accountName": { + "type": "string" + }, + "location": { + "type": "string" + }, + "modelName": { + "type": "string" + }, + "modelFormat": { + "type": "string" + }, + "modelVersion": { + "type": "string" + }, + "modelSkuName": { + "type": "string" + }, + "modelCapacity": { + "type": "int" + }, + "agentSubnetId": { + "type": "string" + }, + "networkInjection": { + "type": "string", + "defaultValue": "true" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-04-01-preview", + "name": "[parameters('accountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "S0" + }, + "kind": "AIServices", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "allowProjectManagement": true, + "customSubDomainName": "[parameters('accountName')]", + "networkAcls": { + "defaultAction": "Deny", + "virtualNetworkRules": [], + "ipRules": [], + "bypass": "AzureServices" + }, + "publicNetworkAccess": "Disabled", + "networkInjections": "[if(equals(parameters('networkInjection'), 'true'), createArray(createObject('scenario', 'agent', 'subnetArmId', parameters('agentSubnetId'), 'useMicrosoftManagedNetwork', false())), null())]", + "disableLocalAuth": false + } + }, + { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('accountName'), parameters('modelName'))]", + "sku": { + "capacity": "[parameters('modelCapacity')]", + "name": "[parameters('modelSkuName')]" + }, + "properties": { + "model": { + "name": "[parameters('modelName')]", + "format": "[parameters('modelFormat')]", + "version": "[parameters('modelVersion')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]" + ] + } + ], + "outputs": { + "accountName": { + "type": "string", + "value": "[parameters('accountName')]" + }, + "accountID": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]" + }, + "accountTarget": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint]" + }, + "accountPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiSearchResourceId": { + "value": "[parameters('aiSearchResourceId')]" + }, + "azureStorageAccountResourceId": { + "value": "[parameters('azureStorageAccountResourceId')]" + }, + "azureCosmosDBAccountResourceId": { + "value": "[parameters('azureCosmosDBAccountResourceId')]" + }, + "existingDnsZones": { + "value": "[parameters('existingDnsZones')]" + }, + "dnsZoneNames": { + "value": "[parameters('dnsZoneNames')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "7641310640078958122" + } + }, + "parameters": { + "aiSearchResourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Search Service." + } + }, + "azureStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Storage Account." + } + }, + "azureCosmosDBAccountResourceId": { + "type": "string", + "metadata": { + "description": "ResourceId of Cosmos DB Account" + } + }, + "existingDnsZones": { + "type": "object", + "metadata": { + "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" + } + }, + "dnsZoneNames": { + "type": "array", + "metadata": { + "description": "List of private DNS zone names to validate" + } + } + }, + "variables": { + "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", + "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]", + "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]", + "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]", + "azureStorageSubscriptionId": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 2)), variables('storageParts')[2], subscription().subscriptionId)]", + "azureStorageResourceGroupName": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 4)), variables('storageParts')[4], resourceGroup().name)]", + "acsParts": "[split(parameters('aiSearchResourceId'), '/')]", + "aiSearchServiceSubscriptionId": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 2)), variables('acsParts')[2], subscription().subscriptionId)]", + "aiSearchServiceResourceGroupName": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 4)), variables('acsParts')[4], resourceGroup().name)]", + "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]", + "cosmosDBSubscriptionId": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 2)), variables('cosmosParts')[2], subscription().subscriptionId)]", + "cosmosDBResourceGroupName": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 4)), variables('cosmosParts')[4], resourceGroup().name)]", + "dnsZoneTypes": [ + "Microsoft.Network/privateDnsZones" + ] + }, + "resources": [], + "outputs": { + "aiSearchExists": { + "type": "bool", + "value": "[and(variables('searchPassedIn'), equals(last(split(parameters('aiSearchResourceId'), '/')), variables('acsParts')[8]))]" + }, + "cosmosDBExists": { + "type": "bool", + "value": "[and(variables('cosmosPassedIn'), equals(last(split(parameters('azureCosmosDBAccountResourceId'), '/')), variables('cosmosParts')[8]))]" + }, + "azureStorageExists": { + "type": "bool", + "value": "[and(variables('storagePassedIn'), equals(last(split(parameters('azureStorageAccountResourceId'), '/')), variables('storageParts')[8]))]" + }, + "aiSearchServiceSubscriptionId": { + "type": "string", + "value": "[variables('aiSearchServiceSubscriptionId')]" + }, + "aiSearchServiceResourceGroupName": { + "type": "string", + "value": "[variables('aiSearchServiceResourceGroupName')]" + }, + "cosmosDBSubscriptionId": { + "type": "string", + "value": "[variables('cosmosDBSubscriptionId')]" + }, + "cosmosDBResourceGroupName": { + "type": "string", + "value": "[variables('cosmosDBResourceGroupName')]" + }, + "azureStorageSubscriptionId": { + "type": "string", + "value": "[variables('azureStorageSubscriptionId')]" + }, + "azureStorageResourceGroupName": { + "type": "string", + "value": "[variables('azureStorageResourceGroupName')]" + }, + "dnsZoneExists": { + "type": "array", + "copy": { + "count": "[length(parameters('dnsZoneNames'))]", + "input": { + "name": "[parameters('dnsZoneNames')[copyIndex()]]", + "exists": "[not(empty(parameters('existingDnsZones')[parameters('dnsZoneNames')[copyIndex()]]))]" + } + } + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('dependencies-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "azureStorageName": { + "value": "[variables('azureStorageName')]" + }, + "aiSearchName": { + "value": "[variables('aiSearchName')]" + }, + "cosmosDBName": { + "value": "[variables('cosmosDBName')]" + }, + "aiSearchResourceId": { + "value": "[parameters('aiSearchResourceId')]" + }, + "aiSearchExists": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchExists.value]" + }, + "azureStorageAccountResourceId": { + "value": "[parameters('azureStorageAccountResourceId')]" + }, + "azureStorageExists": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageExists.value]" + }, + "cosmosDBResourceId": { + "value": "[parameters('azureCosmosDBAccountResourceId')]" + }, + "cosmosDBExists": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBExists.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "2754228344238136934" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "The name of the AI Search resource" + } + }, + "azureStorageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "Name of the new Cosmos DB account" + } + }, + "aiSearchResourceId": { + "type": "string", + "metadata": { + "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "azureStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "cosmosDBResourceId": { + "type": "string", + "metadata": { + "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "aiSearchExists": { + "type": "bool" + }, + "azureStorageExists": { + "type": "bool" + }, + "cosmosDBExists": { + "type": "bool" + }, + "noZRSRegions": { + "type": "array", + "defaultValue": [ + "southindia", + "westus" + ] + }, + "sku": { + "type": "object", + "defaultValue": "[if(contains(parameters('noZRSRegions'), parameters('location')), createObject('name', 'Standard_GRS'), createObject('name', 'Standard_ZRS'))]" + } + }, + "variables": { + "cosmosParts": "[split(parameters('cosmosDBResourceId'), '/')]", + "canaryRegions": [ + "eastus2euap", + "centraluseuap" + ], + "cosmosDbRegion": "[if(contains(variables('canaryRegions'), parameters('location')), 'westus', parameters('location'))]", + "acsParts": "[split(parameters('aiSearchResourceId'), '/')]", + "azureStorageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]" + }, + "resources": [ + { + "condition": "[not(parameters('cosmosDBExists'))]", + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('cosmosDBName')]", + "location": "[variables('cosmosDbRegion')]", + "kind": "GlobalDocumentDB", + "properties": { + "consistencyPolicy": { + "defaultConsistencyLevel": "Session" + }, + "disableLocalAuth": true, + "enableAutomaticFailover": false, + "enableMultipleWriteLocations": false, + "publicNetworkAccess": "Disabled", + "enableFreeTier": false, + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0, + "isZoneRedundant": false + } + ], + "databaseAccountOfferType": "Standard" + } + }, + { + "condition": "[not(parameters('aiSearchExists'))]", + "type": "Microsoft.Search/searchServices", + "apiVersion": "2024-06-01-preview", + "name": "[parameters('aiSearchName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "disableLocalAuth": false, + "authOptions": { + "aadOrApiKey": { + "aadAuthFailureMode": "http401WithBearerChallenge" + } + }, + "encryptionWithCmk": { + "enforcement": "Unspecified" + }, + "hostingMode": "default", + "partitionCount": 1, + "publicNetworkAccess": "disabled", + "replicaCount": 1, + "semanticSearch": "disabled", + "networkRuleSet": { + "bypass": "None", + "ipRules": [] + } + }, + "sku": { + "name": "standard" + } + }, + { + "condition": "[not(parameters('azureStorageExists'))]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-05-01", + "name": "[parameters('azureStorageName')]", + "location": "[parameters('location')]", + "kind": "StorageV2", + "sku": "[parameters('sku')]", + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "publicNetworkAccess": "Disabled", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny", + "virtualNetworkRules": [] + }, + "allowSharedKeyAccess": false + } + } + ], + "outputs": { + "aiSearchName": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), variables('acsParts')[8], parameters('aiSearchName'))]" + }, + "aiSearchID": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('acsParts')[2], variables('acsParts')[4]), 'Microsoft.Search/searchServices', variables('acsParts')[8]), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]" + }, + "aiSearchServiceResourceGroupName": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), variables('acsParts')[4], resourceGroup().name)]" + }, + "aiSearchServiceSubscriptionId": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), variables('acsParts')[2], subscription().subscriptionId)]" + }, + "azureStorageName": { + "type": "string", + "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[8], parameters('azureStorageName'))]" + }, + "azureStorageId": { + "type": "string", + "value": "[if(parameters('azureStorageExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageParts')[2], variables('azureStorageParts')[4]), 'Microsoft.Storage/storageAccounts', variables('azureStorageParts')[8]), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]" + }, + "azureStorageResourceGroupName": { + "type": "string", + "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[4], resourceGroup().name)]" + }, + "azureStorageSubscriptionId": { + "type": "string", + "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[2], subscription().subscriptionId)]" + }, + "cosmosDBName": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[8], parameters('cosmosDBName'))]" + }, + "cosmosDBId": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosParts')[2], variables('cosmosParts')[4]), 'Microsoft.DocumentDB/databaseAccounts', variables('cosmosParts')[8]), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]" + }, + "cosmosDBResourceGroupName": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[4], resourceGroup().name)]" + }, + "cosmosDBSubscriptionId": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[2], subscription().subscriptionId)]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-private-endpoint', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]" + }, + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]" + }, + "storageName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]" + }, + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]" + }, + "fabricWorkspaceResourceId": { + "value": "[parameters('fabricWorkspaceResourceId')]" + }, + "vnetName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkName.value]" + }, + "peSubnetName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetName.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "vnetResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkResourceGroup.value]" + }, + "vnetSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkSubscriptionId.value]" + }, + "cosmosDBSubscriptionId": { + "value": "[variables('cosmosDBSubscriptionId')]" + }, + "cosmosDBResourceGroupName": { + "value": "[variables('cosmosDBResourceGroupName')]" + }, + "aiSearchSubscriptionId": { + "value": "[variables('aiSearchServiceSubscriptionId')]" + }, + "aiSearchResourceGroupName": { + "value": "[variables('aiSearchServiceResourceGroupName')]" + }, + "storageAccountResourceGroupName": { + "value": "[variables('azureStorageResourceGroupName')]" + }, + "storageAccountSubscriptionId": { + "value": "[variables('azureStorageSubscriptionId')]" + }, + "existingDnsZones": { + "value": "[parameters('existingDnsZones')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "10536644141407027053" + } + }, + "parameters": { + "aiAccountName": { + "type": "string", + "metadata": { + "description": "Name of the AI Foundry account" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name of the AI Search service" + } + }, + "storageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "Name of the Cosmos DB account" + } + }, + "fabricWorkspaceResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The Microsoft Fabric Workspace full ARM Resource ID. Optional - leave empty to skip Fabric private endpoint." + } + }, + "vnetName": { + "type": "string", + "metadata": { + "description": "Name of the Vnet" + } + }, + "peSubnetName": { + "type": "string", + "metadata": { + "description": "Name of the Customer subnet" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "vnetResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource Group name for existing Virtual Network (if different from current resource group)" + } + }, + "vnetSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID for Virtual Network" + } + }, + "storageAccountResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource Group name for Storage Account" + } + }, + "storageAccountSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID for Storage account" + } + }, + "aiSearchSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID for AI Search service" + } + }, + "aiSearchResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource Group name for AI Search service" + } + }, + "cosmosDBSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID for Cosmos DB account" + } + }, + "cosmosDBResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Resource group name for Cosmos DB account" + } + }, + "existingDnsZones": { + "type": "object", + "defaultValue": { + "privatelink.services.ai.azure.com": "", + "privatelink.openai.azure.com": "", + "privatelink.cognitiveservices.azure.com": "", + "privatelink.search.windows.net": "", + "[format('privatelink.blob.{0}', environment().suffixes.storage)]": "", + "privatelink.documents.azure.com": "", + "privatelink.fabric.microsoft.com": "" + }, + "metadata": { + "description": "Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them." + } + } + }, + "variables": { + "fabricPassedIn": "[not(equals(parameters('fabricWorkspaceResourceId'), ''))]", + "fabricParts": "[split(parameters('fabricWorkspaceResourceId'), '/')]", + "fabricWorkspaceName": "[if(variables('fabricPassedIn'), last(variables('fabricParts')), '')]", + "aiServicesDnsZoneName": "privatelink.services.ai.azure.com", + "openAiDnsZoneName": "privatelink.openai.azure.com", + "cognitiveServicesDnsZoneName": "privatelink.cognitiveservices.azure.com", + "aiSearchDnsZoneName": "privatelink.search.windows.net", + "storageDnsZoneName": "[format('privatelink.blob.{0}', environment().suffixes.storage)]", + "cosmosDBDnsZoneName": "privatelink.documents.azure.com", + "fabricDnsZoneName": "privatelink.fabric.microsoft.com", + "aiServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('aiServicesDnsZoneName')]]", + "openAiDnsZoneRG": "[parameters('existingDnsZones')[variables('openAiDnsZoneName')]]", + "cognitiveServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('cognitiveServicesDnsZoneName')]]", + "aiSearchDnsZoneRG": "[parameters('existingDnsZones')[variables('aiSearchDnsZoneName')]]", + "storageDnsZoneRG": "[parameters('existingDnsZones')[variables('storageDnsZoneName')]]", + "cosmosDBDnsZoneRG": "[parameters('existingDnsZones')[variables('cosmosDBDnsZoneName')]]", + "fabricDnsZoneRG": "[coalesce(tryGet(parameters('existingDnsZones'), 'fabricDnsZoneName'), '')]", + "aiServicesDnsZoneId": "[if(empty(variables('aiServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')))]", + "openAiDnsZoneId": "[if(empty(variables('openAiDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('openAiDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')))]", + "cognitiveServicesDnsZoneId": "[if(empty(variables('cognitiveServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cognitiveServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')))]", + "aiSearchDnsZoneId": "[if(empty(variables('aiSearchDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiSearchDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')))]", + "storageDnsZoneId": "[if(empty(variables('storageDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('storageDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')))]", + "cosmosDBDnsZoneId": "[if(empty(variables('cosmosDBDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cosmosDBDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')))]", + "fabricDnsZoneId": "[if(variables('fabricPassedIn'), if(empty(variables('fabricDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('fabricDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))), '')]" + }, + "resources": [ + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('aiAccountName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('aiAccountName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiAccountName'))]", + "groupIds": [ + "account" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('aiSearchName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('aiSearchName'))]", + "properties": { + "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchSubscriptionId'), parameters('aiSearchResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName'))]", + "groupIds": [ + "searchService" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('storageName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('storageName'))]", + "properties": { + "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('storageAccountSubscriptionId'), parameters('storageAccountResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('storageName'))]", + "groupIds": [ + "blob" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('cosmosDBName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('cosmosDBName'))]", + "properties": { + "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", + "groupIds": [ + "Sql" + ] + } + } + ] + } + }, + { + "condition": "[variables('fabricPassedIn')]", + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', variables('fabricWorkspaceName'))]", + "properties": { + "privateLinkServiceId": "[parameters('fabricWorkspaceResourceId')]", + "groupIds": [ + "Fabric" + ] + } + } + ] + } + }, + { + "condition": "[empty(variables('aiServicesDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('aiServicesDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('openAiDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('openAiDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('cognitiveServicesDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('aiSearchDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('aiSearchDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('storageDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('storageDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('cosmosDBDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('cosmosDBDnsZoneName')]", + "location": "global" + }, + { + "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('fabricDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(variables('aiServicesDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]" + ] + }, + { + "condition": "[empty(variables('openAiDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]" + ] + }, + { + "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]" + ] + }, + { + "condition": "[empty(variables('aiSearchDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]" + ] + }, + { + "condition": "[empty(variables('storageDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]" + ] + }, + { + "condition": "[empty(variables('cosmosDBDnsZoneRG'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]" + ] + }, + { + "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiAccountName')), format('{0}-dns-group', parameters('aiAccountName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-aiserv-config', parameters('aiAccountName'))]", + "properties": { + "privateDnsZoneId": "[variables('aiServicesDnsZoneId')]" + } + }, + { + "name": "[format('{0}-dns-openai-config', parameters('aiAccountName'))]", + "properties": { + "privateDnsZoneId": "[variables('openAiDnsZoneId')]" + } + }, + { + "name": "[format('{0}-dns-cogserv-config', parameters('aiAccountName'))]", + "properties": { + "privateDnsZoneId": "[variables('cognitiveServicesDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiSearchName')), format('{0}-dns-group', parameters('aiSearchName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('aiSearchName'))]", + "properties": { + "privateDnsZoneId": "[variables('aiSearchDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiSearchName')))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('storageName')), format('{0}-dns-group', parameters('storageName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('storageName'))]", + "properties": { + "privateDnsZoneId": "[variables('storageDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('storageName')))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('cosmosDBName')), format('{0}-dns-group', parameters('cosmosDBName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('cosmosDBName'))]", + "properties": { + "privateDnsZoneId": "[variables('cosmosDBDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('cosmosDBName')))]" + ] + }, + { + "condition": "[variables('fabricPassedIn')]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName')), format('{0}-dns-group', variables('fabricWorkspaceName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', variables('fabricWorkspaceName'))]", + "properties": { + "privateDnsZoneId": "[variables('fabricDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "projectName": { + "value": "[variables('projectName')]" + }, + "projectDescription": { + "value": "[parameters('projectDescription')]" + }, + "displayName": { + "value": "[parameters('displayName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]" + }, + "aiSearchServiceResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceResourceGroupName.value]" + }, + "aiSearchServiceSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceSubscriptionId.value]" + }, + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]" + }, + "cosmosDBSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBSubscriptionId.value]" + }, + "cosmosDBResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBResourceGroupName.value]" + }, + "azureStorageName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]" + }, + "azureStorageSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageSubscriptionId.value]" + }, + "azureStorageResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageResourceGroupName.value]" + }, + "accountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "5095087340309076800" + } + }, + "parameters": { + "accountName": { + "type": "string" + }, + "location": { + "type": "string" + }, + "projectName": { + "type": "string" + }, + "projectDescription": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "aiSearchName": { + "type": "string" + }, + "aiSearchServiceResourceGroupName": { + "type": "string" + }, + "aiSearchServiceSubscriptionId": { + "type": "string" + }, + "cosmosDBName": { + "type": "string" + }, + "cosmosDBSubscriptionId": { + "type": "string" + }, + "cosmosDBResourceGroupName": { + "type": "string" + }, + "azureStorageName": { + "type": "string" + }, + "azureStorageSubscriptionId": { + "type": "string" + }, + "azureStorageResourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('cosmosDBName'))]", + "properties": { + "category": "CosmosDB", + "target": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), '2024-12-01-preview').documentEndpoint]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", + "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), '2024-12-01-preview', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('azureStorageName'))]", + "properties": { + "category": "AzureStorageAccount", + "target": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName')), '2023-05-01').primaryEndpoints.blob]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName'))]", + "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName')), '2023-05-01', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('aiSearchName'))]", + "properties": { + "category": "CognitiveSearch", + "target": "[format('https://{0}.search.windows.net', parameters('aiSearchName'))]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchServiceSubscriptionId'), parameters('aiSearchServiceResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName'))]", + "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchServiceSubscriptionId'), parameters('aiSearchServiceResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName')), '2024-06-01-preview', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/projects", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('accountName'), parameters('projectName'))]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "description": "[parameters('projectDescription')]", + "displayName": "[parameters('displayName')]" + } + } + ], + "outputs": { + "projectName": { + "type": "string", + "value": "[parameters('projectName')]" + }, + "projectId": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]" + }, + "projectPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName')), '2025-04-01-preview', 'full').identity.principalId]" + }, + "projectWorkspaceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName')), '2025-04-01-preview').internalId]" + }, + "cosmosDBConnection": { + "type": "string", + "value": "[parameters('cosmosDBName')]" + }, + "azureStorageConnection": { + "type": "string", + "value": "[parameters('azureStorageName')]" + }, + "aiSearchConnection": { + "type": "string", + "value": "[parameters('aiSearchName')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "projectWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "6910483561575524105" + } + }, + "parameters": { + "projectWorkspaceId": { + "type": "string" + } + }, + "variables": { + "part1": "[substring(parameters('projectWorkspaceId'), 0, 8)]", + "part2": "[substring(parameters('projectWorkspaceId'), 8, 4)]", + "part3": "[substring(parameters('projectWorkspaceId'), 12, 4)]", + "part4": "[substring(parameters('projectWorkspaceId'), 16, 4)]", + "part5": "[substring(parameters('projectWorkspaceId'), 20, 12)]", + "formattedGuid": "[format('{0}-{1}-{2}-{3}-{4}', variables('part1'), variables('part2'), variables('part3'), variables('part4'), variables('part5'))]" + }, + "resources": [], + "outputs": { + "projectWorkspaceIdGuid": { + "type": "string", + "value": "[variables('formattedGuid')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix'))]", + "subscriptionId": "[variables('azureStorageSubscriptionId')]", + "resourceGroup": "[variables('azureStorageResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureStorageName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "14683840003859985069" + } + }, + "parameters": { + "azureStorageName": { + "type": "string" + }, + "projectPrincipalId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('azureStorageName'))]", + "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]", + "properties": { + "principalId": "[parameters('projectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('cosmosDBSubscriptionId')]", + "resourceGroup": "[variables('cosmosDBResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "25128059954858801" + } + }, + "parameters": { + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "Name of the Cosmos DB resource" + } + }, + "projectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the AI project" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]", + "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]", + "properties": { + "principalId": "[parameters('projectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('ai-search-ra-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('aiSearchServiceSubscriptionId')]", + "resourceGroup": "[variables('aiSearchServiceResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "7968115481508840" + } + }, + "parameters": { + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name of the AI Search resource" + } + }, + "projectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the AI project" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('projectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('projectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "accountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]" + }, + "projectName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectName.value]" + }, + "cosmosDBConnection": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBConnection.value]" + }, + "azureStorageConnection": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageConnection.value]" + }, + "aiSearchConnection": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchConnection.value]" + }, + "projectCapHost": { + "value": "[parameters('projectCapHost')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "17458377866351620215" + } + }, + "parameters": { + "cosmosDBConnection": { + "type": "string" + }, + "azureStorageConnection": { + "type": "string" + }, + "aiSearchConnection": { + "type": "string" + }, + "projectName": { + "type": "string" + }, + "accountName": { + "type": "string" + }, + "projectCapHost": { + "type": "string" + } + }, + "variables": { + "threadConnections": [ + "[format('{0}', parameters('cosmosDBConnection'))]" + ], + "storageConnections": [ + "[format('{0}', parameters('azureStorageConnection'))]" + ], + "vectorStoreConnections": [ + "[format('{0}', parameters('aiSearchConnection'))]" + ] + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts/projects/capabilityHosts", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('projectCapHost'))]", + "properties": { + "capabilityHostKind": "Agents", + "vectorStoreConnections": "[variables('vectorStoreConnections')]", + "storageConnections": "[variables('storageConnections')]", + "threadStorageConnections": "[variables('threadConnections')]" + } + } + ], + "outputs": { + "projectCapHost": { + "type": "string", + "value": "[parameters('projectCapHost')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiSearchServiceSubscriptionId'), variables('aiSearchServiceResourceGroupName')), 'Microsoft.Resources/deployments', format('ai-search-ra-{0}-deployment', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.Resources/deployments', format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('storage-containers-ra-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('azureStorageSubscriptionId')]", + "resourceGroup": "[variables('azureStorageResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiProjectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + }, + "storageName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]" + }, + "workspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceIdGuid.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "13874725855824693255" + } + }, + "parameters": { + "storageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the AI Project" + } + }, + "workspaceId": { + "type": "string", + "metadata": { + "description": "Workspace Id of the AI Project" + } + } + }, + "variables": { + "conditionStr": "[format('((!(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read''}}) AND !(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action''}}) AND !(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write''}}) ) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase ''{0}'' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase ''*-azureml-agent''))', parameters('workspaceId'))]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]", + "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "principalType": "ServicePrincipal", + "conditionVersion": "2.0", + "condition": "[variables('conditionStr')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('cosmos-containers-ra-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('cosmosDBSubscriptionId')]", + "resourceGroup": "[variables('cosmosDBResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]" + }, + "projectWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceIdGuid.value]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "16291470712974205281" + } + }, + "parameters": { + "cosmosAccountName": { + "type": "string", + "metadata": { + "description": "Name of the AI Search resource" + } + }, + "projectPrincipalId": { + "type": "string", + "metadata": { + "description": "Project name" + } + }, + "projectWorkspaceId": { + "type": "string" + } + }, + "variables": { + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosAccountName'), '00000000-0000-0000-0000-000000000002')]", + "accountScope": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DocumentDB/databaseAccounts/{2}', subscription().subscriptionId, resourceGroup().name, parameters('cosmosAccountName'))]" + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2022-05-15", + "name": "[format('{0}/{1}', parameters('cosmosAccountName'), guid(parameters('projectWorkspaceId'), parameters('cosmosAccountName'), variables('roleDefinitionId'), parameters('projectPrincipalId')))]", + "properties": { + "principalId": "[parameters('projectPrincipalId')]", + "roleDefinitionId": "[variables('roleDefinitionId')]", + "scope": "[variables('accountScope')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-containers-ra-{0}-deployment', variables('uniqueSuffix')))]" + ] + } + ] +} \ No newline at end of file diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep index b371d61e..b464dedb 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep @@ -13,7 +13,6 @@ This module works with existing virtual networks and required subnets. - Private endpoint subnet for secure connectivity */ - @description('The name of the existing virtual network') param vnetName string @@ -29,17 +28,24 @@ param agentSubnetName string = 'agent-subnet' @description('The name of Private Endpoint subnet') param peSubnetName string = 'pe-subnet' +@description('The name of MCP subnet for user-deployed Container Apps') +param mcpSubnetName string = 'mcp-subnet' + @description('Address prefix for the agent subnet (only needed if creating new subnet)') param agentSubnetPrefix string = '' @description('Address prefix for the private endpoint subnet (only needed if creating new subnet)') param peSubnetPrefix string = '' +@description('Address prefix for the MCP subnet (only needed if creating new subnet)') +param mcpSubnetPrefix string = '' + // Get the address space (array of CIDR strings) var vnetAddressSpace = existingVNet.properties.addressSpace.addressPrefixes[0] var agentSubnetSpaces = empty(agentSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 0) : agentSubnetPrefix var peSubnetSpaces = empty(peSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 1) : peSubnetPrefix +var mcpSubnetSpaces = empty(mcpSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 2) : mcpSubnetPrefix // Reference the existing virtual network resource existingVNet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { @@ -78,11 +84,32 @@ module peSubnet 'subnet.bicep' = { } } +// Create the MCP subnet for user-deployed Container Apps +module mcpSubnet 'subnet.bicep' = { + name: 'mcp-subnet-${uniqueString(deployment().name, mcpSubnetName)}' + scope: resourceGroup(vnetResourceGroupName) + params: { + vnetName: vnetName + subnetName: mcpSubnetName + addressPrefix: mcpSubnetSpaces + delegations: [ + { + name: 'Microsoft.App/environments' + properties: { + serviceName: 'Microsoft.App/environments' + } + } + ] + } +} + // Output variables output peSubnetName string = peSubnetName output agentSubnetName string = agentSubnetName +output mcpSubnetName string = mcpSubnetName output agentSubnetId string = '${existingVNet.id}/subnets/${agentSubnetName}' output peSubnetId string = '${existingVNet.id}/subnets/${peSubnetName}' +output mcpSubnetId string = '${existingVNet.id}/subnets/${mcpSubnetName}' output virtualNetworkName string = existingVNet.name output virtualNetworkId string = existingVNet.id output virtualNetworkResourceGroup string = vnetResourceGroupName diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep index bad8a4f2..7be3fa96 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep @@ -19,6 +19,9 @@ param agentSubnetName string = 'agent-subnet' @description('The name of Private Endpoint subnet') param peSubnetName string = 'pe-subnet' +@description('The name of MCP subnet for user-deployed Container Apps') +param mcpSubnetName string = 'mcp-subnet' + @description('Address space for the VNet (only used for new VNet)') param vnetAddressPrefix string = '' @@ -28,6 +31,9 @@ param agentSubnetPrefix string = '' @description('Address prefix for the private endpoint subnet') param peSubnetPrefix string = '' +@description('Address prefix for the MCP subnet') +param mcpSubnetPrefix string = '' + // Create new VNet if needed module newVNet 'vnet.bicep' = if (!useExistingVnet) { name: 'vnet-deployment' @@ -36,9 +42,11 @@ module newVNet 'vnet.bicep' = if (!useExistingVnet) { vnetName: vnetName agentSubnetName: agentSubnetName peSubnetName: peSubnetName + mcpSubnetName: mcpSubnetName vnetAddressPrefix: vnetAddressPrefix agentSubnetPrefix: agentSubnetPrefix peSubnetPrefix: peSubnetPrefix + mcpSubnetPrefix: mcpSubnetPrefix } } @@ -51,17 +59,29 @@ module existingVNet 'existing-vnet.bicep' = if (useExistingVnet) { vnetSubscriptionId: existingVnetSubscriptionId agentSubnetName: agentSubnetName peSubnetName: peSubnetName + mcpSubnetName: mcpSubnetName agentSubnetPrefix: agentSubnetPrefix peSubnetPrefix: peSubnetPrefix + mcpSubnetPrefix: mcpSubnetPrefix } } // Provide unified outputs regardless of which module was used -output virtualNetworkName string = useExistingVnet ? existingVNet.outputs.virtualNetworkName : newVNet.outputs.virtualNetworkName -output virtualNetworkId string = useExistingVnet ? existingVNet.outputs.virtualNetworkId : newVNet.outputs.virtualNetworkId -output virtualNetworkSubscriptionId string = useExistingVnet ? existingVNet.outputs.virtualNetworkSubscriptionId : newVNet.outputs.virtualNetworkSubscriptionId -output virtualNetworkResourceGroup string = useExistingVnet ? existingVNet.outputs.virtualNetworkResourceGroup : newVNet.outputs.virtualNetworkResourceGroup +output virtualNetworkName string = useExistingVnet + ? existingVNet.outputs.virtualNetworkName + : newVNet.outputs.virtualNetworkName +output virtualNetworkId string = useExistingVnet + ? existingVNet.outputs.virtualNetworkId + : newVNet.outputs.virtualNetworkId +output virtualNetworkSubscriptionId string = useExistingVnet + ? existingVNet.outputs.virtualNetworkSubscriptionId + : newVNet.outputs.virtualNetworkSubscriptionId +output virtualNetworkResourceGroup string = useExistingVnet + ? existingVNet.outputs.virtualNetworkResourceGroup + : newVNet.outputs.virtualNetworkResourceGroup output agentSubnetName string = agentSubnetName output peSubnetName string = peSubnetName +output mcpSubnetName string = mcpSubnetName output agentSubnetId string = useExistingVnet ? existingVNet.outputs.agentSubnetId : newVNet.outputs.agentSubnetId output peSubnetId string = useExistingVnet ? existingVNet.outputs.peSubnetId : newVNet.outputs.peSubnetId +output mcpSubnetId string = useExistingVnet ? existingVNet.outputs.mcpSubnetId : newVNet.outputs.mcpSubnetId diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep index 81179329..96387c41 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep @@ -31,6 +31,8 @@ param aiSearchName string param storageName string @description('Name of the Cosmos DB account') param cosmosDBName string +@description('The Microsoft Fabric Workspace full ARM Resource ID. Optional - leave empty to skip Fabric private endpoint.') +param fabricWorkspaceResourceId string = '' @description('Name of the Vnet') param vnetName string @description('Name of the Customer subnet') @@ -70,6 +72,7 @@ param existingDnsZones object = { 'privatelink.search.windows.net': '' 'privatelink.blob.${environment().suffixes.storage}': '' 'privatelink.documents.azure.com': '' + 'privatelink.fabric.microsoft.com': '' } // ---- Resource references ---- @@ -93,6 +96,11 @@ resource cosmosDBAccount 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' exis scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) } +// ---- Fabric resource reference (conditional) ---- +var fabricPassedIn = fabricWorkspaceResourceId != '' +var fabricParts = split(fabricWorkspaceResourceId, '/') +var fabricWorkspaceName = fabricPassedIn ? last(fabricParts) : '' + // Reference existing network resources resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: vnetName @@ -118,7 +126,7 @@ resource aiAccountPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01 name: '${aiAccountName}-private-link-service-connection' properties: { privateLinkServiceId: aiAccount.id - groupIds: [ 'account' ] // Target AI Services account + groupIds: ['account'] // Target AI Services account } } ] @@ -140,7 +148,7 @@ resource aiSearchPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' name: '${aiSearchName}-private-link-service-connection' properties: { privateLinkServiceId: aiSearch.id - groupIds: [ 'searchService' ] // Target search service + groupIds: ['searchService'] // Target search service } } ] @@ -162,7 +170,7 @@ resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' name: '${storageName}-private-link-service-connection' properties: { privateLinkServiceId: storageAccount.id // Target blob storage - groupIds: [ 'blob' ] + groupIds: ['blob'] } } ] @@ -181,7 +189,30 @@ resource cosmosDBPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' name: '${cosmosDBName}-private-link-service-connection' properties: { privateLinkServiceId: cosmosDBAccount.id // Target Cosmos DB account - groupIds: [ 'Sql' ] + groupIds: ['Sql'] + } + } + ] + } +} + +/*--------------------------------------------- Microsoft Fabric Private Endpoint -------------------------------------*/ + +// Private endpoint for Microsoft Fabric Workspace +// - Creates network interface in customer private endpoint subnet +// - Establishes private connection to Fabric workspace +// - Only created if fabricWorkspaceResourceId is provided +resource fabricPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = if (fabricPassedIn) { + name: '${fabricWorkspaceName}-fabric-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { id: peSubnet.id } // Deploy in customer private endpoint subnet + privateLinkServiceConnections: [ + { + name: '${fabricWorkspaceName}-private-link-service-connection' + properties: { + privateLinkServiceId: fabricWorkspaceResourceId // Target Fabric workspace + groupIds: ['Fabric'] // Fabric private link group } } ] @@ -203,6 +234,7 @@ var cognitiveServicesDnsZoneName = 'privatelink.cognitiveservices.azure.com' var aiSearchDnsZoneName = 'privatelink.search.windows.net' var storageDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}' var cosmosDBDnsZoneName = 'privatelink.documents.azure.com' +var fabricDnsZoneName = 'privatelink.fabric.microsoft.com' // ---- DNS Zone Resource Group lookups ---- var aiServicesDnsZoneRG = existingDnsZones[aiServicesDnsZoneName] @@ -211,6 +243,7 @@ var cognitiveServicesDnsZoneRG = existingDnsZones[cognitiveServicesDnsZoneName] var aiSearchDnsZoneRG = existingDnsZones[aiSearchDnsZoneName] var storageDnsZoneRG = existingDnsZones[storageDnsZoneName] var cosmosDBDnsZoneRG = existingDnsZones[cosmosDBDnsZoneName] +var fabricDnsZoneRG = existingDnsZones.?fabricDnsZoneName ?? '' // ---- DNS Zone Resources and References ---- resource aiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(aiServicesDnsZoneRG)) { @@ -250,7 +283,9 @@ resource existingCognitiveServicesPrivateDnsZone 'Microsoft.Network/privateDnsZo scope: resourceGroup(cognitiveServicesDnsZoneRG) } //creating condition if user pass existing dns zones or not -var cognitiveServicesDnsZoneId = empty(cognitiveServicesDnsZoneRG) ? cognitiveServicesPrivateDnsZone.id : existingCognitiveServicesPrivateDnsZone.id +var cognitiveServicesDnsZoneId = empty(cognitiveServicesDnsZoneRG) + ? cognitiveServicesPrivateDnsZone.id + : existingCognitiveServicesPrivateDnsZone.id resource aiSearchPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(aiSearchDnsZoneRG)) { name: aiSearchDnsZoneName @@ -291,6 +326,22 @@ resource existingCosmosDBPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020- //creating condition if user pass existing dns zones or not var cosmosDBDnsZoneId = empty(cosmosDBDnsZoneRG) ? cosmosDBPrivateDnsZone.id : existingCosmosDBPrivateDnsZone.id +// Microsoft Fabric Private DNS Zone - only created if Fabric workspace is provided +resource fabricPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (fabricPassedIn && empty(fabricDnsZoneRG)) { + name: fabricDnsZoneName + location: 'global' +} + +// Reference existing Fabric private DNS zone if provided +resource existingFabricPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (fabricPassedIn && !empty(fabricDnsZoneRG)) { + name: fabricDnsZoneName + scope: resourceGroup(fabricDnsZoneRG) +} +// Fabric DNS Zone ID - conditional based on whether Fabric is configured +var fabricDnsZoneId = fabricPassedIn + ? (empty(fabricDnsZoneRG) ? fabricPrivateDnsZone.id : existingFabricPrivateDnsZone.id) + : '' + // ---- DNS VNet Links ---- resource aiServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(aiServicesDnsZoneRG)) { parent: aiServicesPrivateDnsZone @@ -347,6 +398,17 @@ resource cosmosDBLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@202 } } +// Fabric VNet Link - only created if Fabric workspace is provided +resource fabricLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (fabricPassedIn && empty(fabricDnsZoneRG)) { + parent: fabricPrivateDnsZone + location: 'global' + name: 'fabric-${suffix}-link' + properties: { + virtualNetwork: { id: vnet.id } + registrationEnabled: false + } +} + // ---- DNS Zone Groups ---- resource aiServicesDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { parent: aiAccountPrivateEndpoint @@ -400,3 +462,17 @@ resource cosmosDBDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGrou empty(cosmosDBDnsZoneRG) ? cosmosDBLink : null ] } + +// Fabric DNS Zone Group - only created if Fabric workspace is provided +resource fabricDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = if (fabricPassedIn) { + parent: fabricPrivateEndpoint + name: '${fabricWorkspaceName}-dns-group' + properties: { + privateDnsZoneConfigs: [ + { name: '${fabricWorkspaceName}-dns-config', properties: { privateDnsZoneId: fabricDnsZoneId } } + ] + } + dependsOn: [ + (fabricPassedIn && empty(fabricDnsZoneRG)) ? fabricLink : null + ] +} diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/vnet.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/vnet.bicep index d5b8db27..8d013eb3 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/vnet.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/vnet.bicep @@ -4,8 +4,9 @@ This module deploys the core network infrastructure with security controls: 1. Address Space: - VNet CIDR: 172.16.0.0/16 OR 192.168.0.0/16 - - Agents Subnet: 172.16.0.0/24 OR 192.168.0.0/24 - - Private Endpoint Subnet: 172.16.101.0/24 OR 192.168.1.0/24 + - Agents Subnet: 172.16.0.0/24 OR 192.168.0.0/24 (reserved for Azure AI Foundry) + - Private Endpoint Subnet: 172.16.1.0/24 OR 192.168.1.0/24 + - MCP Subnet: 172.16.2.0/24 OR 192.168.2.0/24 (for user Container Apps) 2. Security Features: - Network isolation @@ -25,6 +26,8 @@ param agentSubnetName string = 'agent-subnet' @description('The name of Hub subnet') param peSubnetName string = 'pe-subnet' +@description('The name of MCP subnet for user-deployed Container Apps') +param mcpSubnetName string = 'mcp-subnet' @description('Address space for the VNet') param vnetAddressPrefix string = '' @@ -34,10 +37,15 @@ param agentSubnetPrefix string = '' @description('Address prefix for the private endpoint subnet') param peSubnetPrefix string = '' + +@description('Address prefix for the MCP subnet') +param mcpSubnetPrefix string = '' + var defaultVnetAddressPrefix = '192.168.0.0/16' var vnetAddress = empty(vnetAddressPrefix) ? defaultVnetAddressPrefix : vnetAddressPrefix var agentSubnet = empty(agentSubnetPrefix) ? cidrSubnet(vnetAddress, 24, 0) : agentSubnetPrefix var peSubnet = empty(peSubnetPrefix) ? cidrSubnet(vnetAddress, 24, 1) : peSubnetPrefix +var mcpSubnet = empty(mcpSubnetPrefix) ? cidrSubnet(vnetAddress, 24, 2) : mcpSubnetPrefix resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { name: vnetName @@ -69,14 +77,30 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { addressPrefix: peSubnet } } + { + name: mcpSubnetName + properties: { + addressPrefix: mcpSubnet + delegations: [ + { + name: 'Microsoft.App/environments' + properties: { + serviceName: 'Microsoft.App/environments' + } + } + ] + } + } ] } } // Output variables output peSubnetName string = peSubnetName output agentSubnetName string = agentSubnetName +output mcpSubnetName string = mcpSubnetName output agentSubnetId string = '${virtualNetwork.id}/subnets/${agentSubnetName}' output peSubnetId string = '${virtualNetwork.id}/subnets/${peSubnetName}' +output mcpSubnetId string = '${virtualNetwork.id}/subnets/${mcpSubnetName}' output virtualNetworkName string = virtualNetwork.name output virtualNetworkId string = virtualNetwork.id output virtualNetworkResourceGroup string = resourceGroup().name