diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md b/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md index cb6280c2..6d673926 100644 --- a/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md @@ -21,7 +21,7 @@ languages: ![Best Practice Check](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/BestPracticeResult.svg) ![Cred Scan Check](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/CredScanResult.svg) -![Bicep Version](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent-thread/BicepVersion.svg) +![Bicep Version](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/basic-agent-keys/BicepVersion.svg) [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fazureai-samples%2Fmain%2Fscenarios%2FAgents%2Fsetup%2Fnetwork-secured-agent-thread-storage%2Fazuredeploy.json) @@ -57,7 +57,10 @@ To find your discovery_url, run the CLI command: ```az ml workspace show -n {project_name} --resource-group {resource_group_name} --query discovery_url``` Customer needs to login to Azure subscription via Azure CLI and set the environment variables - +In case Azure Funtion Tool required, select from dropdown or edit in bicep +``` +azureFunctionToolSupport=Enabled +``` ## Architecture Overview ### Network Security Design @@ -72,12 +75,14 @@ The deployment creates an isolated network environment: - AI Services - AI Search - Key Vault - - Storage Account + - Storage Account Blob + - Storage Account Queue(Only created for Azure Function tool) - Cosmos DB - **Private DNS Zones** - privatelink.azureml.ms - privatelink.search.windows.net + - privatelonk.queue.core.windows.net - privatelink.blob.core.windows.net - privatelink.documents.azure.com @@ -97,7 +102,8 @@ The deployment creates an isolated network environment: - Azure AI Services - Azure AI Search - Key Vault - - Storage Account + - Storage Account Blob + - Storage Account Queue(Only created for Azure Function tool) - Cosmos DB Account ## Security Features diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json index 2de30534..6eaf0460 100644 --- a/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.33.93.31351", - "templateHash": "11033492472783042104" + "templateHash": "6189989562818270199" } }, "parameters": { @@ -227,6 +227,17 @@ "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." } }, + "azureFunctionToolSupport": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Specifies if supporting resources for the Azure Function Tools should be created. This is only required if you are using the Azure Function Tools." + } + }, "userAssignedIdentityDefaultName": { "type": "string", "defaultValue": "[format('secured-agents-identity-{0}', parameters('uniqueSuffix'))]", @@ -248,7 +259,8 @@ "cosmosParts": "[split(parameters('cosmosDBResourceId'), '/')]", "cosmosDBSubscriptionId": "[if(variables('cosmosExists'), variables('cosmosParts')[2], subscription().subscriptionId)]", "cosmosDBResourceGroupName": "[if(variables('cosmosExists'), variables('cosmosParts')[4], resourceGroup().name)]", - "cosmosDBAccountName": "[if(variables('cosmosExists'), variables('cosmosParts')[8], variables('cosmosDBName'))]" + "cosmosDBAccountName": "[if(variables('cosmosExists'), variables('cosmosParts')[8], variables('cosmosDBName'))]", + "azureFunctionToolSupported": "[equals(parameters('azureFunctionToolSupport'), 'Enabled')]" }, "resources": [ { @@ -525,6 +537,9 @@ "cosmosDBExists": { "value": "[not(empty(parameters('cosmosDBResourceId')))]" }, + "azureFunctionToolSupported": { + "value": "[variables('azureFunctionToolSupported')]" + }, "cosmosDBName": { "value": "[variables('cosmosDBAccountName')]" }, @@ -563,7 +578,7 @@ "_generator": { "name": "bicep", "version": "0.33.93.31351", - "templateHash": "13503356786023840518" + "templateHash": "12245658005145867857" } }, "parameters": { @@ -587,6 +602,10 @@ "type": "bool", "defaultValue": false }, + "azureFunctionToolSupported": { + "type": "bool", + "defaultValue": false + }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", @@ -716,6 +735,33 @@ "storageParts": "[if(parameters('storageExists'), split(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), '/'), split(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned')), '/'))]" }, "resources": [ + { + "condition": "[and(not(parameters('storageExists')), parameters('azureFunctionToolSupported'))]", + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2022-05-01", + "name": "[format('{0}/{1}/{2}', variables('storageNameCleaned'), 'default', 'input-queue')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/queueServices', variables('storageNameCleaned'), 'default')]" + ] + }, + { + "condition": "[and(not(parameters('storageExists')), parameters('azureFunctionToolSupported'))]", + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2022-05-01", + "name": "[format('{0}/{1}/{2}', variables('storageNameCleaned'), 'default', 'output-queue')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/queueServices', variables('storageNameCleaned'), 'default')]" + ] + }, + { + "condition": "[and(not(parameters('storageExists')), parameters('azureFunctionToolSupported'))]", + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2022-05-01", + "name": "[format('{0}/{1}', variables('storageNameCleaned'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, { "type": "Microsoft.ManagedIdentity/userAssignedIdentities", "apiVersion": "2023-07-31-preview", @@ -1480,6 +1526,9 @@ }, "cosmosDBResourceGroup": { "value": "[variables('cosmosDBResourceGroupName')]" + }, + "azureFunctionToolSupported": { + "value": "[variables('azureFunctionToolSupported')]" } }, "template": { @@ -1489,7 +1538,7 @@ "_generator": { "name": "bicep", "version": "0.33.93.31351", - "templateHash": "2815470915491127245" + "templateHash": "1489959358682641210" } }, "parameters": { @@ -1561,6 +1610,13 @@ "metadata": { "description": "Name of the Customer Hub Workspace" } + }, + "azureFunctionToolSupported": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Flag indicating whether azure function tools are supported and backing resources need to be created." + } } }, "resources": [ @@ -1633,7 +1689,7 @@ { "type": "Microsoft.Network/privateEndpoints", "apiVersion": "2024-05-01", - "name": "[format('{0}-private-endpoint', parameters('storageName'))]", + "name": "[format('{0}-blob-private-endpoint', parameters('storageName'))]", "location": "[resourceGroup().location]", "properties": { "subnet": { @@ -1641,7 +1697,7 @@ }, "privateLinkServiceConnections": [ { - "name": "[format('{0}-private-link-service-connection', parameters('storageName'))]", + "name": "[format('{0}-blob-private-link-service-connection', parameters('storageName'))]", "properties": { "privateLinkServiceId": "[parameters('aiStorageId')]", "groupIds": [ @@ -1652,6 +1708,29 @@ ] } }, + { + "condition": "[parameters('azureFunctionToolSupported')]", + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-queue-private-endpoint', parameters('storageName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-queue-private-link-service-connection', parameters('storageName'))]", + "properties": { + "privateLinkServiceId": "[parameters('aiStorageId')]", + "groupIds": [ + "queue" + ] + } + } + ] + } + }, { "type": "Microsoft.Network/privateEndpoints", "apiVersion": "2024-05-01", @@ -1894,10 +1973,17 @@ "name": "[format('privatelink.blob.{0}', environment().suffixes.storage)]", "location": "global" }, + { + "condition": "[parameters('azureFunctionToolSupported')]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[format('privatelink.queue.{0}', environment().suffixes.storage)]", + "location": "global" + }, { "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2024-06-01", - "name": "[format('{0}/{1}', format('privatelink.blob.{0}', environment().suffixes.storage), format('storage-{0}-link', parameters('suffix')))]", + "name": "[format('{0}/{1}', format('privatelink.blob.{0}', environment().suffixes.storage), format('storage-blob-{0}-link', parameters('suffix')))]", "location": "global", "properties": { "virtualNetwork": { @@ -1909,14 +1995,30 @@ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]" ] }, + { + "condition": "[parameters('azureFunctionToolSupported')]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', format('privatelink.queue.{0}', environment().suffixes.storage), format('storage-queue-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]" + ] + }, { "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')))]", + "name": "[format('{0}/{1}', format('{0}-blob-private-endpoint', parameters('storageName')), format('{0}-blob-dns-group', parameters('storageName')))]", "properties": { "privateDnsZoneConfigs": [ { - "name": "[format('{0}-dns-config', parameters('storageName'))]", + "name": "[format('{0}-blob-dns-config', parameters('storageName'))]", "properties": { "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]" } @@ -1925,7 +2027,27 @@ }, "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]", - "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('storageName')))]" + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-blob-private-endpoint', parameters('storageName')))]" + ] + }, + { + "condition": "[parameters('azureFunctionToolSupported')]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-queue-private-endpoint', parameters('storageName')), format('{0}-queue-dns-group', parameters('storageName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-queue-dns-config', parameters('storageName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-queue-private-endpoint', parameters('storageName')))]" ] }, { diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep index 93de1239..c4b82acb 100644 --- a/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep @@ -138,6 +138,12 @@ param aiSearchServiceName 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 cosmosDBResourceId string = '' +@description('Specifies if supporting resources for the Azure Function Tools should be created. This is only required if you are using the Azure Function Tools.') +@allowed([ + 'Enabled' + 'Disabled' +]) +param azureFunctionToolSupport string = 'Disabled' // @description('The Ai Storage Account name. This is an optional field, and if not provided, the resource will be created.The resource should exist in same resource group') // param aiStorageAccountName string = '' @@ -173,6 +179,8 @@ var cosmosParts = split(cosmosDBResourceId, '/') var cosmosDBSubscriptionId = cosmosExists ? cosmosParts[2] : subscription().subscriptionId var cosmosDBResourceGroupName = cosmosExists ? cosmosParts[4] : resourceGroup().name var cosmosDBAccountName = cosmosExists ? cosmosParts[8] : cosmosDBName +var azureFunctionToolSupported = (azureFunctionToolSupport == 'Enabled') + // Create Virtual Network and Subnets module vnet 'modules-network-secured/networking/vnet.bicep' = { name: '${name}-${uniqueSuffix}--vnet' @@ -203,6 +211,7 @@ module aiDependencies 'modules-network-secured/network-secured-dependent-resourc aiServicesExists: !empty(aiServiceAccountName) aiSearchExists: !empty(aiSearchServiceName) cosmosDBExists: !empty(cosmosDBResourceId) + azureFunctionToolSupported: azureFunctionToolSupported cosmosDBName: cosmosDBAccountName cosmosDBSubscription: cosmosDBSubscriptionId cosmosDBResourceGroup: cosmosDBResourceGroupName @@ -294,6 +303,7 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b cosmosDBName: aiDependencies.outputs.cosmosDBName // Cosmos DB name cosmosDBSubscription: cosmosDBSubscriptionId // Cosmos DB subscription ID cosmosDBResourceGroup: cosmosDBResourceGroupName // Cosmos DB resource group name + azureFunctionToolSupported: azureFunctionToolSupported // Flag for Azure Function Tools support } dependsOn: [ aiServices // Ensure AI Services exist diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-dependent-resources.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-dependent-resources.bicep index f8f66e4a..e0fb610f 100644 --- a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-dependent-resources.bicep +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-dependent-resources.bicep @@ -24,6 +24,7 @@ param keyvaultExists bool = false param aiServicesExists bool = false param aiSearchExists bool = false param cosmosDBExists bool = false +param azureFunctionToolSupported bool = false @description('Azure region of the deployment') param location string = resourceGroup().location @@ -272,6 +273,18 @@ resource defaultStorage 'Microsoft.Storage/storageAccounts@2022-05-01' = if(!sto } allowSharedKeyAccess: false // Enforce Azure AD authentication } + + resource queueServices 'queueServices' = if (azureFunctionToolSupported) { + name: 'default' + + resource azureFunctionInputQueue 'queues' = { + name: 'input-queue' + } + + resource azureFunctionOutputQueue 'queues' = { + name: 'output-queue' + } + } } /* -------------------------------------------- Role Assignments -------------------------------------------- */ diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/private-endpoint-and-dns.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/private-endpoint-and-dns.bicep index 6953a286..e603ac32 100644 --- a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/private-endpoint-and-dns.bicep +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/private-endpoint-and-dns.bicep @@ -58,6 +58,9 @@ param hubWorkspaceId string @secure() param hubWorkspaceName string +@description('Flag indicating whether azure function tools are supported and backing resources need to be created.') +param azureFunctionToolSupported bool = false + // Reference existing services that need private endpoints resource aiServices 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { name: aiServicesName @@ -159,13 +162,13 @@ resource aiSearchPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' } } -/* -------------------------------------------- Storage Private Endpoint -------------------------------------------- */ +/* -------------------------------------------- Storage Blob Private Endpoint -------------------------------------------- */ // Private endpoint for Storage Account // - Creates network interface in customer hub subnet // - Establishes private connection to blob storage -resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { - name: '${storageName}-private-endpoint' +resource storageBlobPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: '${storageName}-blob-private-endpoint' location: resourceGroup().location properties: { subnet: { @@ -173,7 +176,7 @@ resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' } privateLinkServiceConnections: [ { - name: '${storageName}-private-link-service-connection' + name: '${storageName}-blob-private-link-service-connection' properties: { privateLinkServiceId: aiStorageId groupIds: [ @@ -185,6 +188,32 @@ resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' } } +/* -------------------------------------------- Storage Queue Private Endpoint -------------------------------------------- */ + +// Private endpoint for Storage Account +// - Creates network interface in customer hub subnet +// - Establishes private connection to blob storage +resource storageQueuePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = if (azureFunctionToolSupported) { + name: '${storageName}-queue-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${storageName}-queue-private-link-service-connection' + properties: { + privateLinkServiceId: aiStorageId + groupIds: [ + 'queue' // Target storage queues + ] + } + } + ] + } +} + /*------------------------------------------Cosmos DB Private Endpoint-------------------------------*/ resource cosmosDBPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { name: '${cosmosDBName}-private-endpoint' @@ -406,16 +435,35 @@ resource aiSearchDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGrou // Private DNS Zone for Storage // - Enables custom DNS resolution for blob storage private endpoint -resource storagePrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { +resource storageBlobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { name: 'privatelink.blob.${environment().suffixes.storage}' // Dynamic DNS zone for storage location: 'global' } -// Link Storage DNS Zone to VNet -resource storageLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { - parent: storagePrivateDnsZone +// - Enables custom DNS resolution for queue storage private endpoint +resource storageQueuePrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (azureFunctionToolSupported) { + name: 'privatelink.queue.${environment().suffixes.storage}' // Dynamic DNS zone for storage location: 'global' - name: 'storage-${suffix}-link' +} + +// Link Storage Blob DNS Zone to VNet +resource storageBlobLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: storageBlobPrivateDnsZone + location: 'global' + name: 'storage-blob-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} + +// Link Storage Queue DNS Zone to VNet +resource storageQueueLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (azureFunctionToolSupported) { + parent: storageQueuePrivateDnsZone + location: 'global' + name: 'storage-queue-${suffix}-link' properties: { virtualNetwork: { id: vnet.id // Link to specified VNet @@ -424,16 +472,32 @@ resource storageLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024 } } -// DNS Zone Group for Storage -resource storageDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { - parent: storagePrivateEndpoint - name: '${storageName}-dns-group' +// DNS Zone Group for Storage Blob +resource storageBlobDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: storageBlobPrivateEndpoint + name: '${storageName}-blob-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${storageName}-blob-dns-config' + properties: { + privateDnsZoneId: storageBlobPrivateDnsZone.id + } + } + ] + } +} + +// DNS Zone Group for Storage Queue +resource storageQueueDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = if (azureFunctionToolSupported) { + parent: storageQueuePrivateEndpoint + name: '${storageName}-queue-dns-group' properties: { privateDnsZoneConfigs: [ { - name: '${storageName}-dns-config' + name: '${storageName}-queue-dns-config' properties: { - privateDnsZoneId: storagePrivateDnsZone.id + privateDnsZoneId: storageQueuePrivateDnsZone.id } } ]