diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep index 5386c9ba9..14e51d249 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep @@ -2,30 +2,8 @@ Standard Setup Managed Network Secured Steps for main.bicep ----------------------------------- */ -@description('Location for all resources.') -@allowed([ - 'spaincentral' - 'westus' - 'eastus' - 'japaneast' - 'francecentral' - 'eastus2' - 'uaenorth' - 'brazilsouth' - 'germanywestcentral' - 'italynorth' - 'southcentralus' - 'westcentralus' - 'australiaeast' - 'swedencentral' - 'canadaeast' - 'southafricanorth' - 'westeurope' - 'westus3' - 'southindia' - 'uksouth' -]) -param location string = 'eastus2' +@description('Location for all resources. Defaults to the resource group location.') +param location string = resourceGroup().location @description('The isolation mode for the managed network') @allowed([ @@ -237,19 +215,52 @@ resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) } +// Assign Network Connection Approver role on each target resource group +// This allows the AI account identity to approve managed PE connections to resources in other RGs +module networkApproverRoleStorage 'modules-network-secured/network-connection-approver-role.bicep' = if (storagePassedIn) { + name: 'network-approver-storage-${uniqueSuffix}-deployment' + scope: resourceGroup(azureStorageSubscriptionId, azureStorageResourceGroupName) + params: { + aiAccountPrincipalId: aiAccount.outputs.accountPrincipalId + aiAccountResourceId: aiAccount.outputs.accountID + } +} + +module networkApproverRoleCosmos 'modules-network-secured/network-connection-approver-role.bicep' = if (cosmosPassedIn) { + name: 'network-approver-cosmos-${uniqueSuffix}-deployment' + scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) + params: { + aiAccountPrincipalId: aiAccount.outputs.accountPrincipalId + aiAccountResourceId: aiAccount.outputs.accountID + } +} + +module networkApproverRoleSearch 'modules-network-secured/network-connection-approver-role.bicep' = if (searchPassedIn) { + name: 'network-approver-search-${uniqueSuffix}-deployment' + scope: resourceGroup(aiSearchServiceSubscriptionId, aiSearchServiceResourceGroupName) + params: { + aiAccountPrincipalId: aiAccount.outputs.accountPrincipalId + aiAccountResourceId: aiAccount.outputs.accountID + } +} + // Configure Managed Network for AI Services Account -// This module sets up outbound rules for the managed virtual network to allow -// secure communication to customer resources (Storage, AI Search, Cosmos DB) +// This module sets up the managed virtual network and outbound PE rules to allow +// secure communication from hosted agents to customer resources (Storage, AI Search, Cosmos DB) module managedNetwork 'modules-network-secured/managed-network.bicep' = { name: 'managed-network-${uniqueSuffix}-deployment' params: { accountName: aiAccount.outputs.accountName isolationMode: isolationMode + storageAccountResourceId: storage.id + cosmosDBResourceId: cosmosDB.id + aiSearchResourceId: aiSearch.id } dependsOn: [ - aiSearch // Ensure AI Search exists - storage // Ensure Storage exists - cosmosDB // Ensure Cosmos DB exists + aiDependencies // Ensure dependent resources (Storage, CosmosDB, Search) are fully created + networkApproverRoleStorage + networkApproverRoleCosmos + networkApproverRoleSearch ] } @@ -394,6 +405,7 @@ module addProjectCapabilityHost 'modules-network-secured/add-project-capability- cosmosAccountRoleAssignments storageAccountRoleAssignment aiSearchRoleAssignments + managedNetwork // Ensure managed network outbound rules are provisioned ] } diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam index 0df098e5f..66cf35949 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam @@ -1,6 +1,5 @@ using './main.bicep' -param location = 'eastus2' param isolationMode = 'AllowOnlyApprovedOutbound' param aiServices = 'aiservices' param firstProjectName = 'project' diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep index 285948878..4d5e2d8a8 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep @@ -8,6 +8,15 @@ param accountName string ]) param isolationMode string = 'AllowOnlyApprovedOutbound' +@description('Resource ID of the Storage Account for outbound PE rule') +param storageAccountResourceId string + +@description('Resource ID of the Cosmos DB Account for outbound PE rule') +param cosmosDBResourceId string + +@description('Resource ID of the AI Search Service for outbound PE rule') +param aiSearchResourceId string + // Reference the existing AI Services account in the same resource group resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { name: accountName @@ -28,4 +37,67 @@ resource managedNetwork 'Microsoft.CognitiveServices/accounts/managednetworks@20 } } +// Outbound PE rules allow the managed VNet (where hosted agents run) to reach dependent resources +// Rules must be created sequentially to avoid conflicting state errors on the managed network + +// The agent needs a PE back to the Foundry/AI Services endpoint itself (public access is disabled) +#disable-next-line BCP081 +resource aiServicesOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview' = { + parent: managedNetwork + name: 'aiservices-account-rule' + properties: { + type: 'PrivateEndpoint' + destination: { + serviceResourceId: aiAccount.id + subresourceTarget: 'account' + } + category: 'UserDefined' + } +} + +#disable-next-line BCP081 +resource storageOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview' = { + parent: managedNetwork + name: 'storage-blob-rule' + properties: { + type: 'PrivateEndpoint' + destination: { + serviceResourceId: storageAccountResourceId + subresourceTarget: 'blob' + } + category: 'UserDefined' + } + dependsOn: [aiServicesOutboundRule] +} + +#disable-next-line BCP081 +resource cosmosDBOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview' = { + parent: managedNetwork + name: 'cosmos-sql-rule' + properties: { + type: 'PrivateEndpoint' + destination: { + serviceResourceId: cosmosDBResourceId + subresourceTarget: 'Sql' + } + category: 'UserDefined' + } + dependsOn: [storageOutboundRule] +} + +#disable-next-line BCP081 +resource aiSearchOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview' = { + parent: managedNetwork + name: 'aisearch-rule' + properties: { + type: 'PrivateEndpoint' + destination: { + serviceResourceId: aiSearchResourceId + subresourceTarget: 'searchService' + } + category: 'UserDefined' + } + dependsOn: [cosmosDBOutboundRule] +} + output managedNetworkSettingsName string = managedNetwork.name diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/network-connection-approver-role.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/network-connection-approver-role.bicep new file mode 100644 index 000000000..43baa426b --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/network-connection-approver-role.bicep @@ -0,0 +1,18 @@ +@description('The principal ID of the AI Services account managed identity') +param aiAccountPrincipalId string + +@description('The AI Services account resource ID (used for deterministic GUID generation)') +param aiAccountResourceId string + +// Azure AI Enterprise Network Connection Approver role definition ID +var networkConnectionApproverRoleId = 'b556d68e-0be0-4f35-a333-ad7ee1ce17ea' + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(aiAccountResourceId, networkConnectionApproverRoleId, resourceGroup().id) + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', networkConnectionApproverRoleId) + principalId: aiAccountPrincipalId + principalType: 'ServicePrincipal' + } +}