diff --git a/.github/scripts/build_agent_setup_bicep.py b/.github/scripts/build_agent_setup_bicep.py index 720c1a15..4f7e6c78 100755 --- a/.github/scripts/build_agent_setup_bicep.py +++ b/.github/scripts/build_agent_setup_bicep.py @@ -16,7 +16,7 @@ def run_az_command(*args: Union[str, Path]) -> None: sys.exit(exit_code) -def get_main_bicep_files(modified_files: List[str]) -> List[Path]: +def get_main_bicep_files(modified_files: List[Path]) -> List[Path]: """Finds unique folders with modified files and ensures 'main.bicep' exists in each.""" modified_folders = {Path(f).parent for f in modified_files} return [folder / "main.bicep" for folder in modified_folders if (folder / "main.bicep").exists()] diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md b/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md new file mode 100644 index 00000000..cb6280c2 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/README.md @@ -0,0 +1,269 @@ +--- +description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation using User Managed Identity authetication for the AI Service/AOAI connection and private network links to connect the agent to your secure data. +page_type: sample +products: +- azure +- azure-resource-manager +urlFragment: network-secured-agent +languages: +- bicep +- json +--- + +# Network-Secured Azure AI Agent Infrastructure with User Managed Identity + +![Azure Public Test Date](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/PublicLastTestDate.svg) +![Azure Public Test Result](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/PublicDeployment.svg) + +![Azure US Gov Last Test Date](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/FairfaxLastTestDate.svg) +![Azure US Gov Last Test Result](https://azurequickstartsservice.blob.core.windows.net/badges/quickstarts/microsoft.azure-ai-agent-service/network-secured-agent/FairfaxDeployment.svg) + +![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) + +[![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) + +[![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fazureai-samples%2Fmain%2Fscenarios%2FAgents%2Fsetup%2Fnetwork-secured-agent-thread-storage%2Fazuredeploy.json) + +This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking, managed identities, and role-based access control (RBAC). + + +## Note: +Make sure you have an active Azure subscription that allows registering resource providers. +Subnet delegation requires the Microsoft.App provider to be registered in your subscription. If it's not already registered, run the command below: + +``` +Register-AzResourceProvider -ProviderNamespace Microsoft.App +``` + +In case Hub/Project workspace need Public Network Disabled select from dropdown or edit in bicep. + +``` +hubPublicNetworkAccess = Disabled +``` + +When Hub and Project workspace Public Network Disabled, the project connection string output from the deployment will look like, this should be used data plane operations + +``` +PROJECT_CONNECTION_STRING='.workspace.japaneast.api.azureml.ms;12345678-abcd-1234-9fc6-62780b3d3e05;my-resource-group;my-project-name' +``` + +It follows format +```;;;``` +HostName can be found by navigating to your discovery_url and removing the leading "https://" and trailing "/discovery" +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 + + +## Architecture Overview + +### Network Security Design + +The deployment creates an isolated network environment: + +- **Virtual Network (172.16.0.0/16)** + - Customer Hub Subnet (172.16.0.0/24): Hosts private endpoints + - Agents Subnet (172.16.101.0/24): For azure ai agent workloads + +- **Private Endpoints** + - AI Services + - AI Search + - Key Vault + - Storage Account + - Cosmos DB + +- **Private DNS Zones** + - privatelink.azureml.ms + - privatelink.search.windows.net + - privatelink.blob.core.windows.net + - privatelink.documents.azure.com + +### Core Components + +1. **AI Hub** + - Central orchestration point + - Manages service connections + - Network-isolated capability hosts + +2. **AI Project** + - Workspace configuration + - Service integration + - Agent deployment + +3. **Supporting Services** + - Azure AI Services + - Azure AI Search + - Key Vault + - Storage Account + - Cosmos DB Account + +## Security Features + +### Authentication & Authorization + +- **Managed Identity** + - Zero-trust security model + - No credential storage + - Platform-managed rotation + +- **Role Assignments** + - AI Services: Administrator, OpenAI User + - AI Search: Index Data Contributor, Service Contributor + - Key Vault: Contributor, Secrets Officer + - Storage: Blob Data Owner, Queue Data Contributor + - Cosmos DB: DocumentDB Account Operator Role, Cosmos DB Built-In Data Contributor + +### Network Security + +- Public network access disabled +- Private endpoints for all services +- Service endpoints for Azure services +- Network ACLs with deny by default + +## Deployment Options + +### 1. Infrastructure as Code (Bicep) +```bash +az deployment group create \ + --template-file main.bicep \ + --parameters @parameters.json +``` +Features: +- Declarative approach +- Native Azure integration +- Easy to version control +- Clear resource dependencies + +## Module Structure + +``` +modules-network-secured/ +├── ai-search-role-assignments.bicep # AI Search RBAC configuration +├── ai-search-service.bicep # AI Search deployment +├── ai-service-role-assignments.bicep # AI Services RBAC configuration +├── cognitive-services-role-assignments.bicep # OpenAI permissions +├── keyvault-role-assignments.bicep # Key Vault RBAC configuration +├── network-secured-ai-hub.bicep # AI Hub deployment +├── network-secured-ai-project.bicep # AI Project deployment +├── network-secured-dependent-resources.bicep # Core infrastructure +├── network-secured-identity.bicep # Managed identity +├── private-endpoint-and-dns.bicep # Network security +├── cosmos-account-role-assignments.bicep # Cosmos Account RBAC +├── cosmos-container-role-assignments.bice # Cosmos Container RBAC +└── storage-role-assignments.bicep # Storage RBAC configuration + +``` + +## Role Assignments + +The deployment configures the following RBAC permissions: + +### AI Services +- Azure AI Administrator (b78c5d69-af96-48a3-bf8d-a8b4d589de94) + * Full access to manage AI resources + * Model deployment permissions + * Security settings management + +### AI Search +- Search Index Data Contributor (8ebe5a00-799e-43f5-93ac-243d3dce84a7) + * Read/write access to indexes + * Query and update operations +- Search Service Contributor (7ca78c08-252a-4471-8644-bb5ff32d4ba0) + * Service management access + * Configuration changes + +### Key Vault +- Key Vault Contributor (f25e0fa2-a7c8-4377-a976-54943a77a395) + * Manage vault properties + * Cannot access secrets +- Key Vault Secrets Officer (b86a8fe4-44ce-4948-aee5-eccb2c155cd7) + * Full secrets access + * Manage secret metadata + +### Storage +- Storage Blob Data Owner (b7e6dc6d-f1e8-4753-8033-0f276bb0955b) + * Full blob access + * Container management +- Storage Queue Data Contributor (974c5e8b-45b9-4653-ba55-5f855dd0fb88) + * Queue operations + * Message management + +### Cosmos DB Account +- DocumentDB Account Operator Role(230815da-be43-4aae-9cb4-875f7bd000aa) +- Cosmos DB Built-In Data Contributor Role (00000000-0000-0000-0000-000000000002) + +## Networking Details + +### Private Endpoints +Each service is deployed with a private endpoint in the Customer Hub subnet: + +```plaintext +AI Services: account +AI Search: searchService +Storage: blob +Cosmos DB: sql +``` + +### DNS Configuration +Private DNS zones are created and linked to the VNet: + +```plaintext +AI Services: privatelink.azureml.ms +AI Search: privatelink.search.windows.net +Storage: privatelink.blob.core.windows.net +Cosmos DB: privatelink.documents.azure.com +``` + +## Security Considerations + +1. **Network Isolation** + - No public internet exposure + - Private endpoint access only + - Network ACLs with deny-by-default + +2. **Authentication** + - Managed identity authentication + - No stored credentials + - AAD integration + +3. **Authorization** + - Granular RBAC assignments + - Principle of least privilege + - Service-specific roles + +4. **Monitoring** + - Diagnostic settings enabled + - Activity logging + - Network monitoring + +## Limitations +- AI Services/Azure OpenAI resource must be in the same region as Hub and Project workspace. This restriction would be removed in next revision (coming soon). +- The capability host sub-resources of Hub/Project must be deleted before deleting the Hub/Project resource itself. You can use the script as sample to delete it or can be done in alternate ways via ARM. This restriction would be removed in next revision (coming soon). + - [Run delete script](../utils/deleteCaphost.sh) + + +## Maintenance + +### Regular Tasks +1. Review role assignments +2. Monitor network security +3. Check service health +4. Update configurations as needed + +### Troubleshooting +1. Verify private endpoint connectivity +2. Check DNS resolution +3. Validate role assignments +4. Review network security groups + +## References + +- [Azure AI Services Documentation](https://learn.microsoft.com/en-us/azure/ai-services/) +- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/) +- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/) +- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices) + +`Tags: ` \ No newline at end of file diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json new file mode 100644 index 00000000..a855aa23 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.json @@ -0,0 +1,2665 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "12425839983447429475" + } + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "network-secured-agent" + }, + "deploymentTimestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddHHmmss')]" + }, + "uniqueSuffix": { + "type": "string", + "defaultValue": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]" + }, + "defaultAiHubName": { + "type": "string", + "defaultValue": "hub-demo", + "minLength": 2, + "maxLength": 12, + "metadata": { + "description": "Name for the AI resource and used to derive name of dependent resources." + } + }, + "defaultAiHubFriendlyName": { + "type": "string", + "defaultValue": "Agents Hub resource", + "metadata": { + "description": "Friendly name for your Hub resource" + } + }, + "defaultAiHubDescription": { + "type": "string", + "defaultValue": "This is an example AI resource for use in Azure AI Studio.", + "metadata": { + "description": "Description of your Azure AI resource displayed in AI studio" + } + }, + "defaultAiProjectName": { + "type": "string", + "defaultValue": "project-demo", + "metadata": { + "description": "Name for the AI project resources." + } + }, + "defaultAiProjectFriendlyName": { + "type": "string", + "defaultValue": "Agents Project resource", + "metadata": { + "description": "Friendly name for your Azure AI resource" + } + }, + "hubPublicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Specifies the public network access for the Azure AI Hub workspace." + } + }, + "projectPublicNetworkAccess": { + "type": "string", + "defaultValue": "[parameters('hubPublicNetworkAccess')]", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Specifies the public network access for the Azure AI Project workspace.Note: Please ensure that if you are setting this to Enabled, the AI Hub workspace is also set to Enabled." + } + }, + "defaultAiProjectDescription": { + "type": "string", + "defaultValue": "This is an example AI Project resource for use in Azure AI Studio.", + "metadata": { + "description": "Description of your Azure AI resource displayed in AI studio" + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Resource group location" + } + }, + "location": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "allowedValues": [ + "australiaeast", + "eastus", + "eastus2", + "francecentral", + "japaneast", + "norwayeast", + "southindia", + "swedencentral", + "uaenorth", + "uksouth", + "westus", + "westus3" + ], + "metadata": { + "description": "Location for all resources." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Set of tags to apply to all resources." + } + }, + "defaultAiSearchName": { + "type": "string", + "defaultValue": "agent-ai-search", + "metadata": { + "description": "Name of the Azure AI Search account" + } + }, + "defaultCapabilityHostName": { + "type": "string", + "defaultValue": "caphost1", + "metadata": { + "description": "Name for capabilityHost." + } + }, + "defaultStorageName": { + "type": "string", + "defaultValue": "agentstorage", + "metadata": { + "description": "Name of the storage account" + } + }, + "defaultAiServicesName": { + "type": "string", + "defaultValue": "agent-ai-service", + "metadata": { + "description": "Name of the Azure AI Services account" + } + }, + "defaultCosmosDBName": { + "type": "string", + "defaultValue": "agent-thread-storage", + "metadata": { + "description": "Name of the Azure Cosmos DB account" + } + }, + "modelName": { + "type": "string", + "defaultValue": "gpt-4o-mini", + "metadata": { + "description": "Model name for deployment" + } + }, + "modelFormat": { + "type": "string", + "defaultValue": "OpenAI", + "metadata": { + "description": "Model format for deployment" + } + }, + "modelVersion": { + "type": "string", + "defaultValue": "2024-07-18", + "metadata": { + "description": "Model version for deployment" + } + }, + "modelSkuName": { + "type": "string", + "defaultValue": "GlobalStandard", + "metadata": { + "description": "Model deployment SKU name" + } + }, + "modelCapacity": { + "type": "int", + "defaultValue": 50, + "metadata": { + "description": "Model deployment capacity" + } + }, + "modelLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Model deployment location. If you want to deploy an Azure AI resource/model in different location than the rest of the resources created." + } + }, + "aisKind": { + "type": "string", + "defaultValue": "AIServices", + "metadata": { + "description": "AI service kind, values can be \"OpenAI\" or \"AIService\"" + } + }, + "aiServiceAccountName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The AI Service Account name. This is an optional field, and if not provided, the resource will be created. The resource should exist in same resource group" + } + }, + "aiSearchServiceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The AI Search Service name. This is an optional field, and if not provided, the resource will be created.The resource should exist in same resource group must be Public Network Disabled" + } + }, + "cosmosDBResourceId": { + "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." + } + }, + "userAssignedIdentityDefaultName": { + "type": "string", + "defaultValue": "[format('secured-agents-identity-{0}', parameters('uniqueSuffix'))]", + "metadata": { + "description": "The name of User Assigned Identity" + } + } + }, + "variables": { + "keyVaultOverride": "", + "userAssignedIdentityOverride": "", + "uaiName": "[if(equals(variables('userAssignedIdentityOverride'), ''), parameters('userAssignedIdentityDefaultName'), variables('userAssignedIdentityOverride'))]", + "keyVaultName": "[if(empty(variables('keyVaultOverride')), format('kv-{0}-{1}', parameters('defaultAiHubName'), parameters('uniqueSuffix')), variables('keyVaultOverride'))]", + "aiServiceName": "[if(empty(parameters('aiServiceAccountName')), format('{0}{1}', parameters('defaultAiServicesName'), parameters('uniqueSuffix')), parameters('aiServiceAccountName'))]", + "aiSearchName": "[if(empty(parameters('aiSearchServiceName')), format('{0}{1}', parameters('defaultAiSearchName'), parameters('uniqueSuffix')), parameters('aiSearchServiceName'))]", + "cosmosExists": "[not(empty(parameters('cosmosDBResourceId')))]", + "storageNameClean": "[format('{0}{1}', parameters('defaultStorageName'), parameters('uniqueSuffix'))]", + "cosmosDBName": "[format('{0}{1}', parameters('defaultCosmosDBName'), parameters('uniqueSuffix'))]", + "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'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "userAssignedIdentityName": { + "value": "[variables('uaiName')]" + }, + "uaiExists": { + "value": "[not(equals(variables('userAssignedIdentityOverride'), ''))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "15872236774552793157" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region for resource deployment" + } + }, + "userAssignedIdentityName": { + "type": "string", + "metadata": { + "description": "Name of the user-assigned managed identity" + } + }, + "uaiExists": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Flag indicating if the identity already exists" + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-07-31-preview", + "name": "[parameters('userAssignedIdentityName')]", + "location": "[parameters('location')]" + } + ], + "outputs": { + "uaiName": { + "type": "string", + "value": "[if(parameters('uaiExists'), parameters('userAssignedIdentityName'), parameters('userAssignedIdentityName'))]" + }, + "uaiId": { + "type": "string", + "value": "[if(parameters('uaiExists'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')))]" + }, + "uaiPrincipalId": { + "type": "string", + "value": "[if(parameters('uaiExists'), reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').principalId, reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').principalId)]" + }, + "uaiClientId": { + "type": "string", + "value": "[if(parameters('uaiExists'), reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').clientId, reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').clientId)]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "suffix": { + "value": "[parameters('uniqueSuffix')]" + }, + "modelLocation": { + "value": "[parameters('modelLocation')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "11931798975158233939" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the deployment" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to resources" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Unique suffix for resource names" + } + }, + "vnetName": { + "type": "string", + "defaultValue": "[format('agents-vnet-{0}', parameters('suffix'))]", + "metadata": { + "description": "The name of the virtual network" + } + }, + "agentsSubnetName": { + "type": "string", + "defaultValue": "[format('agents-subnet-{0}', parameters('suffix'))]", + "metadata": { + "description": "The name of Agents Subnet" + } + }, + "hubSubnetName": { + "type": "string", + "defaultValue": "[format('hub-subnet-{0}', parameters('suffix'))]", + "metadata": { + "description": "The name of Hub subnet" + } + }, + "modelLocation": { + "type": "string", + "metadata": { + "description": "Model/AI Resource deployment location" + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2024-05-01", + "name": "[parameters('vnetName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "172.16.0.0/16" + ] + }, + "subnets": [ + { + "name": "[parameters('hubSubnetName')]", + "properties": { + "addressPrefix": "172.16.0.0/24" + } + }, + { + "name": "[parameters('agentsSubnetName')]", + "properties": { + "addressPrefix": "172.16.101.0/24", + "delegations": [ + { + "name": "Microsoft.app/environments", + "properties": { + "serviceName": "Microsoft.App/environments" + } + } + ] + } + } + ] + } + } + ], + "outputs": { + "virtualNetworkName": { + "type": "string", + "value": "[parameters('vnetName')]" + }, + "virtualNetworkId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "hubSubnetName": { + "type": "string", + "value": "[parameters('hubSubnetName')]" + }, + "agentsSubnetName": { + "type": "string", + "value": "[parameters('agentsSubnetName')]" + }, + "hubSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('hubSubnetName'))]" + }, + "agentsSubnetId": { + "type": "string", + "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentsSubnetName'))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "suffix": { + "value": "[parameters('uniqueSuffix')]" + }, + "storageName": { + "value": "[variables('storageNameClean')]" + }, + "keyvaultName": { + "value": "[variables('keyVaultName')]" + }, + "aiServicesName": { + "value": "[variables('aiServiceName')]" + }, + "aiSearchName": { + "value": "[variables('aiSearchName')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "aisKind": { + "value": "[parameters('aisKind')]" + }, + "aiServicesExists": { + "value": "[not(empty(parameters('aiServiceAccountName')))]" + }, + "aiSearchExists": { + "value": "[not(empty(parameters('aiSearchServiceName')))]" + }, + "cosmosDBExists": { + "value": "[not(empty(parameters('cosmosDBResourceId')))]" + }, + "cosmosDBName": { + "value": "[variables('cosmosDBAccountName')]" + }, + "cosmosDBSubscription": { + "value": "[variables('cosmosDBSubscriptionId')]" + }, + "cosmosDBResourceGroup": { + "value": "[variables('cosmosDBResourceGroupName')]" + }, + "modelName": { + "value": "[parameters('modelName')]" + }, + "modelFormat": { + "value": "[parameters('modelFormat')]" + }, + "modelVersion": { + "value": "[parameters('modelVersion')]" + }, + "modelSkuName": { + "value": "[parameters('modelSkuName')]" + }, + "modelCapacity": { + "value": "[parameters('modelCapacity')]" + }, + "modelLocation": { + "value": "[parameters('modelLocation')]" + }, + "userAssignedIdentityName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiName.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.33.93.31351", + "templateHash": "13503356786023840518" + } + }, + "parameters": { + "storageExists": { + "type": "bool", + "defaultValue": false + }, + "keyvaultExists": { + "type": "bool", + "defaultValue": false + }, + "aiServicesExists": { + "type": "bool", + "defaultValue": false + }, + "aiSearchExists": { + "type": "bool", + "defaultValue": false + }, + "cosmosDBExists": { + "type": "bool", + "defaultValue": false + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to add to the resources" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Unique suffix for resource names" + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "AI services name" + } + }, + "keyvaultName": { + "type": "string", + "metadata": { + "description": "The name of the Key Vault" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "The name of the AI Search resource" + } + }, + "storageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "modelName": { + "type": "string", + "metadata": { + "description": "Model name for deployment" + } + }, + "modelFormat": { + "type": "string", + "metadata": { + "description": "Model format for deployment" + } + }, + "modelVersion": { + "type": "string", + "metadata": { + "description": "Model version for deployment" + } + }, + "modelSkuName": { + "type": "string", + "metadata": { + "description": "Model deployment SKU name" + } + }, + "modelCapacity": { + "type": "int", + "metadata": { + "description": "Model deployment capacity" + } + }, + "modelLocation": { + "type": "string", + "metadata": { + "description": "Model/AI Resource deployment location" + } + }, + "aisKind": { + "type": "string", + "metadata": { + "description": "The Kind of AI Service, can be \"OpenAI\" or \"AIService\"" + } + }, + "userAssignedIdentityName": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity name" + } + }, + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "Name of Cosmos DB account" + } + }, + "cosmosDBSubscription": { + "type": "string", + "metadata": { + "description": "Cosmos DB Subscription" + } + }, + "cosmosDBResourceGroup": { + "type": "string", + "metadata": { + "description": "Cosmos DB Resource Group" + } + }, + "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": { + "storageNameCleaned": "[if(parameters('storageExists'), parameters('storageName'), replace(parameters('storageName'), '-', ''))]", + "aiServiceParts": "[if(parameters('aiServicesExists'), split(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '/'), split(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '/'))]", + "acsParts": "[if(parameters('aiSearchExists'), split(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiSearchName')), '/'), split(resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')), '/'))]", + "storageParts": "[if(parameters('storageExists'), split(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), '/'), split(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned')), '/'))]" + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-07-31-preview", + "name": "[parameters('userAssignedIdentityName')]", + "location": "[parameters('location')]" + }, + { + "condition": "[not(parameters('cosmosDBExists'))]", + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('cosmosDBName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "GlobalDocumentDB", + "properties": { + "consistencyPolicy": { + "defaultConsistencyLevel": "Session" + }, + "publicNetworkAccess": "Disabled", + "disableLocalAuth": true, + "enableAutomaticFailover": false, + "enableMultipleWriteLocations": false, + "enableFreeTier": false, + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0, + "isZoneRedundant": false + } + ], + "databaseAccountOfferType": "Standard" + } + }, + { + "condition": "[not(parameters('keyvaultExists'))]", + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2022-07-01", + "name": "[parameters('keyvaultName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "createMode": "default", + "enabledForDeployment": true, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true, + "enableSoftDelete": true, + "enableRbacAuthorization": true, + "enablePurgeProtection": true, + "publicNetworkAccess": "Disabled", + "accessPolicies": [ + { + "tenantId": "[subscription().tenantId]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').principalId]", + "permissions": { + "secrets": [ + "set", + "get", + "list", + "delete", + "purge" + ] + } + } + ], + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny" + }, + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "[subscription().tenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName'))]" + ] + }, + { + "condition": "[not(parameters('aiServicesExists'))]", + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-06-01-preview", + "name": "[parameters('aiServicesName')]", + "location": "[parameters('modelLocation')]", + "sku": { + "name": "S0" + }, + "kind": "[parameters('aisKind')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "customSubDomainName": "[toLower(format('{0}', parameters('aiServicesName')))]", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny" + }, + "publicNetworkAccess": "Disabled" + } + }, + { + "condition": "[not(parameters('aiServicesExists'))]", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2024-06-01-preview", + "name": "[format('{0}/{1}', parameters('aiServicesName'), 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('aiServicesName'))]" + ] + }, + { + "condition": "[not(parameters('aiSearchExists'))]", + "type": "Microsoft.Search/searchServices", + "apiVersion": "2024-06-01-preview", + "name": "[parameters('aiSearchName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')))]": {} + } + }, + "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" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName'))]" + ] + }, + { + "condition": "[not(parameters('storageExists'))]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-05-01", + "name": "[variables('storageNameCleaned')]", + "location": "[parameters('location')]", + "kind": "StorageV2", + "sku": "[parameters('sku')]", + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "publicNetworkAccess": "Disabled", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny" + }, + "allowSharedKeyAccess": false + } + }, + { + "condition": "[not(parameters('storageExists'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('dependencies-{0}-storage-rbac', parameters('suffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "suffix": { + "value": "[parameters('suffix')]" + }, + "storageName": { + "value": "[variables('storageNameCleaned')]" + }, + "UAIPrincipalId": { + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "1498445684207187922" + } + }, + "parameters": { + "storageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "UAIPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity" + } + }, + "suffix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Unique suffix for resource naming" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('suffix'))]", + "properties": { + "principalId": "[parameters('UAIPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'), parameters('suffix'))]", + "properties": { + "principalId": "[parameters('UAIPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName'))]" + ] + }, + { + "condition": "[not(parameters('keyvaultExists'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('dependencies-{0}-keyvault-rbac', parameters('suffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "suffix": { + "value": "[parameters('suffix')]" + }, + "keyvaultName": { + "value": "[parameters('keyvaultName')]" + }, + "UAIPrincipalId": { + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName')), '2023-07-31-preview').principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "6274162434845500752" + } + }, + "parameters": { + "keyvaultName": { + "type": "string", + "metadata": { + "description": "Name of the Key Vault" + } + }, + "UAIPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Unique suffix for role assignment naming" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('keyvaultName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395'), parameters('suffix'))]", + "properties": { + "principalId": "[parameters('UAIPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('keyvaultName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), 'Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'), parameters('suffix'))]", + "properties": { + "principalId": "[parameters('UAIPrincipalId')]", + "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), 'Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7')]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "contributorRoleId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), 'Microsoft.Authorization/roleAssignments', guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395'), parameters('suffix')))]" + }, + "secretsOfficerRoleId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), 'Microsoft.Authorization/roleAssignments', guid(subscription().subscriptionId, resourceGroup().id, extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), 'Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'), parameters('suffix')))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentityName'))]" + ] + } + ], + "outputs": { + "aiServicesName": { + "type": "string", + "value": "[if(parameters('aiServicesExists'), parameters('aiServicesName'), parameters('aiServicesName'))]" + }, + "aiservicesID": { + "type": "string", + "value": "[if(parameters('aiServicesExists'), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')))]" + }, + "aiservicesTarget": { + "type": "string", + "value": "[if(parameters('aiServicesExists'), reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2024-06-01-preview').endpoint, reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2024-06-01-preview').endpoint)]" + }, + "aiServiceAccountResourceGroupName": { + "type": "string", + "value": "[variables('aiServiceParts')[4]]" + }, + "aiServiceAccountSubscriptionId": { + "type": "string", + "value": "[variables('aiServiceParts')[2]]" + }, + "aiSearchName": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), parameters('aiSearchName'), parameters('aiSearchName'))]" + }, + "aisearchID": { + "type": "string", + "value": "[if(parameters('aiSearchExists'), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiSearchName')), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]" + }, + "aiSearchServiceResourceGroupName": { + "type": "string", + "value": "[variables('acsParts')[4]]" + }, + "aiSearchServiceSubscriptionId": { + "type": "string", + "value": "[variables('acsParts')[2]]" + }, + "storageAccountName": { + "type": "string", + "value": "[if(parameters('storageExists'), parameters('storageName'), parameters('storageName'))]" + }, + "storageId": { + "type": "string", + "value": "[if(parameters('storageExists'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned')))]" + }, + "storageAccountResourceGroupName": { + "type": "string", + "value": "[variables('storageParts')[4]]" + }, + "storageAccountSubscriptionId": { + "type": "string", + "value": "[variables('storageParts')[2]]" + }, + "keyvaultId": { + "type": "string", + "value": "[if(parameters('keyvaultExists'), resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')))]" + }, + "cosmosDBName": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), parameters('cosmosDBName'), parameters('cosmosDBName'))]" + }, + "cosmosDBId": { + "type": "string", + "value": "[if(parameters('cosmosDBExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscription'), parameters('cosmosDBResourceGroup')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiHubName": { + "value": "[format('{0}-{1}', parameters('defaultAiHubName'), parameters('uniqueSuffix'))]" + }, + "aiHubFriendlyName": { + "value": "[parameters('defaultAiHubFriendlyName')]" + }, + "aiHubDescription": { + "value": "[parameters('defaultAiHubDescription')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiSearchName.value]" + }, + "aiSearchId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aisearchID.value]" + }, + "aiSearchServiceResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiSearchServiceResourceGroupName.value]" + }, + "aiSearchServiceSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiSearchServiceSubscriptionId.value]" + }, + "aiServicesName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiServicesId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiservicesID.value]" + }, + "aiServicesTarget": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiservicesTarget.value]" + }, + "aiServiceAccountResourceGroupName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiServiceAccountResourceGroupName.value]" + }, + "aiServiceAccountSubscriptionId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiServiceAccountSubscriptionId.value]" + }, + "keyVaultId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.keyvaultId.value]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.storageId.value]" + }, + "uaiName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiName.value]" + }, + "publicNetworkAccess": { + "value": "[parameters('hubPublicNetworkAccess')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "15229829833658622658" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "metadata": { + "description": "Tags to add to the resources" + } + }, + "aiHubName": { + "type": "string", + "metadata": { + "description": "AI hub name" + } + }, + "aiHubFriendlyName": { + "type": "string", + "defaultValue": "[parameters('aiHubName')]", + "metadata": { + "description": "AI hub display name" + } + }, + "aiHubDescription": { + "type": "string", + "metadata": { + "description": "AI hub description" + } + }, + "keyVaultId": { + "type": "string", + "metadata": { + "description": "Resource ID of the key vault for secure secret storage" + } + }, + "storageAccountId": { + "type": "string", + "metadata": { + "description": "Resource ID of the storage account for data persistence" + } + }, + "aiServicesId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Services" + } + }, + "aiServicesTarget": { + "type": "string", + "metadata": { + "description": "Endpoint URL of the AI Services" + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Name of the AI Services resource" + } + }, + "aiServiceAccountResourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group containing AI Services" + } + }, + "aiServiceAccountSubscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID containing AI Services" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name of the AI Search service" + } + }, + "aiSearchId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Search service" + } + }, + "aiSearchServiceResourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group containing AI Search" + } + }, + "aiSearchServiceSubscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID containing AI Search" + } + }, + "uaiName": { + "type": "string", + "metadata": { + "description": "Name of the user-assigned managed identity" + } + }, + "aiHubExists": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Flag indicating if the hub already exists" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Specifies the public network access for the machine learning workspace." + } + } + }, + "variables": { + "acsConnectionName": "[format('{0}-connection-AISearch', parameters('aiHubName'))]", + "aoaiConnection": "[format('{0}-connection-AIServices_aoai', parameters('aiHubName'))]", + "userAssignedIdentities": "[json(format('{{''{0}'': {{}}}}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('uaiName'))))]" + }, + "resources": [ + { + "condition": "[not(parameters('aiHubExists'))]", + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2024-07-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), format('{0}-connection-AIServices', parameters('aiHubName')))]", + "properties": { + "category": "AIServices", + "target": "[parameters('aiServicesTarget')]", + "authType": "AAD", + "isSharedToAll": true, + "metadata": { + "ApiType": "Azure", + "ResourceId": "[parameters('aiServicesId')]", + "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiServiceAccountSubscriptionId'), parameters('aiServiceAccountResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2023-05-01', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + ] + }, + { + "condition": "[not(parameters('aiHubExists'))]", + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2024-07-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), variables('acsConnectionName'))]", + "properties": { + "category": "CognitiveSearch", + "target": "[format('https://{0}.search.windows.net', parameters('aiSearchName'))]", + "authType": "AAD", + "isSharedToAll": true, + "metadata": { + "ApiType": "Azure", + "ResourceId": "[parameters('aiSearchId')]", + "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchServiceSubscriptionId'), parameters('aiSearchServiceResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName')), '2023-11-01', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + ] + }, + { + "condition": "[not(parameters('aiHubExists'))]", + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2024-10-01-preview", + "name": "[parameters('aiHubName')]", + "location": "[parameters('location')]", + "kind": "hub", + "tags": "[parameters('tags')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": "[variables('userAssignedIdentities')]" + }, + "properties": { + "friendlyName": "[parameters('aiHubFriendlyName')]", + "description": "[parameters('aiHubDescription')]", + "primaryUserAssignedIdentity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('uaiName'))]", + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "keyVault": "[parameters('keyVaultId')]", + "storageAccount": "[parameters('storageAccountId')]" + } + } + ], + "outputs": { + "aiHubID": { + "type": "string", + "value": "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + }, + "aiHubName": { + "type": "string", + "value": "[parameters('aiHubName')]" + }, + "aoaiConnectionName": { + "type": "string", + "value": "[variables('aoaiConnection')]" + }, + "acsConnectionName": { + "type": "string", + "value": "[variables('acsConnectionName')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--private-endpoint', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiServicesName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiSearchName.value]" + }, + "aiStorageId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.storageId.value]" + }, + "storageName": { + "value": "[variables('storageNameClean')]" + }, + "vnetName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.virtualNetworkName.value]" + }, + "cxSubnetName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.hubSubnetName.value]" + }, + "suffix": { + "value": "[parameters('uniqueSuffix')]" + }, + "hubWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiHubID.value]" + }, + "hubWorkspaceName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiHubName.value]" + }, + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.cosmosDBName.value]" + }, + "cosmosDBSubscription": { + "value": "[variables('cosmosDBSubscriptionId')]" + }, + "cosmosDBResourceGroup": { + "value": "[variables('cosmosDBResourceGroupName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "2815470915491127245" + } + }, + "parameters": { + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Name of the AI Services 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 Customer Cosmos DB" + } + }, + "cosmosDBSubscription": { + "type": "string", + "metadata": { + "description": "Name of the Customer Cosmos DB Subscription" + } + }, + "cosmosDBResourceGroup": { + "type": "string", + "metadata": { + "description": "Name of the Customer Cosmos DB Resource Group" + } + }, + "vnetName": { + "type": "string" + }, + "cxSubnetName": { + "type": "string", + "metadata": { + "description": "Name of the Customer subnet" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "aiStorageId": { + "type": "string", + "metadata": { + "description": "Name of the AI Storage Account" + } + }, + "hubWorkspaceId": { + "type": "string", + "metadata": { + "description": "Specifies the resource id of the Azure Hub Workspace." + } + }, + "hubWorkspaceName": { + "type": "securestring", + "metadata": { + "description": "Name of the Customer Hub Workspace" + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('aiServicesName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('aiServicesName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "groupIds": [ + "account" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-openAi-private-endpoint', parameters('aiServicesName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-openAi-private-link-service-connection', parameters('aiServicesName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "groupIds": [ + "account" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('aiSearchName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('aiSearchName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('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": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('storageName'))]", + "properties": { + "privateLinkServiceId": "[parameters('aiStorageId')]", + "groupIds": [ + "blob" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('cosmosDBName'))]", + "location": "[resourceGroup().location]", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('cosmosDBName'))]", + "properties": { + "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscription'), parameters('cosmosDBResourceGroup')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", + "groupIds": [ + "Sql" + ] + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-11-01", + "name": "[format('{0}-private-endpoint', parameters('hubWorkspaceName'))]", + "location": "[resourceGroup().location]", + "properties": { + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('hubWorkspaceName'))]", + "properties": { + "privateLinkServiceId": "[parameters('hubWorkspaceId')]", + "groupIds": [ + "amlworkspace" + ] + } + } + ], + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('cxSubnetName'))]" + } + } + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.azureml.ms", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.openai.azure.com", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', 'privatelink.azureml.ms', format('aiServices-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azureml.ms')]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', 'privatelink.openai.azure.com', format('aiServicesOpenAI-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.openai.azure.com')]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiServicesName')), format('{0}-dns-group', parameters('aiServicesName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('aiServicesName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azureml.ms')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', 'privatelink.azureml.ms', format('aiServices-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azureml.ms')]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiServicesName')))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-openAi-private-endpoint', parameters('aiServicesName')), format('{0}-openAi-dns-group', parameters('aiServicesName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-openAi-dns-config', parameters('aiServicesName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.openai.azure.com')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', 'privatelink.openai.azure.com', format('aiServicesOpenAI-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-openAi-private-endpoint', parameters('aiServicesName')))]", + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.openai.azure.com')]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.api.azureml.ms", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.notebooksazureml.net", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', 'privatelink.api.azureml.ms', format('mlApi-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.api.azureml.ms')]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', 'privatelink.notebooksazureml.net', format('mlNotebook-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.notebooksazureml.net')]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('hubWorkspaceName')), format('{0}-mlApiNotebook-dns-group', parameters('hubWorkspaceName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-mlApi-dns-config', parameters('hubWorkspaceName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.api.azureml.ms')]" + } + }, + { + "name": "[format('{0}-mlNotebook-dns-config', parameters('hubWorkspaceName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.notebooksazureml.net')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('hubWorkspaceName')))]", + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.api.azureml.ms')]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', 'privatelink.api.azureml.ms', format('mlApi-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.notebooksazureml.net')]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', 'privatelink.notebooksazureml.net', format('mlNotebook-{0}-link', parameters('suffix')))]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.search.windows.net", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', 'privatelink.search.windows.net', format('aiSearch-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.search.windows.net')]" + ] + }, + { + "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": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.search.windows.net')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.search.windows.net')]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiSearchName')))]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[format('privatelink.blob.{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')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{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')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('storageName'))]", + "properties": { + "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('storageName')))]" + ] + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "privatelink.documents.azure.com", + "location": "global" + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', 'privatelink.documents.azure.com', format('cosmosDB-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]" + ] + }, + { + "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": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', 'privatelink.documents.azure.com', format('cosmosDB-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.documents.azure.com')]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('cosmosDBName')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiProjectName": { + "value": "[format('{0}-{1}', parameters('defaultAiProjectName'), parameters('uniqueSuffix'))]" + }, + "aiProjectFriendlyName": { + "value": "[parameters('defaultAiProjectFriendlyName')]" + }, + "aiProjectDescription": { + "value": "[parameters('defaultAiProjectDescription')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "aiHubId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiHubID.value]" + }, + "uaiName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiName.value]" + }, + "publicNetworkAccess": { + "value": "[parameters('projectPublicNetworkAccess')]" + }, + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.cosmosDBName.value]" + }, + "cosmosDBSubscriptionId": { + "value": "[variables('cosmosDBSubscriptionId')]" + }, + "cosmosDBResourceGroupName": { + "value": "[variables('cosmosDBResourceGroupName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "10639340475928662664" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "metadata": { + "description": "Tags to add to the resources" + } + }, + "aiProjectName": { + "type": "string", + "metadata": { + "description": "AI Project name" + } + }, + "aiProjectFriendlyName": { + "type": "string", + "defaultValue": "[parameters('aiProjectName')]", + "metadata": { + "description": "AI Project display name" + } + }, + "aiProjectDescription": { + "type": "string", + "metadata": { + "description": "AI Project description" + } + }, + "aiHubId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Hub resource" + } + }, + "uaiName": { + "type": "string", + "metadata": { + "description": "Name of the user-assigned managed identity" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "metadata": { + "description": "Specifies the public network access for the machine learning workspace." + } + }, + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "The name of the CosmosDB account" + } + }, + "cosmosDBSubscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the Cosmos DB resource" + } + }, + "cosmosDBResourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group name of the Cosmos DB resource" + } + } + }, + "variables": { + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroupName": "[resourceGroup().name]", + "projectConnectionString": "[format('{0}.api.azureml.ms;{1};{2};{3}', parameters('location'), variables('subscriptionId'), variables('resourceGroupName'), parameters('aiProjectName'))]", + "cosmosConnectionName": "[format('{0}-connection-CosmosDBAccount', parameters('aiProjectName'))]" + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-08-01-preview", + "name": "[parameters('aiProjectName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), createObject('ProjectConnectionString', variables('projectConnectionString')))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('uaiName')))]": {} + } + }, + "properties": { + "friendlyName": "[parameters('aiProjectFriendlyName')]", + "description": "[parameters('aiProjectDescription')]", + "primaryUserAssignedIdentity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('uaiName'))]", + "hubResourceId": "[parameters('aiHubId')]", + "publicNetworkAccess": "[parameters('publicNetworkAccess')]" + }, + "kind": "project" + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2025-01-01-preview", + "name": "[format('{0}/{1}', parameters('aiProjectName'), variables('cosmosConnectionName'))]", + "properties": { + "category": "CosmosDB", + "target": "[format('https://{0}.documents.azure.com:443/', parameters('cosmosDBName'))]", + "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.MachineLearningServices/workspaces', parameters('aiProjectName'))]" + ] + } + ], + "outputs": { + "aiProjectName": { + "type": "string", + "value": "[parameters('aiProjectName')]" + }, + "aiProjectResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName'))]" + }, + "aiProjectWorkspaceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), '2023-08-01-preview').workspaceId]" + }, + "projectConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), '2023-08-01-preview', 'full').tags.ProjectConnectionString]" + }, + "cosmosConnectionName": { + "type": "string", + "value": "[variables('cosmosConnectionName')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--private-endpoint', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--AiServices-RA', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiServicesName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiPrincipalId.value]" + }, + "aiProjectId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectResourceId.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.33.93.31351", + "templateHash": "12976852655018469345" + } + }, + "parameters": { + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Name of the AI Services account" + } + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity" + } + }, + "aiProjectId": { + "type": "string", + "metadata": { + "description": "Unique identifier for role assignments" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('cosmos-account-role-assignments-{0}-{1}-deployment', toLower(format('{0}', parameters('defaultAiProjectName'))), parameters('uniqueSuffix'))]", + "subscriptionId": "[variables('cosmosDBSubscriptionId')]", + "resourceGroup": "[variables('cosmosDBResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosDBName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.cosmosDBName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiPrincipalId.value]" + }, + "projectWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectWorkspaceId.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.33.93.31351", + "templateHash": "14045621062976275724" + } + }, + "parameters": { + "cosmosDBName": { + "type": "string", + "metadata": { + "description": "Name of the Cosmos DB account" + } + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the AI project" + } + }, + "projectWorkspaceId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]", + "name": "[guid(parameters('projectWorkspaceId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--AiSearch-RA', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiSearchName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiSearchName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiPrincipalId.value]" + }, + "aiProjectId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectResourceId.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.33.93.31351", + "templateHash": "16079856828729639404" + } + }, + "parameters": { + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name of the AI Search service" + } + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity" + } + }, + "aiProjectId": { + "type": "string", + "metadata": { + "description": "Unique suffix for resource naming" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "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(subscription().subscriptionId, resourceGroup().id, resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}--capability-host', parameters('name'), parameters('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "capabilityHostName": { + "value": "[format('{0}-{1}', parameters('uniqueSuffix'), parameters('defaultCapabilityHostName'))]" + }, + "aiHubName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiHubName.value]" + }, + "aiProjectName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectName.value]" + }, + "acsConnectionName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.acsConnectionName.value]" + }, + "aoaiConnectionName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aoaiConnectionName.value]" + }, + "customerSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.agentsSubnetId.value]" + }, + "cosmosConnectionName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.cosmosConnectionName.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.33.93.31351", + "templateHash": "1173900748886822455" + } + }, + "parameters": { + "aiHubName": { + "type": "string", + "metadata": { + "description": "AI hub name" + } + }, + "aiProjectName": { + "type": "string", + "metadata": { + "description": "AI project name" + } + }, + "acsConnectionName": { + "type": "string", + "metadata": { + "description": "Name for ACS connection." + } + }, + "aoaiConnectionName": { + "type": "string", + "metadata": { + "description": "Name for ACS connection." + } + }, + "capabilityHostName": { + "type": "string", + "metadata": { + "description": "Name for capabilityHost." + } + }, + "customerSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name for customer subnet id" + } + }, + "cosmosConnectionName": { + "type": "string", + "metadata": { + "description": "Name for CosmosDB connection." + } + } + }, + "variables": { + "storageConnections": [ + "[format('{0}/workspaceblobstore', parameters('aiProjectName'))]" + ], + "aiSearchConnection": [ + "[format('{0}', parameters('acsConnectionName'))]" + ], + "aiServiceConnections": [ + "[format('{0}', parameters('aoaiConnectionName'))]" + ], + "cosmosDBConnections": [ + "[format('{0}', parameters('cosmosConnectionName'))]" + ] + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces/capabilityHosts", + "apiVersion": "2024-10-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), format('{0}-{1}', parameters('aiHubName'), parameters('capabilityHostName')))]", + "properties": "[if(empty(parameters('customerSubnetId')), createObject('capabilityHostKind', 'Agents'), createObject('capabilityHostKind', 'Agents', 'customerSubnet', parameters('customerSubnetId')))]" + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/capabilityHosts", + "apiVersion": "2025-01-01-preview", + "name": "[format('{0}/{1}', parameters('aiProjectName'), format('{0}-{1}', parameters('aiProjectName'), parameters('capabilityHostName')))]", + "properties": { + "capabilityHostKind": "Agents", + "aiServicesConnections": "[variables('aiServiceConnections')]", + "vectorStoreConnections": "[variables('aiSearchConnection')]", + "storageConnections": "[variables('storageConnections')]", + "threadStorageConnections": "[variables('cosmosDBConnections')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/capabilityHosts', parameters('aiHubName'), format('{0}-{1}', parameters('aiHubName'), parameters('capabilityHostName')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--hub', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--AiSearch-RA', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--AiServices-RA', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--vnet', parameters('name'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('cosmos-container-role-assignments-{0}-{1}-deployment', toLower(format('{0}', parameters('defaultAiProjectName'))), parameters('uniqueSuffix'))]", + "subscriptionId": "[variables('cosmosDBSubscriptionId')]", + "resourceGroup": "[variables('cosmosDBResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.cosmosDBName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.uaiPrincipalId.value]" + }, + "aiProjectId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectResourceId.value]" + }, + "projectWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectWorkspaceId.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.33.93.31351", + "templateHash": "3236725067655686559" + } + }, + "parameters": { + "cosmosAccountName": { + "type": "string", + "metadata": { + "description": "Name of the cosmos DB account" + } + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the AI project" + } + }, + "aiProjectId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI project" + } + }, + "projectWorkspaceId": { + "type": "string" + } + }, + "variables": { + "userThreadName": "[format('{0}-thread-message-store', parameters('projectWorkspaceId'))]", + "systemThreadName": "[format('{0}-system-thread-message-store', parameters('projectWorkspaceId'))]", + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosAccountName'), '00000000-0000-0000-0000-000000000002')]", + "scopeSystemContainer": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DocumentDB/databaseAccounts/{2}/dbs/enterprise_memory/colls/{3}', subscription().subscriptionId, resourceGroup().name, parameters('cosmosAccountName'), variables('systemThreadName'))]", + "scopeUserContainer": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DocumentDB/databaseAccounts/{2}/dbs/enterprise_memory/colls/{3}', subscription().subscriptionId, resourceGroup().name, parameters('cosmosAccountName'), variables('userThreadName'))]" + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2022-05-15", + "name": "[format('{0}/{1}', parameters('cosmosAccountName'), guid(parameters('aiProjectId'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosAccountName'), 'enterprise_memory', variables('userThreadName')), variables('roleDefinitionId')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[variables('roleDefinitionId')]", + "scope": "[variables('scopeUserContainer')]" + } + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2022-05-15", + "name": "[format('{0}/{1}', parameters('cosmosAccountName'), guid(parameters('aiProjectId'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosAccountName'), 'enterprise_memory', variables('systemThreadName')), variables('roleDefinitionId')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[variables('roleDefinitionId')]", + "scope": "[variables('scopeSystemContainer')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--capability-host', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--dependencies', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}--identity', parameters('name'), parameters('uniqueSuffix')))]" + ] + } + ], + "outputs": { + "PROJECT_CONNECTION_STRING": { + "type": "string", + "value": "[if(equals(parameters('hubPublicNetworkAccess'), 'Enabled'), reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.projectConnectionString.value, format('{0};{1};{2};{3}', format('{0}.workspace.{1}', reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.aiProjectWorkspaceId.value, split(reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.projectConnectionString.value, ';')[0]), split(reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.projectConnectionString.value, ';')[1], split(reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.projectConnectionString.value, ';')[2], split(reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}--project', parameters('name'), parameters('uniqueSuffix'))), '2022-09-01').outputs.projectConnectionString.value, ';')[3]))]" + } + } +} \ No newline at end of file diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.parameters.json b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.parameters.json new file mode 100644 index 00000000..828d67ef --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/azuredeploy.parameters.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "defaultAiHubName": { + "value": "hub-demo" + }, + "defaultAiHubFriendlyName": { + "value": "Agents Hub resource" + }, + "defaultAiHubDescription": { + "value": "This is an example AI resource for use in Azure AI Studio." + }, + "defaultAiProjectName": { + "value": "project-demo" + }, + "defaultAiProjectFriendlyName": { + "value": "Agents Project resource" + }, + "defaultAiProjectDescription": { + "value": "This is an example AI Project resource for use in Azure AI Studio." + }, + "tags": { + "value": {} + }, + "defaultAiSearchName": { + "value": "agent-ai-search" + }, + "defaultCapabilityHostName": { + "value": "caphost1" + }, + "defaultStorageName": { + "value": "agentstorage" + }, + "defaultAiServicesName": { + "value": "agent-ai-services" + }, + "modelName": { + "value": "gpt-4o-mini" + }, + "modelFormat": { + "value": "OpenAI" + }, + "modelVersion": { + "value": "2024-07-18" + }, + "modelSkuName": { + "value": "GlobalStandard" + }, + "modelCapacity": { + "value": 50 + }, + "modelLocation": { + "value": "westus2" + }, + "cosmosDBResourceId":{ + "value": "" + } + } + } diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep new file mode 100644 index 00000000..27997b8c --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/main.bicep @@ -0,0 +1,399 @@ +/* +Network-Secured Agent Architecture Overview +----------------------------------------- +This template deploys an AI agent infrastructure in a network-secured configuration: + +1. Network Security: + - All services are deployed with private endpoints + - Access is restricted through VNet integration + - Private DNS zones manage internal name resolution + +2. Key Network Components: + - Virtual Network: Isolated network environment for all resources + - Subnets: Segregated network spaces for different service types + - Private Endpoints: Secure access points for Azure services + - Private DNS Zones: Internal name resolution for private endpoints + +3. Security Design: + - No public internet exposure for core services + - Network isolation between components + - Managed identity for secure authentication +*/ + +// Existing Resource Overrides - Used when connecting to pre-existing resources +var keyVaultOverride = '' // Override for existing Key Vault +var userAssignedIdentityOverride = '' // Override for existing managed identity + +/* ---------------------------------- Deployment Identifiers ---------------------------------- */ + +param name string = 'network-secured-agent' + +// Create a short, unique suffix, that will be unique to each resource group +param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') +param uniqueSuffix string = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) + +/* ---------------------------------- Default Parameters if Overrides Not Set ---------------------------------- */ + +// Parameters +@minLength(2) +@maxLength(12) +@description('Name for the AI resource and used to derive name of dependent resources.') +param defaultAiHubName string = 'hub-demo' + +@description('Friendly name for your Hub resource') +param defaultAiHubFriendlyName string = 'Agents Hub resource' + +@description('Description of your Azure AI resource displayed in AI studio') +param defaultAiHubDescription string = 'This is an example AI resource for use in Azure AI Studio.' + +@description('Name for the AI project resources.') +param defaultAiProjectName string = 'project-demo' + +@description('Friendly name for your Azure AI resource') +param defaultAiProjectFriendlyName string = 'Agents Project resource' + +@description('Specifies the public network access for the Azure AI Hub workspace.') +@allowed([ + 'Disabled' + 'Enabled' +]) +param hubPublicNetworkAccess string = 'Disabled' + +@description('Specifies the public network access for the Azure AI Project workspace.Note: Please ensure that if you are setting this to Enabled, the AI Hub workspace is also set to Enabled.') +@allowed([ + 'Disabled' + 'Enabled' +]) +param projectPublicNetworkAccess string = hubPublicNetworkAccess + + +@description('Description of your Azure AI resource displayed in AI studio') +param defaultAiProjectDescription string = 'This is an example AI Project resource for use in Azure AI Studio.' + +@description('Resource group location') +param resourceGroupLocation string = resourceGroup().location + +@allowed([ + 'australiaeast' + 'eastus' + 'eastus2' + 'francecentral' + 'japaneast' + 'norwayeast' + 'southindia' + 'swedencentral' + 'uaenorth' + 'uksouth' + 'westus' + 'westus3' +]) +@description('Location for all resources.') +param location string = resourceGroupLocation + +@description('Set of tags to apply to all resources.') +param tags object = {} + +@description('Name of the Azure AI Search account') +param defaultAiSearchName string = 'agent-ai-search' + +@description('Name for capabilityHost.') +param defaultCapabilityHostName string = 'caphost1' + +@description('Name of the storage account') +param defaultStorageName string = 'agentstorage' + +@description('Name of the Azure AI Services account') +param defaultAiServicesName string = 'agent-ai-service' + +@description('Name of the Azure Cosmos DB account') +param defaultCosmosDBName string = 'agent-thread-storage' + +@description('Model name for deployment') +param modelName string = 'gpt-4o-mini' + +@description('Model format for deployment') +param modelFormat string = 'OpenAI' + +@description('Model version for deployment') +param modelVersion string = '2024-07-18' + +@description('Model deployment SKU name') +param modelSkuName string = 'GlobalStandard' + +@description('Model deployment capacity') +param modelCapacity int = 50 + +@description('Model deployment location. If you want to deploy an Azure AI resource/model in different location than the rest of the resources created.') +param modelLocation string = resourceGroupLocation + +@description('AI service kind, values can be "OpenAI" or "AIService"') +param aisKind string = 'AIServices' + +@description('The AI Service 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 aiServiceAccountName string = '' + +@description('The AI Search Service name. This is an optional field, and if not provided, the resource will be created.The resource should exist in same resource group must be Public Network Disabled') +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('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 = '' + +/* ---------------------------------- Create User Assigned Identity ---------------------------------- */ + +@description('The name of User Assigned Identity') +param userAssignedIdentityDefaultName string = 'secured-agents-identity-${uniqueSuffix}' +var uaiName = (userAssignedIdentityOverride == '') ? userAssignedIdentityDefaultName : userAssignedIdentityOverride + + + +module identity 'modules-network-secured/network-secured-identity.bicep' = { + name: '${name}-${uniqueSuffix}--identity' + params: { + location: location + userAssignedIdentityName: uaiName + uaiExists: userAssignedIdentityOverride != '' + } +} + +/* ---------------------------------- Create AI Assistant Dependent Resources ---------------------------------- */ + +// var storageName = empty(aiStorageAccountName) ? '${defaultStorageName}${uniqueSuffix}' : aiStorageAccountName +var keyVaultName = empty(keyVaultOverride) ? 'kv-${defaultAiHubName}-${uniqueSuffix}' : keyVaultOverride +var aiServiceName = empty(aiServiceAccountName) ? '${defaultAiServicesName}${uniqueSuffix}' : aiServiceAccountName +var aiSearchName = empty(aiSearchServiceName) ? '${defaultAiSearchName}${uniqueSuffix}' : aiSearchServiceName + +var cosmosExists = !empty(cosmosDBResourceId) +var storageNameClean = '${defaultStorageName}${uniqueSuffix}' +var cosmosDBName = '${defaultCosmosDBName}${uniqueSuffix}' +var cosmosParts = split(cosmosDBResourceId, '/') +var cosmosDBSubscriptionId = cosmosExists ? cosmosParts[2] : subscription().subscriptionId +var cosmosDBResourceGroupName = cosmosExists ? cosmosParts[4] : resourceGroup().name +var cosmosDBAccountName = cosmosExists ? cosmosParts[8] : cosmosDBName +// Create Virtual Network and Subnets +module vnet 'modules-network-secured/networking/vnet.bicep' = { + name: '${name}-${uniqueSuffix}--vnet' + params: { + location: location + tags: tags + suffix: uniqueSuffix + modelLocation: modelLocation + } + dependsOn: [ + identity + ] +} + +// Dependent resources for the Azure Machine Learning workspace +module aiDependencies 'modules-network-secured/network-secured-dependent-resources.bicep' = { + name: '${name}-${uniqueSuffix}--dependencies' + params: { + suffix: uniqueSuffix + storageName: storageNameClean + keyvaultName: keyVaultName + aiServicesName: aiServiceName + aiSearchName: aiSearchName + tags: tags + location: location + aisKind: aisKind + + aiServicesExists: !empty(aiServiceAccountName) + aiSearchExists: !empty(aiSearchServiceName) + cosmosDBExists: !empty(cosmosDBResourceId) + cosmosDBName: cosmosDBAccountName + cosmosDBSubscription: cosmosDBSubscriptionId + cosmosDBResourceGroup: cosmosDBResourceGroupName + // Model deployment parameters + modelName: modelName + modelFormat: modelFormat + modelVersion: modelVersion + modelSkuName: modelSkuName + modelCapacity: modelCapacity + modelLocation: modelLocation + // User-assigned managed identity + userAssignedIdentityName: identity.outputs.uaiName + } +} + + + +module aiHub 'modules-network-secured/network-secured-ai-hub.bicep' = { + name: '${name}-${uniqueSuffix}--hub' + params: { + // workspace organization + aiHubName: '${defaultAiHubName}-${uniqueSuffix}' + aiHubFriendlyName: defaultAiHubFriendlyName + aiHubDescription: defaultAiHubDescription + location: location + tags: tags + + aiSearchName: aiDependencies.outputs.aiSearchName + aiSearchId: aiDependencies.outputs.aisearchID + aiSearchServiceResourceGroupName: aiDependencies.outputs.aiSearchServiceResourceGroupName + aiSearchServiceSubscriptionId: aiDependencies.outputs.aiSearchServiceSubscriptionId + + aiServicesName: aiDependencies.outputs.aiServicesName + aiServicesId: aiDependencies.outputs.aiservicesID + aiServicesTarget: aiDependencies.outputs.aiservicesTarget + aiServiceAccountResourceGroupName:aiDependencies.outputs.aiServiceAccountResourceGroupName + aiServiceAccountSubscriptionId:aiDependencies.outputs.aiServiceAccountSubscriptionId + + keyVaultId: aiDependencies.outputs.keyvaultId + storageAccountId: aiDependencies.outputs.storageId + + uaiName: identity.outputs.uaiName + publicNetworkAccess: hubPublicNetworkAccess // Public network access for the workspace + } +} + +resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { + name: aiDependencies.outputs.storageAccountName + scope: resourceGroup() +} + +resource aiServices 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { + name: aiDependencies.outputs.aiServicesName + scope: resourceGroup(aiDependencies.outputs.aiServiceAccountSubscriptionId, aiDependencies.outputs.aiServiceAccountResourceGroupName) +} + +resource aiSearch 'Microsoft.Search/searchServices@2023-11-01' existing = { + name: aiDependencies.outputs.aiSearchName + scope: resourceGroup(aiDependencies.outputs.aiSearchServiceSubscriptionId, aiDependencies.outputs.aiSearchServiceResourceGroupName) +} + +resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = { + name: aiDependencies.outputs.cosmosDBName + scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) +} + +// Private Endpoint and DNS Configuration +// This module sets up private network access for all Azure services: +// 1. Creates private endpoints in the specified subnet +// 2. Sets up private DNS zones for each service: +// - privatelink.search.windows.net for AI Search +// - privatelink.cognitiveservices.azure.com for AI Services +// - privatelink.blob.core.windows.net for Storage +// 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: '${name}-${uniqueSuffix}--private-endpoint' + params: { + aiServicesName: aiDependencies.outputs.aiServicesName // AI Services to secure + aiSearchName: aiDependencies.outputs.aiSearchName // AI Search to secure + aiStorageId: aiDependencies.outputs.storageId // Storage to secure + + storageName: storageNameClean // Clean storage name for DNS + vnetName: vnet.outputs.virtualNetworkName // VNet containing subnets + cxSubnetName: vnet.outputs.hubSubnetName // Subnet for private endpoints + suffix: uniqueSuffix // Unique identifier + hubWorkspaceId: aiHub.outputs.aiHubID // AI Hub workspace ID + hubWorkspaceName: aiHub.outputs.aiHubName // AI Hub workspace name + cosmosDBName: aiDependencies.outputs.cosmosDBName // Cosmos DB name + cosmosDBSubscription: cosmosDBSubscriptionId // Cosmos DB subscription ID + cosmosDBResourceGroup: cosmosDBResourceGroupName // Cosmos DB resource group name + } + dependsOn: [ + aiServices // Ensure AI Services exist + aiSearch // Ensure AI Search exists + storage // Ensure Storage exists + cosmosDB // Ensure Cosmos DB exists + ] +} + +module aiProject 'modules-network-secured/network-secured-ai-project.bicep' = { + name: '${name}-${uniqueSuffix}--project' + params: { + // workspace organization + aiProjectName: '${defaultAiProjectName}-${uniqueSuffix}' + aiProjectFriendlyName: defaultAiProjectFriendlyName + aiProjectDescription: defaultAiProjectDescription + location: location + tags: tags + aiHubId: aiHub.outputs.aiHubID + uaiName: identity.outputs.uaiName + publicNetworkAccess: projectPublicNetworkAccess // Public network access for the workspace + cosmosDBName: aiDependencies.outputs.cosmosDBName + cosmosDBSubscriptionId: cosmosDBSubscriptionId + cosmosDBResourceGroupName: cosmosDBResourceGroupName + } + dependsOn: [ + privateEndpointAndDNS + cosmosDB + ] +} + +module aiServiceRoleAssignments 'modules-network-secured/ai-service-role-assignments.bicep' = { + name: '${name}-${uniqueSuffix}--AiServices-RA' + scope: resourceGroup() + params: { + aiServicesName: aiDependencies.outputs.aiServicesName + aiProjectPrincipalId: identity.outputs.uaiPrincipalId + aiProjectId: aiProject.outputs.aiProjectResourceId + } +} + +module cosmosAccountRoleAssignments 'modules-network-secured/database/cosmos-account-role-assignments.bicep' = { + name: 'cosmos-account-role-assignments-${toLower('${defaultAiProjectName}')}-${uniqueSuffix}-deployment' + scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) + params: { + cosmosDBName: aiDependencies.outputs.cosmosDBName + aiProjectPrincipalId: identity.outputs.uaiPrincipalId + projectWorkspaceId: aiProject.outputs.aiProjectWorkspaceId + } + dependsOn: [ + cosmosDB + ] +} + +module aiSearchRoleAssignments 'modules-network-secured/ai-search-role-assignments.bicep' = { + name: '${name}-${uniqueSuffix}--AiSearch-RA' + scope: resourceGroup() + params: { + aiSearchName: aiDependencies.outputs.aiSearchName + aiProjectPrincipalId: identity.outputs.uaiPrincipalId + aiProjectId: aiProject.outputs.aiProjectResourceId + } +} + +module addCapabilityHost 'modules-network-secured/network-capability-host.bicep' = { + name: '${name}-${uniqueSuffix}--capability-host' + params: { + capabilityHostName: '${uniqueSuffix}-${defaultCapabilityHostName}' + aiHubName: aiHub.outputs.aiHubName + aiProjectName: aiProject.outputs.aiProjectName + acsConnectionName: aiHub.outputs.acsConnectionName + aoaiConnectionName: aiHub.outputs.aoaiConnectionName + customerSubnetId: vnet.outputs.agentsSubnetId + cosmosConnectionName: aiProject.outputs.cosmosConnectionName + } + dependsOn: [ + aiSearchRoleAssignments, aiServiceRoleAssignments + ] +} + +module cosmosContainerRoleAssignments 'modules-network-secured/database/cosmos-container-role-assignment.bicep' = { + name: 'cosmos-container-role-assignments-${toLower('${defaultAiProjectName}')}-${uniqueSuffix}-deployment' + scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName) + params: { + cosmosAccountName: aiDependencies.outputs.cosmosDBName + aiProjectPrincipalId: identity.outputs.uaiPrincipalId + aiProjectId: aiProject.outputs.aiProjectResourceId + projectWorkspaceId: aiProject.outputs.aiProjectWorkspaceId + +} +dependsOn: [ + addCapabilityHost + ] +} + +var projectConnectionString = aiProject.outputs.projectConnectionString +var parts = split(projectConnectionString, ';') +var projectHost = parts[0] +var subscriptionId = parts[1] +var resourceGroupName = parts[2] +var projectName = parts[3] +var privateProjectHost = '${aiProject.outputs.aiProjectWorkspaceId}.workspace.${projectHost}' +output PROJECT_CONNECTION_STRING string = hubPublicNetworkAccess == 'Enabled' ? aiProject.outputs.projectConnectionString : '${privateProjectHost};${subscriptionId};${resourceGroupName};${projectName}' diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/metadata.json b/scenarios/Agents/setup/network-secured-agent-thread-storage/metadata.json new file mode 100644 index 00000000..600ff5f5 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/metadata.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://aka.ms/azure-quickstart-templates-metadata-schema#", + "type": "QuickStart", + "itemDisplayName": "Network Secured Agent with support for Thread Storage", + "description": "This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation using User Managed Identity authetication for the AI Service/AOAI connection and private network links to connect the agent to your secure data.", + "summary": "This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation and authentication using user managed identity", + "githubUsername": "jkrame", + "dateUpdated": "2025-04-09", + "environments": [ + "AzureCloud" + ] +} \ No newline at end of file diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-role-assignments.bicep new file mode 100644 index 00000000..911c0ff3 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-role-assignments.bicep @@ -0,0 +1,72 @@ +/* +AI Search Role Assignments Module +------------------------------- +This module configures RBAC permissions for AI Search service access: + +1. Role Assignments: + - Search Index Data Contributor: Read/write access to search indexes + - Search Service Contributor: Manage search service settings + +2. Unique Assignment Names: + - Uses deployment-specific suffix for unique role assignments + - Prevents conflicts in multi-deployment scenarios +*/ + +@description('Name of the AI Search service') +param aiSearchName string + +@description('Principal ID of the managed identity') +param aiProjectPrincipalId string + +@description('Unique suffix for resource naming') +param aiProjectId string + +// Reference existing search service +resource searchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = { + name: aiSearchName + scope: resourceGroup() +} + +/* -------------------------------------------- Role Definitions -------------------------------------------- */ + +// Search Index Data Contributor Role +// Provides read/write access to search indexes and their data +resource searchIndexDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' // Built-in role ID + scope: resourceGroup() +} + +// Search Service Contributor Role +// Provides access to manage search service settings +resource searchServiceContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Built-in role ID + scope: resourceGroup() +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +// Assign Search Index Data Contributor role +// Uses subscription ID and timestamp in guid to ensure uniqueness +resource searchIndexDataContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, searchIndexDataContributorRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: searchIndexDataContributorRole.id + principalType: 'ServicePrincipal' + } +} + +// Assign Search Service Contributor role +// Uses subscription ID and timestamp in guid to ensure uniqueness +resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, searchServiceContributorRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: searchServiceContributorRole.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-service.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-service.bicep new file mode 100644 index 00000000..9bba3d24 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-search-service.bicep @@ -0,0 +1,87 @@ +/* +AI Search Service Module +---------------------- +This module deploys an Azure AI Search service with network security controls: + +1. Security Features: + - User-assigned managed identity for authentication + - Public network access disabled + - AAD-based authentication + - Optional CMK encryption support + +2. Network Security: + - Private endpoint access only + - No public internet exposure + - AAD authentication with bearer challenge + +3. Service Configuration: + - Standard SKU for production workloads + - Configurable partition and replica counts + - Managed identity integration + +4. Authentication: + - AAD-first authentication model + - Bearer token challenge for failed auth + - Local auth optionally available +*/ + +/* -------------------------------------------- Parameters -------------------------------------------- */ + +@description('Name of the user-assigned managed identity') +param userAssignedIdentityName string + +@description('Name of the AI Search service') +param aiSearchName string + +@description('Azure region for the search service') +param searchLocation string + +@description('Tags to add to the resources') +param tags object = {} + +/* -------------------------------------------- Resources -------------------------------------------- */ + +// Reference to user-assigned managed identity +resource uai 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = { + name: userAssignedIdentityName + scope: resourceGroup() +} + +// AI Search Service +// Documentation: https://learn.microsoft.com/en-us/azure/templates/microsoft.search/searchservices +resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = { + name: aiSearchName + location: searchLocation + tags: tags + identity: { + type: 'UserAssigned' // Use managed identity for authentication + userAssignedIdentities: { + '${uai.id}': {} + } + } + properties: { + disableLocalAuth: false // Allow both AAD and API key auth + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' // Proper AAD auth challenge + } + } + encryptionWithCmk: { + enforcement: 'Unspecified' // Default encryption mode + } + hostingMode: 'default' // Standard hosting mode + partitionCount: 1 // Number of search partitions + publicNetworkAccess: 'Disabled' // Force private endpoint access + replicaCount: 1 // Number of search replicas + semanticSearch: 'disabled' // Semantic search capability + } + sku: { + name: 'standard' // Production-grade SKU + } +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +output searchServiceName string = aiSearch.name +output searchServiceId string = aiSearch.id +output searchServiceEndpoint string = 'https://${aiSearch.name}.search.windows.net' diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-service-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-service-role-assignments.bicep new file mode 100644 index 00000000..2da75091 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai-service-role-assignments.bicep @@ -0,0 +1,91 @@ +/* +AI Services Role Assignments Module +-------------------------------- +This module configures RBAC permissions for AI Services access: + +1. Role Assignments: + - Cognitive Services Contributor: Full service management + - Cognitive Services OpenAI User: Access to OpenAI features + - Cognitive Services User: Basic service usage + +2. Unique Assignment Names: + - Uses subscription and resource group IDs + - Includes deployment-specific identifiers + - Prevents conflicts in multi-deployment scenarios +*/ + +@description('Name of the AI Services account') +param aiServicesName string + +@description('Principal ID of the managed identity') +param aiProjectPrincipalId string + +@description('Unique identifier for role assignments') +param aiProjectId string + +// Reference existing AI Services account +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = { + name: aiServicesName + scope: resourceGroup() +} + +/* -------------------------------------------- Role Definitions -------------------------------------------- */ + +// Cognitive Services Contributor Role +// Provides full access to manage AI services and their settings +resource cognitiveServicesContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' // Built-in role ID + scope: resourceGroup() +} + +// Cognitive Services OpenAI User Role +// Provides access to use OpenAI features +resource cognitiveServicesOpenAIUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Built-in role ID + scope: resourceGroup() +} + +// Cognitive Services User Role +// Provides basic access to use AI services +resource cognitiveServicesUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'a97b65f3-24c7-4388-baec-2e87135dc908' // Built-in role ID + scope: resourceGroup() +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +// Assign Cognitive Services Contributor role +resource cognitiveServicesContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01'= { + scope: aiServices + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, cognitiveServicesContributorRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesContributorRole.id + principalType: 'ServicePrincipal' + } +} + +// Assign Cognitive Services OpenAI User role +resource cognitiveServicesOpenAIUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, cognitiveServicesOpenAIUserRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesOpenAIUserRole.id + principalType: 'ServicePrincipal' + } +} + +// Assign Cognitive Services User role +resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, cognitiveServicesUserRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesUserRole.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai/ai-services-config.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai/ai-services-config.bicep new file mode 100644 index 00000000..8da8a0c7 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/ai/ai-services-config.bicep @@ -0,0 +1,141 @@ +/* +AI Services Configuration Module +----------------------------- +This module deploys AI Services with network security controls: + +1. Security Features: + - Network ACLs + - Private networking + - Managed identity + - Custom subdomain + +2. Model Deployment: + - Model configuration + - SKU settings + - Capacity management +*/ + +@description('Azure region for the deployment') +param location string + +@description('Tags to apply to resources') +param tags object = {} + +@description('The name of the AI Services account') +param aiServicesName string + +@description('ID of the subnet for network rules') +param subnetId string + +@description('Whether to enable public network access') +param enablePublicNetworkAccess bool = false + +// Model deployment parameters +@description('Model name for deployment') +param modelName string + +@description('Model format for deployment') +param modelFormat string + +@description('Model version for deployment') +param modelVersion string + +@description('Model deployment SKU name') +param modelSkuName string + +@description('Model deployment capacity') +param modelCapacity int + +@description('Log Analytics workspace ID for diagnostics') +param logAnalyticsWorkspaceId string = '' + +// AI Services account with network security controls +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' = { + name: aiServicesName + location: location + tags: tags + sku: { + name: 'S0' + } + kind: 'AIServices' + identity: { + type: 'SystemAssigned' + } + properties: { + customSubDomainName: toLower(aiServicesName) + apiProperties: { + statisticsEnabled: false + } + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + virtualNetworkRules: [ + { + id: subnetId + } + ] + } + publicNetworkAccess: enablePublicNetworkAccess ? 'Enabled' : 'Disabled' + } +} + +// Model deployment +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-06-01-preview' = { + parent: aiServices + name: modelName + sku: { + capacity: modelCapacity + name: modelSkuName + } + properties: { + model: { + name: modelName + format: modelFormat + version: modelVersion + } + } +} + +// Diagnostic settings +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceId)) { + name: '${aiServicesName}-diagnostics' + scope: aiServices + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + category: 'Audit' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + { + category: 'RequestResponse' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + } +} + +// Output variables +output aiServicesName string = aiServices.name +output aiServicesId string = aiServices.id +output aiServicesEndpoint string = aiServices.properties.endpoint +output modelDeploymentName string = modelDeployment.name +output principalId string = aiServices.identity.principalId diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/cognitive-services-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/cognitive-services-role-assignments.bicep new file mode 100644 index 00000000..88328fd1 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/cognitive-services-role-assignments.bicep @@ -0,0 +1,56 @@ +/* +Cognitive Services Role Assignments Module +--------------------------------------- +This module configures RBAC permissions for Azure AI Services: + +1. Role Configuration: + - Azure AI Administrator Role (b78c5d69-af96-48a3-bf8d-a8b4d589de94) + - Provides full access to manage AI resources and deployments + - Required for AI Studio operations + +2. Permissions Granted: + - Create and manage AI deployments + - Configure model settings + - Monitor resource usage + - Manage security settings + +3. Security Considerations: + - Uses managed identity for authentication + - Scoped to resource group level + - Follows principle of least privilege + +Documentation: +- Azure AI Administrator Role: https://learn.microsoft.com/en-us/azure/ai-studio/concepts/rbac-ai-studio#azure-ai-administrator-role +*/ + +@description('Principal ID of the managed identity') +param UAIPrincipalId string + +@description('Unique suffix for role assignment naming') +param suffix string + +/* -------------------------------------------- Role Definitions -------------------------------------------- */ + +// Azure AI Administrator Role +// Provides full access to manage AI resources and their settings +resource openAIAdmin 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'b78c5d69-af96-48a3-bf8d-a8b4d589de94' // Built-in role ID + scope: resourceGroup() +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +// Assign Azure AI Administrator role +resource openAIContributorContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, openAIAdmin.id, suffix) + properties: { + principalId: UAIPrincipalId // Managed identity principal ID + roleDefinitionId: openAIAdmin.id // AI Administrator role + principalType: 'ServicePrincipal' // Identity type + } +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +output roleAssignmentId string = openAIContributorContributorAssignment.id diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/parameters.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/parameters.bicep new file mode 100644 index 00000000..c7530a4b --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/parameters.bicep @@ -0,0 +1,156 @@ +/* +Common Parameters Module +---------------------- +This module defines common parameters and variables used across the infrastructure: + +1. Naming Standards: + - Resource prefixes + - Naming patterns + - Unique suffix generation + +2. Network Configuration: + - Address spaces + - Subnet ranges + - Service endpoints + +3. Tags: + - Environment tags + - Project tags + - Security tags +*/ + +// Resource naming +@description('Prefix for resource names') +param prefix string + +@description('Azure region for the deployment') +param location string = resourceGroup().location + +@description('Environment (e.g., prod, dev, test)') +@allowed([ + 'prod' + 'dev' + 'test' + 'stage' +]) +param environment string = 'prod' + +// Generate unique suffix +var uniqueSuffix = uniqueString(subscription().id, resourceGroup().id, prefix) + +// Common tags +var commonTags = { + Environment: environment + Project: 'NetworkSecuredAgent' + SecurityLevel: 'High' + DataClassification: 'Confidential' + CostCenter: 'AI-Services' + CreatedBy: 'IaC' + ManagedBy: 'DevOps' +} + +// Network configuration +var networkConfig = { + vnetAddressSpace: '172.16.0.0/16' + hubSubnetPrefix: '172.16.0.0/24' + agentsSubnetPrefix: '172.16.101.0/24' + serviceEndpoints: [ + { + service: 'Microsoft.KeyVault' + locations: [ + location + ] + } + { + service: 'Microsoft.Storage' + locations: [ + location + ] + } + { + service: 'Microsoft.CognitiveServices' + locations: [ + location + ] + } + ] + dnsZones: { + keyVault: 'privatelink.vaultcore.azure.net' + storage: 'privatelink.blob.core.windows.net' + aiServices: 'privatelink.cognitiveservices.azure.com' + aiSearch: 'privatelink.search.windows.net' + } +} + +// Resource naming patterns +var namingPatterns = { + vnet: '${prefix}-vnet-${uniqueSuffix}' + keyvault: 'kv-${prefix}-${uniqueSuffix}' + storage: '${prefix}store${uniqueSuffix}' + aiServices: '${prefix}-ai-${uniqueSuffix}' + aiSearch: '${prefix}-search-${uniqueSuffix}' + identity: '${prefix}-identity-${uniqueSuffix}' + logAnalytics: '${prefix}-logs-${uniqueSuffix}' + privateEndpoint: '${prefix}-pe-${uniqueSuffix}' +} + +// Security configuration +var securityConfig = { + enablePublicAccess: false + tlsVersion: 'TLS1_2' + allowSharedKeyAccess: false + enableSoftDelete: true + softDeleteRetentionDays: 7 + enablePurgeProtection: true + diagnosticRetentionDays: 30 + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + } +} + +// Monitoring configuration +var monitoringConfig = { + metrics: { + enabled: true + retentionDays: 30 + } + logs: { + enabled: true + retentionDays: 30 + categories: { + audit: true + requests: true + operations: true + } + } +} + +// SKU configuration +var skuConfig = { + aiServices: { + name: 'S0' + tier: 'Standard' + } + aiSearch: { + name: 'standard' + tier: 'Standard' + } + keyVault: { + name: 'standard' + family: 'A' + } + storage: { + name: 'Standard_ZRS' + tier: 'Standard' + } +} + +// Output variables +output suffix string = uniqueSuffix +output tags object = commonTags +output naming object = namingPatterns +output network object = networkConfig +output security object = securityConfig +output monitoring object = monitoringConfig +output skus object = skuConfig diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/wait-script.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/wait-script.bicep new file mode 100644 index 00000000..39bd9934 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/common/wait-script.bicep @@ -0,0 +1,21 @@ +param name string +param location string +param retentionTime string = 'PT1H' // Retention duration + +resource waitScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + name: name + location: location + kind: 'AzurePowerShell' + properties: { + azPowerShellVersion: '10.0' + scriptContent: ''' + Write-Output "Starting wait script..." + Start-Sleep -Seconds 120 + Write-Output "Wait completed. Proceeding with deployment..." + ''' + retentionInterval: retentionTime + cleanupPreference: 'Always' + } +} + +output scriptName string = waitScript.name diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-account-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-account-role-assignments.bicep new file mode 100644 index 00000000..eabb9312 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-account-role-assignments.bicep @@ -0,0 +1,29 @@ +// Assigns the necessary roles to the AI project + +@description('Name of the Cosmos DB account') +param cosmosDBName string + +@description('Principal ID of the AI project') +param aiProjectPrincipalId string + +param projectWorkspaceId string + +resource cosmosDBAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = { + name: cosmosDBName + scope: resourceGroup() +} + +resource cosmosDBOperatorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '230815da-be43-4aae-9cb4-875f7bd000aa' + scope: resourceGroup() +} + +resource cosmosDBOperatorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: cosmosDBAccount + name: guid(projectWorkspaceId, cosmosDBOperatorRole.id, cosmosDBAccount.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cosmosDBOperatorRole.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-container-role-assignment.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-container-role-assignment.bicep new file mode 100644 index 00000000..0f79900c --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/database/cosmos-container-role-assignment.bicep @@ -0,0 +1,72 @@ +// Assigns the necessary roles to the AI project + +@description('Name of the cosmos DB account') +param cosmosAccountName string + + +@description('Principal ID of the AI project') +param aiProjectPrincipalId string + +@description('Resource ID of the AI project') +param aiProjectId string + +param projectWorkspaceId string + +var userThreadName = '${projectWorkspaceId}-thread-message-store' +var systemThreadName = '${projectWorkspaceId}-system-thread-message-store' + + +#disable-next-line BCP081 +resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2025-01-01-preview' existing = { + name: cosmosAccountName + scope: resourceGroup() +} + +// Reference existing database +#disable-next-line BCP081 +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-01-01-preview' existing = { + parent: cosmosAccount + name: 'enterprise_memory' +} + +#disable-next-line BCP081 +resource containerUserMessageStore 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-01-01-preview' existing = { + parent: database + name: userThreadName +} + +#disable-next-line BCP081 +resource containerSystemMessageStore 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-01-01-preview' existing = { + parent: database + name: systemThreadName +} + + +var roleDefinitionId = resourceId( + 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', + cosmosAccountName, + '00000000-0000-0000-0000-000000000002' +) + +var scopeSystemContainer = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosAccountName}/dbs/enterprise_memory/colls/${systemThreadName}' +var scopeUserContainer = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosAccountName}/dbs/enterprise_memory/colls/${userThreadName}' + +resource containerRoleAssignmentUserContainer 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { + parent: cosmosAccount + name: guid(aiProjectId, containerUserMessageStore.id, roleDefinitionId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: roleDefinitionId + scope: scopeUserContainer + } +} + +resource containerRoleAssignmentSystemContainer 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { + parent: cosmosAccount + name: guid(aiProjectId, containerSystemMessageStore.id, roleDefinitionId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: roleDefinitionId + scope: scopeSystemContainer + } +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/keyvault-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/keyvault-role-assignments.bicep new file mode 100644 index 00000000..3c522dff --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/keyvault-role-assignments.bicep @@ -0,0 +1,96 @@ +/* +Key Vault Role Assignments Module +------------------------------ +This module configures RBAC permissions for Azure Key Vault: + +1. Role Configuration: + - Key Vault Contributor (f25e0fa2-a7c8-4377-a976-54943a77a395) + * Manage Key Vault resources + * Cannot access secret content + - Key Vault Secrets Officer (b86a8fe4-44ce-4948-aee5-eccb2c155cd7) + * Full access to secrets + * Manage secret metadata + +2. Permissions Granted: + Key Vault Contributor: + - Manage vault properties + - Create/delete vaults + - Set access policies + + Key Vault Secrets Officer: + - Create/delete secrets + - Set secret values + - Configure secret attributes + +3. Security Considerations: + - Uses managed identity for authentication + - Follows principle of least privilege + - Separate roles for management vs content access + - Scoped to specific Key Vault instance +*/ + +/* -------------------------------------------- Parameters -------------------------------------------- */ + +@description('Name of the Key Vault') +param keyvaultName string + +@description('Principal ID of the managed identity') +param UAIPrincipalId string + +@description('Unique suffix for role assignment naming') +param suffix string + +/* -------------------------------------------- Resources -------------------------------------------- */ + +// Reference to existing Key Vault +resource keyvault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyvaultName + scope: resourceGroup() +} + +/* -------------------------------------------- Role Definitions -------------------------------------------- */ + +// Key Vault Secrets Officer Role +// Provides full access to manage secrets +resource keyVaultSecretOfficer 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' // Built-in role ID + scope: keyvault +} + +// Key Vault Contributor Role +// Provides access to manage vault but not secrets +resource keyVaultContributor 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'f25e0fa2-a7c8-4377-a976-54943a77a395' // Built-in role ID + scope: resourceGroup() +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +// Assign Key Vault Contributor role +resource keyVaultContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: keyvault + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, keyVaultContributor.id, suffix) + properties: { + principalId: UAIPrincipalId // Managed identity principal ID + roleDefinitionId: keyVaultContributor.id // Contributor role + principalType: 'ServicePrincipal' // Identity type + } +} + +// Assign Key Vault Secrets Officer role +resource keyVaultSecretOfficerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: keyvault + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, keyVaultSecretOfficer.id, suffix) + properties: { + principalId: UAIPrincipalId // Managed identity principal ID + roleDefinitionId: keyVaultSecretOfficer.id // Secrets Officer role + principalType: 'ServicePrincipal' // Identity type + } +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +output contributorRoleId string = keyVaultContributorAssignment.id +output secretsOfficerRoleId string = keyVaultSecretOfficerAssignment.id diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-capability-host.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-capability-host.bicep new file mode 100644 index 00000000..f10f8faf --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-capability-host.bicep @@ -0,0 +1,61 @@ +@description('AI hub name') +param aiHubName string + +@description('AI project name') +param aiProjectName string + +@description('Name for ACS connection.') +param acsConnectionName string + +@description('Name for ACS connection.') +param aoaiConnectionName string + +@description('Name for capabilityHost.') +param capabilityHostName string + +@description('Name for customer subnet id') +param customerSubnetId string = '' + +@description('Name for CosmosDB connection.') +param cosmosConnectionName string + +var storageConnections = ['${aiProjectName}/workspaceblobstore'] +var aiSearchConnection = ['${acsConnectionName}'] +var aiServiceConnections = ['${aoaiConnectionName}'] +var cosmosDBConnections = ['${cosmosConnectionName}'] + +resource aiHub 'Microsoft.MachineLearningServices/workspaces@2024-10-01-preview' existing = { + name: aiHubName +} + +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2024-10-01-preview' existing = { + name: aiProjectName +} + +#disable-next-line BCP081 +resource hubCapabilityHost 'Microsoft.MachineLearningServices/workspaces/capabilityHosts@2024-10-01-preview' = { + name: '${aiHubName}-${capabilityHostName}' + parent: aiHub + properties: empty(customerSubnetId) ? { + capabilityHostKind: 'Agents' + }: { + capabilityHostKind: 'Agents' + customerSubnet: customerSubnetId + } +} + +#disable-next-line BCP081 +resource projectCapabilityHost 'Microsoft.MachineLearningServices/workspaces/capabilityHosts@2025-01-01-preview' = { + name: '${aiProjectName}-${capabilityHostName}' + parent: aiProject + properties: { + capabilityHostKind: 'Agents' + aiServicesConnections: aiServiceConnections + vectorStoreConnections: aiSearchConnection + storageConnections: storageConnections + threadStorageConnections: cosmosDBConnections + } + dependsOn: [ + hubCapabilityHost + ] +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-hub.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-hub.bicep new file mode 100644 index 00000000..10ed8b42 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-hub.bicep @@ -0,0 +1,184 @@ +/* +Network-Secured AI Hub Module +--------------------------- +This module deploys an Azure AI Hub with network-secured configuration: + +1. Hub Architecture: + - Central orchestration point for AI services + - Manages connections to dependent services + - Provides network-isolated capability hosting + +2. Network Security: + - VNet integration for capability hosts + - Private endpoints for service access + - No public network exposure + - AAD-based authentication + +3. Service Connections: + - AI Services: For model inference and cognitive capabilities + - AI Search: For vector storage and search + - Key Vault: For secure secret management + - Storage: For data persistence + +4. Security Features: + - User-assigned managed identity + - RBAC-based access control + - Private networking for all connections +*/ + +/* -------------------------------------------- Parameters -------------------------------------------- */ + +@description('Azure region of the deployment') +param location string + +@description('Tags to add to the resources') +param tags object + +@description('AI hub name') +param aiHubName string + +@description('AI hub display name') +param aiHubFriendlyName string = aiHubName + +@description('AI hub description') +param aiHubDescription string + +@description('Resource ID of the key vault for secure secret storage') +param keyVaultId string + +@description('Resource ID of the storage account for data persistence') +param storageAccountId string + +// AI Services Configuration +@description('Resource ID of the AI Services') +param aiServicesId string + +@description('Endpoint URL of the AI Services') +param aiServicesTarget string + +@description('Name of the AI Services resource') +param aiServicesName string + +@description('Resource Group containing AI Services') +param aiServiceAccountResourceGroupName string + +@description('Subscription ID containing AI Services') +param aiServiceAccountSubscriptionId string + +// AI Search Configuration +@description('Name of the AI Search service') +param aiSearchName string + +@description('Resource ID of the AI Search service') +param aiSearchId string + +@description('Resource Group containing AI Search') +param aiSearchServiceResourceGroupName string + +@description('Subscription ID containing AI Search') +param aiSearchServiceSubscriptionId string + +@description('Name of the user-assigned managed identity') +param uaiName string + + +@description('Flag indicating if the hub already exists') +param aiHubExists bool = false + + +@description('Specifies the public network access for the machine learning workspace.') +@allowed([ + 'Disabled' + 'Enabled' +]) +param publicNetworkAccess string = 'Enabled' + + +/* -------------------------------------------- Variables -------------------------------------------- */ + +// Connection names for service integration +var acsConnectionName = '${aiHubName}-connection-AISearch' +var aoaiConnection = '${aiHubName}-connection-AIServices_aoai' + +/* -------------------------------------------- Resources -------------------------------------------- */ + +// Reference to managed identity +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: uaiName +} +var userAssignedIdentities = json('{\'${userAssignedIdentity.id}\': {}}') + +// Reference to existing services +resource aiServices 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { + name: aiServicesName + scope: resourceGroup(aiServiceAccountSubscriptionId, aiServiceAccountResourceGroupName) +} + +resource searchService 'Microsoft.Search/searchServices@2023-11-01' existing = { + name: aiSearchName + scope: resourceGroup(aiSearchServiceSubscriptionId, aiSearchServiceResourceGroupName) +} + +// AI Hub Workspace +// Documentation: https://learn.microsoft.com/en-us/azure/templates/microsoft.machinelearningservices/workspaces +resource aiHub 'Microsoft.MachineLearningServices/workspaces@2024-10-01-preview' = if(!aiHubExists) { + name: aiHubName + location: location + kind: 'hub' + tags: tags + identity: { + type: 'UserAssigned' // Use managed identity for authentication + userAssignedIdentities: userAssignedIdentities + } + properties: { + // Organization metadata + friendlyName: aiHubFriendlyName + description: aiHubDescription + primaryUserAssignedIdentity: userAssignedIdentity.id + + publicNetworkAccess: publicNetworkAccess + // Core service connections + keyVault: keyVaultId // For secret management + storageAccount: storageAccountId // For data persistence + } + + // AI Services Connection + // Documentation: https://learn.microsoft.com/en-us/azure/templates/microsoft.machinelearningservices/workspaces/connections + resource aiServicesConnection 'connections@2024-07-01-preview' = { + name: '${aiHubName}-connection-AIServices' + properties: { + category: 'AIServices' + target: aiServicesTarget // Service endpoint + authType: 'AAD' // Use Azure AD auth + isSharedToAll: true // Available to all projects + metadata: { + ApiType: 'Azure' + ResourceId: aiServicesId + location: aiServices.location + } + } + } + + // AI Search Connection + resource hub_connection_azureai_search 'connections@2024-07-01-preview' = { + name: acsConnectionName + properties: { + category: 'CognitiveSearch' + target: 'https://${aiSearchName}.search.windows.net' + authType: 'AAD' // Use Azure AD auth + isSharedToAll: true // Available to all projects + metadata: { + ApiType: 'Azure' + ResourceId: aiSearchId + location: searchService.location + } + } + } +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +output aiHubID string = aiHub.id +output aiHubName string = aiHub.name +output aoaiConnectionName string = aoaiConnection +output acsConnectionName string = acsConnectionName diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-project.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-project.bicep new file mode 100644 index 00000000..81c069c5 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-ai-project.bicep @@ -0,0 +1,133 @@ +/* +Network-Secured AI Project Module +------------------------------- +This module deploys an Azure AI Project workspace with network-secured configuration: + +1. Project Configuration: + - Creates an AI Project workspace with managed identity + - Configures capability host for Agents functionality + - Sets up secure connections to dependent services + +2. Security Features: + - Uses user-assigned managed identity for authentication + - Integrates with network-secured AI Hub + - Configures private connections to storage and services + +3. Service Connections: + - Storage: For data persistence + - AI Search: For vector storage and search capabilities + - AI Services: For model inference and cognitive capabilities + +4. Network Security: + - All connections use private endpoints + - No public internet exposure + - Secure service-to-service communication +*/ + +/* -------------------------------------------- Parameters -------------------------------------------- */ + +@description('Azure region of the deployment') +param location string + +@description('Tags to add to the resources') +param tags object + +@description('AI Project name') +param aiProjectName string + +@description('AI Project display name') +param aiProjectFriendlyName string = aiProjectName + +@description('AI Project description') +param aiProjectDescription string + +@description('Resource ID of the AI Hub resource') +param aiHubId string + + +@description('Name of the user-assigned managed identity') +param uaiName string + +@description('Specifies the public network access for the machine learning workspace.') +param publicNetworkAccess string = 'Disabled' + +@description('The name of the CosmosDB account') +param cosmosDBName string + +@description('Subscription ID of the Cosmos DB resource') +param cosmosDBSubscriptionId string + +@description('Resource Group name of the Cosmos DB resource') +param cosmosDBResourceGroupName string + +/* -------------------------------------------- Variables -------------------------------------------- */ + +// Connection string components +var subscriptionId = subscription().subscriptionId +var resourceGroupName = resourceGroup().name +var projectConnectionString = '${location}.api.azureml.ms;${subscriptionId};${resourceGroupName};${aiProjectName}' +var cosmosConnectionName = '${aiProjectName}-connection-CosmosDBAccount' +/* -------------------------------------------- Resources -------------------------------------------- */ + +// Reference to user-assigned managed identity +resource uai 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = { + name: uaiName + scope: resourceGroup() +} + +resource cosmosDBAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = { + name: cosmosDBName + scope: resourceGroup(cosmosDBSubscriptionId,cosmosDBResourceGroupName) +} + +// AI Project Workspace +// Documentation: https://learn.microsoft.com/en-us/azure/templates/microsoft.machinelearningservices/workspaces +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' = { + name: aiProjectName + location: location + tags: union(tags, { + ProjectConnectionString: projectConnectionString // Store connection string in tags for easy access + }) + identity: { + type: 'UserAssigned' // Use managed identity for authentication + userAssignedIdentities: { + '${uai.id}': {} + } + } + properties: { + // Organization metadata + friendlyName: aiProjectFriendlyName + description: aiProjectDescription + primaryUserAssignedIdentity: uai.id + + // Hub integration + hubResourceId: aiHubId // Link to network-secured AI Hub + publicNetworkAccess: publicNetworkAccess // Enable/disable public network access + } + kind: 'project' +} + +/*---------------------Cosmos DB Connection----------------------------*/ +resource project_connection_cosmosdb 'Microsoft.MachineLearningServices/workspaces/connections@2025-01-01-preview' = { + name: cosmosConnectionName + parent: aiProject + properties: { + category: 'CosmosDB' + target: 'https://${cosmosDBName}.documents.azure.com:443/' + authType: 'AAD' + metadata: { + ApiType: 'Azure' + ResourceId: cosmosDBAccount.id + location: cosmosDBAccount.location + } + } +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +// Project identifiers +output aiProjectName string = aiProject.name +output aiProjectResourceId string = aiProject.id +output aiProjectWorkspaceId string = aiProject.properties.workspaceId +output projectConnectionString string = aiProject.tags.ProjectConnectionString +output cosmosConnectionName string = project_connection_cosmosdb.name 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 new file mode 100644 index 00000000..f8f66e4a --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-dependent-resources.bicep @@ -0,0 +1,324 @@ +/* +Network-Secured Dependencies Module +--------------------------------- +This module deploys core infrastructure components with network security controls: + +1. Virtual Network Architecture: + - Address space: 172.16.0.0/16 + - Customer Hub subnet: 172.16.0.0/24 (for private endpoints) + - Agents subnet: 172.16.101.0/24 (for container apps) + +2. Network Security Features: + - Service endpoints for secure Azure service access + - Network ACLs to restrict access + - Private endpoints for secure communication + - Disabled public network access + +3. Subnet Configuration: + - Customer Hub subnet: Hosts private endpoints and service endpoints + - Agents subnet: Delegated to container apps with specific CIDR range +*/ + +param storageExists bool = false +param keyvaultExists bool = false +param aiServicesExists bool = false +param aiSearchExists bool = false +param cosmosDBExists bool = false + +@description('Azure region of the deployment') +param location string = resourceGroup().location + +@description('Tags to add to the resources') +param tags object = {} + +@description('Unique suffix for resource names') +param suffix string + +@description('AI services name') +param aiServicesName string + +@description('The name of the Key Vault') +param keyvaultName string + +@description('The name of the AI Search resource') +param aiSearchName string + +@description('Name of the storage account') +param storageName string + +@description('Model name for deployment') +param modelName string + +@description('Model format for deployment') +param modelFormat string + +@description('Model version for deployment') +param modelVersion string + +@description('Model deployment SKU name') +param modelSkuName string + +@description('Model deployment capacity') +param modelCapacity int + +@description('Model/AI Resource deployment location') +param modelLocation string + +@description('The Kind of AI Service, can be "OpenAI" or "AIService"') +param aisKind string + +@description('User-assigned managed identity name') +param userAssignedIdentityName string + +@description('Name of Cosmos DB account') +param cosmosDBName string + +@description('Cosmos DB Subscription') +param cosmosDBSubscription string + +@description('Cosmos DB Resource Group') +param cosmosDBResourceGroup string + +// Subnet reference variables for network rules +// var cxSubnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, cxSubnetName) +// var agentSubnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, agentsSubnetName) + +// User-assigned managed identity for secure access +resource uai 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + location: location + name: userAssignedIdentityName +} + +/* -------------------------------------------- Existing Resource References -------------------------------------------- */ + +resource existingStorage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = if(storageExists) { + name: storageName + scope: resourceGroup() +} + +resource existingKeyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if(keyvaultExists) { + name: keyvaultName + scope: resourceGroup() +} + +resource existingAiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = if(aiServicesExists) { + name: aiServicesName + scope: resourceGroup() +} + +resource existingAiSearch 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = if(aiSearchExists) { + name: aiSearchName + scope: resourceGroup() +} + +// Check if Cosmos DB exists +resource existingCosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = if (cosmosDBExists) { + name: cosmosDBName + scope: resourceGroup(cosmosDBSubscription, cosmosDBResourceGroup) +} + +/* -------------------------------------------- Network-Secured Resources -------------------------------------------- */ + +// Cosmos DB with network security controls + + +resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = if(!cosmosDBExists) { + name: cosmosDBName + location: location + tags: tags + kind: 'GlobalDocumentDB' + properties: { + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + publicNetworkAccess: 'Disabled' // Block public access + disableLocalAuth: true + enableAutomaticFailover: false + enableMultipleWriteLocations: false + enableFreeTier: false + locations: [ + { + locationName: location + failoverPriority: 0 + isZoneRedundant: false + } + ] + databaseAccountOfferType: 'Standard' + } +} + + +// Key Vault with network security controls +resource defaultKeyVault 'Microsoft.KeyVault/vaults@2022-07-01' = if(!keyvaultExists) { + name: keyvaultName + location: location + tags: tags + properties: { + createMode: 'default' + enabledForDeployment: true + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + enableSoftDelete: true + enableRbacAuthorization: true + enablePurgeProtection: true + publicNetworkAccess: 'Disabled' // Block public access + accessPolicies: [ + { + tenantId: subscription().tenantId + objectId: uai.properties.principalId + permissions: { secrets: [ 'set', 'get', 'list', 'delete', 'purge' ] } + } + ] + networkAcls: { + bypass: 'AzureServices' // Allow trusted Azure services + defaultAction: 'Deny' // Deny all other traffic + } + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} + +// AI Services with network security controls +resource defaultAiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' = if(!aiServicesExists) { + name: aiServicesName + location: modelLocation + sku: { + name: 'S0' + } + kind: aisKind + identity: { + type: 'SystemAssigned' + } + properties: { + customSubDomainName: toLower('${(aiServicesName)}') + networkAcls: { + bypass: 'AzureServices' // Allow trusted Azure services + defaultAction: 'Deny' // Deny all other traffic + } + publicNetworkAccess: 'Disabled' // Block public access + } +} + +// AI Model deployment +resource defaultModelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-06-01-preview'= if(!aiServicesExists){ + parent: defaultAiServices + name: modelName + sku : { + capacity: modelCapacity + name: modelSkuName + } + properties: { + model:{ + name: modelName + format: modelFormat + version: modelVersion + } + } +} + +// AI Search with network security controls +resource defaultAiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = if(!aiSearchExists) { + name: aiSearchName + location: location + tags: tags + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${uai.id}': {} + } + } + properties: { + disableLocalAuth: false + authOptions: { aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge' }} + encryptionWithCmk: { + enforcement: 'Unspecified' + } + hostingMode: 'default' + partitionCount: 1 + publicNetworkAccess: 'disabled' // Block public access, use lowercase + replicaCount: 1 + semanticSearch: 'disabled' // use lowercase + networkRuleSet: { + bypass: 'None' + ipRules: [] + } + } + sku: { + name: 'standard' + } +} + +// Storage Account with network security controls +param noZRSRegions array = ['southindia', 'westus'] +param sku object = contains(noZRSRegions, location) ? { name: 'Standard_GRS' } : { name: 'Standard_ZRS' } +var storageNameCleaned = storageExists ? existingStorage.name : replace(storageName, '-', '') + +resource defaultStorage 'Microsoft.Storage/storageAccounts@2022-05-01' = if(!storageExists){ + name: storageNameCleaned + location: location + kind: 'StorageV2' + sku: sku + properties: { + minimumTlsVersion: 'TLS1_2' // Enforce TLS 1.2 + allowBlobPublicAccess: false // Prevent public blob access + publicNetworkAccess: 'Disabled' // Block public access + networkAcls: { + bypass: 'AzureServices' // Allow trusted Azure services + defaultAction: 'Deny' // Deny all other traffic + } + allowSharedKeyAccess: false // Enforce Azure AD authentication + } +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +module storageAccessAssignment './storage-role-assignments.bicep' = if(!storageExists){ + name: 'dependencies-${suffix}-storage-rbac' + params: { + suffix: suffix + storageName: storageNameCleaned + UAIPrincipalId: uai.properties.principalId + } + dependsOn: [ defaultStorage ] +} + +module keyVaultAccessAssignment './keyvault-role-assignments.bicep' = if(!keyvaultExists){ + name: 'dependencies-${suffix}-keyvault-rbac' + params: { + suffix: suffix + keyvaultName: keyvaultName + UAIPrincipalId: uai.properties.principalId + } + dependsOn: [ defaultKeyVault ] +} + +/* -------------------------------------------- Output Variables -------------------------------------------- */ + +var aiServiceParts = aiServicesExists ? split(existingAiServices.id, '/') : split(defaultAiServices.id, '/') +var acsParts = aiSearchExists ? split(existingAiSearch.id, '/') : split(defaultAiSearch.id, '/') +var storageParts = storageExists ? split(existingStorage.id, '/') : split(defaultStorage.id, '/') + +output aiServicesName string = aiServicesExists ? existingAiServices.name : defaultAiServices.name +output aiservicesID string = aiServicesExists ? existingAiServices.id : defaultAiServices.id +output aiservicesTarget string = aiServicesExists ? existingAiServices.properties.endpoint : defaultAiServices.properties.endpoint +output aiServiceAccountResourceGroupName string = aiServiceParts[4] +output aiServiceAccountSubscriptionId string = aiServiceParts[2] + +output aiSearchName string = aiSearchExists ? existingAiSearch.name : defaultAiSearch.name +output aisearchID string = aiSearchExists ? existingAiSearch.id : defaultAiSearch.id +output aiSearchServiceResourceGroupName string = acsParts[4] +output aiSearchServiceSubscriptionId string = acsParts[2] + +output storageAccountName string = storageExists ? existingStorage.name : storageName +output storageId string = storageExists ? existingStorage.id : defaultStorage.id +output storageAccountResourceGroupName string = storageParts[4] +output storageAccountSubscriptionId string = storageParts[2] + +output keyvaultId string = keyvaultExists ? existingKeyVault.id : defaultKeyVault.id + +output cosmosDBName string = cosmosDBExists ? existingCosmosDB.name : cosmosDB.name +output cosmosDBId string = cosmosDBExists ? existingCosmosDB.id : cosmosDB.id diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-identity.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-identity.bicep new file mode 100644 index 00000000..991204cb --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/network-secured-identity.bicep @@ -0,0 +1,57 @@ +/* +Network-Secured Identity Module +----------------------------- +This module manages the User-Assigned Managed Identity (UAI) for the network-secured deployment: + +1. Purpose: + - Provides a managed identity for secure service-to-service authentication + - Enables zero-trust security model with no credential storage + - Used by AI services to access other Azure resources + +2. Features: + - Supports new identity creation or existing identity reference + - Provides identity properties through outputs + - Enables RBAC-based access control + +3. Security Benefits: + - No password/secret management required + - Platform-managed credential rotation + - Granular access control through RBAC +*/ + +@description('Azure region for resource deployment') +param location string + +@description('Name of the user-assigned managed identity') +param userAssignedIdentityName string + +@description('Flag indicating if the identity already exists') +param uaiExists bool = false + +/* -------------------------------------------- Identity Resources -------------------------------------------- */ + +// Reference existing identity if specified +resource existingUAI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = if(uaiExists) { + name: userAssignedIdentityName + scope: resourceGroup() +} + +// Create new identity if it doesn't exist +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + location: location + name: userAssignedIdentityName +} + +/* -------------------------------------------- Outputs -------------------------------------------- */ + +// Identity name for reference by other resources +output uaiName string = uaiExists ? existingUAI.name : userAssignedIdentity.name + +// Full resource ID for RBAC assignments +output uaiId string = uaiExists ? existingUAI.id : userAssignedIdentity.id + +// Principal ID for role assignments +output uaiPrincipalId string = uaiExists ? existingUAI.properties.principalId : userAssignedIdentity.properties.principalId + +// Client ID for service authentication +output uaiClientId string = uaiExists ? existingUAI.properties.clientId : userAssignedIdentity.properties.clientId diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/networking/vnet.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/networking/vnet.bicep new file mode 100644 index 00000000..0f817f23 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/networking/vnet.bicep @@ -0,0 +1,80 @@ +/* +Virtual Network Module +--------------------- +This module deploys the core network infrastructure with security controls: + +1. Address Space: + - VNet CIDR: 172.16.0.0/16 + - Hub Subnet: 172.16.0.0/24 (private endpoints) + - Agents Subnet: 172.16.101.0/24 (container apps) + +2. Security Features: + - Service endpoints + - Network isolation + - Subnet delegation +*/ + +@description('Azure region for the deployment') +param location string + +@description('Tags to apply to resources') +param tags object = {} + +@description('Unique suffix for resource names') +param suffix string + +@description('The name of the virtual network') +param vnetName string = 'agents-vnet-${suffix}' + +@description('The name of Agents Subnet') +param agentsSubnetName string = 'agents-subnet-${suffix}' + +@description('The name of Hub subnet') +param hubSubnetName string = 'hub-subnet-${suffix}' + +@description('Model/AI Resource deployment location') +param modelLocation string + +// Virtual Network with segregated subnets +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { + name: vnetName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + '172.16.0.0/16' + ] + } + subnets: [ + { + name: hubSubnetName + properties: { + addressPrefix: '172.16.0.0/24' + } + } + { + name: agentsSubnetName + properties: { + addressPrefix: '172.16.101.0/24' + delegations: [ + { + name: 'Microsoft.app/environments' + properties: { + serviceName: 'Microsoft.App/environments' + } + } + ] + } + } + ] + } +} + +// Output variables +output virtualNetworkName string = virtualNetwork.name +output virtualNetworkId string = virtualNetwork.id +output hubSubnetName string = hubSubnetName +output agentsSubnetName string = agentsSubnetName +output hubSubnetId string = '${virtualNetwork.id}/subnets/${hubSubnetName}' +output agentsSubnetId string = '${virtualNetwork.id}/subnets/${agentsSubnetName}' 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 new file mode 100644 index 00000000..6953a286 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/private-endpoint-and-dns.bicep @@ -0,0 +1,478 @@ +/* +Private Endpoint and DNS Configuration Module +------------------------------------------ +This module configures private network access for Azure services using: + +1. Private Endpoints: + - Creates network interfaces in the specified subnet + - Establishes private connections to Azure services + - Enables secure access without public internet exposure + +2. Private DNS Zones: + - privatelink.azureml.ms for AI Services + - privatelink.search.windows.net for AI Search + - privatelink.blob.core.windows.net for Storage + - privatelink.documents.azure.com + - Enables custom DNS resolution for private endpoints + +3. DNS Zone Links: + - Links private DNS zones to the VNet + - Enables name resolution for resources in the VNet + - Prevents DNS resolution conflicts + +Security Benefits: +- Eliminates public internet exposure +- Enables secure access from within VNet +- Prevents data exfiltration through network +*/ + +// Resource names and identifiers +@description('Name of the AI Services account') +param aiServicesName string +@description('Name of the AI Search service') +param aiSearchName string +@description('Name of the storage account') +param storageName string + +@description('Name of the Customer Cosmos DB') +param cosmosDBName string + +@description('Name of the Customer Cosmos DB Subscription') +param cosmosDBSubscription string + +@description('Name of the Customer Cosmos DB Resource Group') +param cosmosDBResourceGroup string + +param vnetName string +@description('Name of the Customer subnet') +param cxSubnetName string +@description('Suffix for unique resource names') +param suffix string +@description('Name of the AI Storage Account') +param aiStorageId string + +@description('Specifies the resource id of the Azure Hub Workspace.') +param hubWorkspaceId string + +@description('Name of the Customer Hub Workspace') +@secure() +param hubWorkspaceName string + +// Reference existing services that need private endpoints +resource aiServices 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { + name: aiServicesName + scope: resourceGroup() +} + +resource aiSearch 'Microsoft.Search/searchServices@2023-11-01' existing = { + name: aiSearchName + scope: resourceGroup() +} + +// Check if Cosmos DB exists +resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = { + name: cosmosDBName + scope: resourceGroup(cosmosDBSubscription, cosmosDBResourceGroup) +} + +// Reference existing network resources +resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: vnetName + scope: resourceGroup() +} + +resource cxSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = { + parent: vnet + name: cxSubnetName +} + +/* -------------------------------------------- AI Services Private Endpoint -------------------------------------------- */ + +// Private endpoint for AI Services +// - Creates network interface in customer hub subnet +// - Establishes private connection to AI Services account +resource aiServicesPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: '${aiServicesName}-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${aiServicesName}-private-link-service-connection' + properties: { + privateLinkServiceId: aiServices.id + groupIds: [ + 'account' // Target AI Services account + ] + } + } + ] + } +} + +resource aiServiceOpenAiPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: '${aiServicesName}-openAi-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${aiServicesName}-openAi-private-link-service-connection' + properties: { + privateLinkServiceId: aiServices.id + groupIds: [ + 'account' // Target AI Services account + ] + } + } + ] + } +} + +/* -------------------------------------------- AI Search Private Endpoint -------------------------------------------- */ + +// Private endpoint for AI Search +// - Creates network interface in customer hub subnet +// - Establishes private connection to AI Search service +resource aiSearchPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: '${aiSearchName}-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${aiSearchName}-private-link-service-connection' + properties: { + privateLinkServiceId: aiSearch.id + groupIds: [ + 'searchService' // Target search service + ] + } + } + ] + } +} + +/* -------------------------------------------- Storage 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' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${storageName}-private-link-service-connection' + properties: { + privateLinkServiceId: aiStorageId + groupIds: [ + 'blob' // Target blob storage + ] + } + } + ] + } +} + +/*------------------------------------------Cosmos DB Private Endpoint-------------------------------*/ +resource cosmosDBPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: '${cosmosDBName}-private-endpoint' + location: resourceGroup().location + properties: { + subnet: { + id: cxSubnet.id // Deploy in customer hub subnet + } + privateLinkServiceConnections: [ + { + name: '${cosmosDBName}-private-link-service-connection' + properties: { + privateLinkServiceId: cosmosDB.id + groupIds: [ + 'Sql' + ] + } + } + ] + } +} + +/*----------------------------------------------Hub Workspace Kind---------------------------------------------*/ +resource hubWorkspacePrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = { + name: '${hubWorkspaceName}-private-endpoint' + location: resourceGroup().location + properties: { + privateLinkServiceConnections: [ + { + name: '${hubWorkspaceName}-private-link-service-connection' + properties: { + privateLinkServiceId: hubWorkspaceId + groupIds: [ + 'amlworkspace' + ] + } + } + ] + subnet: { + id: cxSubnet.id + } + } +} + +/* -------------------------------------------- Private DNS Zones -------------------------------------------- */ + +// Private DNS Zone for AI Services +// - Enables custom DNS resolution for AI Services private endpoint +resource aiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.azureml.ms' // Standard DNS zone for AI Services + location: 'global' +} + +resource openAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.openai.azure.com' + location: 'global' +} + +// Link AI Services DNS Zone to VNet +resource aiServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: aiServicesPrivateDnsZone + location: 'global' + name: 'aiServices-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} + +resource aiOpenAILink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: openAiPrivateDnsZone + location: 'global' + name: 'aiServicesOpenAI-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} + +// DNS Zone Group for AI Services +resource aiServicesDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: aiServicesPrivateEndpoint + name: '${aiServicesName}-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${aiServicesName}-dns-config' + properties: { + privateDnsZoneId: aiServicesPrivateDnsZone.id + } + } + ] + } + dependsOn: [ + aiServicesLink + ] +} + +// DNS Zone Group for Azure OpenAI +resource aiOpenAIDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: aiServiceOpenAiPrivateEndpoint + name: '${aiServicesName}-openAi-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${aiServicesName}-openAi-dns-config' + properties: { + privateDnsZoneId: openAiPrivateDnsZone.id + } + } + ] + } + dependsOn: [ + aiOpenAILink + ] +} + +// Private DNS Zone for AI Hub +// - Enables custom DNS resolution for AI Hub private endpoint +resource mlApiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.api.azureml.ms' + location: 'global' +} + +resource mlNotebooksPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.notebooksazureml.net' + location: 'global' +} + +// Link AI Hub DNS Zone to VNet +resource mlApiPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: mlApiPrivateDnsZone + name: 'mlApi-${suffix}-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +resource mlNotebooksPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: mlNotebooksPrivateDnsZone + name: 'mlNotebook-${suffix}-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +resource hubWorkspacePrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = { + parent: hubWorkspacePrivateEndpoint + name: '${hubWorkspaceName}-mlApiNotebook-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${hubWorkspaceName}-mlApi-dns-config' + properties: { + privateDnsZoneId: mlApiPrivateDnsZone.id + } + } + { + name: '${hubWorkspaceName}-mlNotebook-dns-config' + properties: { + privateDnsZoneId: mlNotebooksPrivateDnsZone.id + } + } + ] + } + dependsOn: [ + mlApiPrivateDnsZoneVirtualNetworkLink + mlNotebooksPrivateDnsZoneVirtualNetworkLink + ] +} + +// Private DNS Zone for AI Search +// - Enables custom DNS resolution for AI Search private endpoint +resource aiSearchPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.search.windows.net' // Standard DNS zone for AI Search + location: 'global' +} + +// Link AI Search DNS Zone to VNet +resource aiSearchLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: aiSearchPrivateDnsZone + location: 'global' + name: 'aiSearch-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} + +// DNS Zone Group for AI Search +resource aiSearchDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: aiSearchPrivateEndpoint + name: '${aiSearchName}-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${aiSearchName}-dns-config' + properties: { + privateDnsZoneId: aiSearchPrivateDnsZone.id + } + } + ] + } +} + +// Private DNS Zone for Storage +// - Enables custom DNS resolution for blob storage private endpoint +resource storagePrivateDnsZone '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 + location: 'global' + name: 'storage-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} + +// DNS Zone Group for Storage +resource storageDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: storagePrivateEndpoint + name: '${storageName}-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${storageName}-dns-config' + properties: { + privateDnsZoneId: storagePrivateDnsZone.id + } + } + ] + } +} + +// Private DNS Zone for Cosmos DB +resource cosmosDBprivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.documents.azure.com' + location: 'global' +} + +// Link Cosmos DB DNS Zone to VNet +resource cosmosDBLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + parent: cosmosDBprivateDnsZone + location: 'global' + name: 'cosmosDB-${suffix}-link' + properties: { + virtualNetwork: { + id: vnet.id // Link to specified VNet + } + registrationEnabled: false // Don't auto-register VNet resources + } +} +// DNS Zone Group for Cosmos DB +resource cosmosDBDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: cosmosDBPrivateEndpoint + name: '${cosmosDBName}-dns-group' + properties: { + privateDnsZoneConfigs: [ + { + name: '${cosmosDBName}-dns-config' + properties: { + privateDnsZoneId: cosmosDBprivateDnsZone.id + } + } + ] + } + dependsOn: [ + cosmosDBLink + ] +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/security/keyvault-config.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/security/keyvault-config.bicep new file mode 100644 index 00000000..4b701bcc --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/security/keyvault-config.bicep @@ -0,0 +1,122 @@ +/* +Key Vault Configuration Module +---------------------------- +This module deploys a Key Vault with network security controls: + +1. Security Features: + - RBAC authorization + - Network ACLs + - Private networking + - Soft delete enabled + +2. Access Controls: + - Azure AD authentication + - VNet integration + - Service endpoint access +*/ + +@description('Azure region for the deployment') +param location string + +@description('Tags to apply to resources') +param tags object = {} + +@description('The name of the Key Vault') +param keyvaultName string + +@description('Principal ID of the managed identity') +param principalId string + +@description('ID of the subnet for network rules') +param subnetId string + +@description('Whether to enable public network access') +param enablePublicNetworkAccess bool = false + +// Key Vault with network security controls +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyvaultName + location: location + tags: tags + properties: { + createMode: 'default' + enabledForDeployment: true + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + enableSoftDelete: true + enableRbacAuthorization: true + enablePurgeProtection: true + publicNetworkAccess: enablePublicNetworkAccess ? 'Enabled' : 'Disabled' + accessPolicies: [ + { + tenantId: subscription().tenantId + objectId: principalId + permissions: { + secrets: [ + 'set' + 'get' + 'list' + 'delete' + 'purge' + ] + } + } + ] + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + virtualNetworkRules: [ + { + id: subnetId + } + ] + } + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} + +// Diagnostic settings for Key Vault +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: '${keyvaultName}-diagnostics' + scope: keyVault + properties: { + logs: [ + { + category: 'AuditEvent' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + { + category: 'AzurePolicyEvaluationDetails' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + } +} + +// Output variables +output keyVaultName string = keyVault.name +output keyVaultId string = keyVault.id +output keyVaultUri string = keyVault.properties.vaultUri diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage-role-assignments.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage-role-assignments.bicep new file mode 100644 index 00000000..3b3ae0c9 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage-role-assignments.bicep @@ -0,0 +1,71 @@ +/* +Storage Role Assignments Module +----------------------------- +This module configures RBAC permissions for Storage Account access: + +1. Role Assignments: + - Storage Blob Data Owner: Full access to blob containers and data + - Storage Queue Data Contributor: Read/write access to queues + +2. Unique Assignment Names: + - Uses subscription and resource group IDs + - Includes deployment-specific suffix + - Prevents conflicts in multi-deployment scenarios +*/ + +@description('Name of the storage account') +param storageName string + +@description('Principal ID of the managed identity') +param UAIPrincipalId string + +@description('Unique suffix for resource naming') +param suffix string = '' + +// Reference existing storage account +resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { + name: storageName + scope: resourceGroup() +} + +/* -------------------------------------------- Role Definitions -------------------------------------------- */ + +// Storage Blob Data Owner Role +// Provides full access to Azure Storage blob containers and data +resource storageBlobDataOwner 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Built-in role ID + scope: resourceGroup() +} + +// Storage Queue Data Contributor Role +// Provides read/write access to Azure Storage queues and messages +resource storageQueueDataContributor 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // Built-in role ID + scope: resourceGroup() +} + +/* -------------------------------------------- Role Assignments -------------------------------------------- */ + +// Assign Storage Blob Data Owner role +resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storage + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, storageBlobDataOwner.id, suffix) + properties: { + principalId: UAIPrincipalId + roleDefinitionId: storageBlobDataOwner.id + principalType: 'ServicePrincipal' + } +} + +// Assign Storage Queue Data Contributor role +resource storageQueueDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storage + // Use subscription ID and resource group ID to ensure uniqueness across deployments + name: guid(subscription().subscriptionId, resourceGroup().id, storageQueueDataContributor.id, suffix) + properties: { + principalId: UAIPrincipalId + roleDefinitionId: storageQueueDataContributor.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage/storage-config.bicep b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage/storage-config.bicep new file mode 100644 index 00000000..42ed0b17 --- /dev/null +++ b/scenarios/Agents/setup/network-secured-agent-thread-storage/modules-network-secured/storage/storage-config.bicep @@ -0,0 +1,167 @@ +/* +Storage Configuration Module +-------------------------- +This module deploys a storage account with network security controls: + +1. Security Features: + - Network ACLs + - Private networking + - TLS enforcement + - Azure AD authentication + +2. Storage Configuration: + - ZRS/GRS replication + - Blob service settings + - Diagnostic settings +*/ + +@description('Azure region for the deployment') +param location string + +@description('Tags to apply to resources') +param tags object = {} + +@description('The name of the storage account') +param storageName string + +@description('ID of the subnet for network rules') +param subnetId string + +@description('Whether to enable public network access') +param enablePublicNetworkAccess bool = false + +@description('Log Analytics workspace ID for diagnostics') +param logAnalyticsWorkspaceId string = '' + +// Regions without ZRS support +param noZRSRegions array = [ + 'southindia' + 'westus' +] + +// Determine SKU based on location +var sku = contains(noZRSRegions, location) ? { name: 'Standard_GRS' } : { name: 'Standard_ZRS' } + +// Storage Account with network security controls +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = { + name: replace(storageName, '-', '') // Remove hyphens for storage naming rules + location: location + tags: tags + kind: 'StorageV2' + sku: sku + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + publicNetworkAccess: enablePublicNetworkAccess ? 'Enabled' : 'Disabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + virtualNetworkRules: [ + { + id: subnetId + } + ] + } + allowSharedKeyAccess: false + supportsHttpsTrafficOnly: true + encryption: { + services: { + blob: { + enabled: true + } + file: { + enabled: true + } + queue: { + enabled: true + } + table: { + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + } +} + +// Blob service configuration +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2022-05-01' = { + parent: storageAccount + name: 'default' + properties: { + deleteRetentionPolicy: { + enabled: true + days: 7 + } + containerDeleteRetentionPolicy: { + enabled: true + days: 7 + } + isVersioningEnabled: true + changeFeed: { + enabled: true + retentionInDays: 7 + } + } +} + +// Diagnostic settings +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceId)) { + name: '${storageAccount.name}-diagnostics' + scope: storageAccount + properties: { + workspaceId: logAnalyticsWorkspaceId + metrics: [ + { + category: 'Transaction' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + { + category: 'Capacity' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + logs: [ + { + category: 'StorageRead' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + { + category: 'StorageWrite' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + { + category: 'StorageDelete' + enabled: true + retentionPolicy: { + days: 30 + enabled: true + } + } + ] + } +} + +// Output variables +output storageAccountName string = storageAccount.name +output storageAccountId string = storageAccount.id +output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob +output queueEndpoint string = storageAccount.properties.primaryEndpoints.queue +output tableEndpoint string = storageAccount.properties.primaryEndpoints.table +output fileEndpoint string = storageAccount.properties.primaryEndpoints.file diff --git a/scenarios/Agents/setup/virtual-machine/README.md b/scenarios/Agents/setup/virtual-machine/README.md new file mode 100644 index 00000000..34e0aacb --- /dev/null +++ b/scenarios/Agents/setup/virtual-machine/README.md @@ -0,0 +1,105 @@ +# Virtual Machine Setup for Azure AI Agents + +This directory contains tools and scripts to configure virtual machines for running Azure AI Agents scenarios. + +## Overview + +The resources in this directory help you set up and configure virtual machines optimized for Azure AI agent development, testing, and deployment. These scripts automate the provisioning and configuration process to ensure a consistent environment for AI agent operations. + +## Prerequisites + +- Azure subscription with contributor access +- Azure CLI installed and configured +- Bash shell environment (Linux, macOS, or WSL on Windows) +- Sufficient quota for the VM sizes you intend to deploy + +## Getting Started + +### Setup + +1. Clone this repository: + ```bash + git clone https://github.com/Azure/azureai-samples.git + cd azureai-samples/scenarios/Agents/setup/virtual-machine + ``` + +2. Run the setup script: + ```bash + ./setup.sh + ``` + +### Configuration Options + +The setup can be customized by modifying the following parameters in the script or providing them as environment variables: + +- `VM_SIZE`: Size of the virtual machine (default: Standard_D4s_v3) +- `LOCATION`: Azure region for deployment (default: eastus) +- `VM_NAME`: Name of the virtual machine (default: ai-agent-vm) +- `RESOURCE_GROUP`: Resource group name (default: ai-agent-resources) + +## Deploy + +## Deploy +[![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%2Fvirtual-machine%2Fazuredeploy.json) +## Features + +- Automated VM provisioning with optimal settings for AI workloads +- Pre-installed AI development tools and dependencies +- Network security configuration for agent communication +- Integration with Azure AI services + +## Step to connect to VS Code + +You can connect to your Azure VM directly from Visual Studio Code using the Remote SSH extension. Here's how to set it up on Windows and Linux: + +Windows +Install Required Extensions + +Open VS Code and install the "Remote - SSH" extension +Install "Azure Account" and "Azure Resources" extensions +Configure SSH + +Open File Explorer and navigate to C:\Users\YourUsername\.ssh +Place your existing private key in this folder (the one that pairs with the public key deployed to the VM) +Create or edit the file config in this folder with: +Connect through Azure Portal + +Example SSH Config +``` +Host ubuntu24-vm + User azureuser + HostName + IdentityFile /C:/Users//.ssh/project-vm.pem +``` + +Open VS Code command palette (Ctrl+Shift+P) +Type "Azure: Sign In" and complete the authentication +Select the Azure icon in the Activity Bar +Expand your subscription and locate your VM +Right-click and choose "Connect to Host in Current Window" +Connect Directly via Remote SSH + +Click the green >< icon in the bottom-left corner of VS Code +Select "Connect to Host..." +Choose your VM from the list +VS Code will open a new window connected to your VM + +## Troubleshooting + +- **Deployment Failures**: Check Azure activity logs and ensure you have sufficient quota +- **Connection Issues**: Verify network security group settings and that you have the correct SSH keys +- **Performance Problems**: Consider upgrading to a VM with more resources + +## Additional Resources + +- [Azure AI Agents Documentation](https://learn.microsoft.com/azure/ai-services/agents/) +- [Azure Virtual Machines Documentation](https://learn.microsoft.com/azure/virtual-machines/) +- [Azure AI Services](https://learn.microsoft.com/azure/ai-services/) + +## Contributing + +Please see the [CONTRIBUTING.md](../../../../CONTRIBUTING.md) file for guidelines on how to contribute to this project. + +## License + +This project is licensed under the MIT License - see the [LICENSE](../../../../LICENSE) file for details. diff --git a/scenarios/Agents/setup/virtual-machine/azuredeploy.json b/scenarios/Agents/setup/virtual-machine/azuredeploy.json new file mode 100644 index 00000000..e93fb649 --- /dev/null +++ b/scenarios/Agents/setup/virtual-machine/azuredeploy.json @@ -0,0 +1,347 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "7735979115864224877" + } + }, + "parameters": { + "subnetName": { + "type": "string", + "metadata": { + "description": "The public SSH key for the VM administrator account." + } + }, + "virtualNetworkName": { + "type": "string", + "metadata": { + "description": "The name of the existing subnet in the virtual network." + } + }, + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "The name of the existing virtual network to use." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The name of the existing virtual network to use." + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "The name of the existing virtual network to use." + } + }, + "adminPublicKey": { + "type": "securestring", + "metadata": { + "description": "The username for the VM administrator account." + } + }, + "vmSize": { + "type": "string", + "defaultValue": "Standard_D2s_v3" + }, + "bastionName": { + "type": "string", + "defaultValue": "[format('{0}-bastion', parameters('virtualNetworkName'))]" + }, + "userAssignedIdentity": { + "type": "string", + "metadata": { + "description": "User assigned identity for the VM." + } + }, + "uniqueSuffix": { + "type": "string", + "defaultValue": "[substring(uniqueString(resourceGroup().id), 0, 4)]" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('virtualNetworkName'), 'AzureBastionSubnet')]", + "properties": { + "addressPrefix": "172.16.1.0/26", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled" + } + }, + { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-05-01", + "name": "[format('{0}-pip-{1}', parameters('bastionName'), parameters('uniqueSuffix'))]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "publicIPAllocationMethod": "Static", + "publicIPAddressVersion": "IPv4", + "idleTimeoutInMinutes": 4, + "dnsSettings": { + "domainNameLabel": "[toLower(format('{0}-{1}', parameters('bastionName'), uniqueString(resourceGroup().id)))]" + } + }, + "zones": [ + "1", + "2", + "3" + ] + }, + { + "type": "Microsoft.Network/bastionHosts", + "apiVersion": "2023-05-01", + "name": "[format('{0}-{1}', parameters('bastionName'), parameters('uniqueSuffix'))]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "enableFileCopy": true, + "enableTunneling": true, + "enableIpConnect": true, + "scaleUnits": 2, + "ipConfigurations": [ + { + "name": "IpConf", + "properties": { + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('bastionName'), parameters('uniqueSuffix')))]" + }, + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'AzureBastionSubnet')]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('bastionName'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'AzureBastionSubnet')]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-05-01", + "name": "[format('{0}-nsg-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))]", + "location": "[parameters('location')]", + "properties": { + "securityRules": [ + { + "name": "AllowSSHFromBastion", + "properties": { + "priority": 100, + "protocol": "Tcp", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "VirtualNetwork", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "22", + "description": "Allow SSH access from Bastion only" + } + }, + { + "name": "DenyAllInbound", + "properties": { + "priority": 4096, + "protocol": "*", + "access": "Deny", + "direction": "Inbound", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "*", + "description": "Deny all other inbound traffic" + } + } + ] + }, + "tags": { + "vm": "[parameters('virtualMachineName')]" + } + }, + { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-05-01", + "name": "[format('{0}-pip-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "publicIPAllocationMethod": "Static", + "publicIPAddressVersion": "IPv4", + "dnsSettings": { + "domainNameLabel": "[toLower(format('{0}-{1}', parameters('virtualMachineName'), uniqueString(resourceGroup().id)))]" + } + }, + "zones": [ + "1", + "2", + "3" + ], + "tags": { + "vm": "[parameters('virtualMachineName')]" + } + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-05-01", + "name": "[format('{0}-nic-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))]", + "location": "[parameters('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + }, + "enableAcceleratedNetworking": true + }, + "tags": { + "vm": "[parameters('virtualMachineName')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]", + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[format('{0}-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('userAssignedIdentity')))]": {} + } + }, + "location": "[parameters('location')]", + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "canonical", + "offer": "ubuntu-24_04-lts", + "sku": "server", + "version": "latest" + }, + "osDisk": { + "name": "[format('{0}-osdisk', parameters('virtualMachineName'))]", + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "caching": "ReadWrite", + "deleteOption": "Delete" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-nic-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]", + "properties": { + "deleteOption": "Delete" + } + } + ] + }, + "osProfile": { + "computerName": "[parameters('virtualMachineName')]", + "adminUsername": "[parameters('adminUsername')]", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminPublicKey')]" + } + ] + }, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + } + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": true + } + } + }, + "tags": { + "application": "agents", + "environment": "development" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-nic-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2023-09-01", + "name": "[format('{0}/{1}', format('{0}-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')), 'AzureMonitorLinuxAgent')]", + "location": "[parameters('location')]", + "properties": { + "publisher": "Microsoft.Azure.Monitor", + "type": "AzureMonitorLinuxAgent", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + ] + } + ], + "outputs": { + "vmId": { + "type": "string", + "value": "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix')))]" + }, + "vmName": { + "type": "string", + "value": "[format('{0}-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))]" + }, + "vmFqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('virtualMachineName'), parameters('uniqueSuffix'))), '2023-05-01').dnsSettings.fqdn]" + }, + "bastionId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/bastionHosts', format('{0}-{1}', parameters('bastionName'), parameters('uniqueSuffix')))]" + }, + "bastionFqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-pip-{1}', parameters('bastionName'), parameters('uniqueSuffix'))), '2023-05-01').dnsSettings.fqdn]" + } + } +} \ No newline at end of file diff --git a/scenarios/Agents/setup/virtual-machine/main.bicep b/scenarios/Agents/setup/virtual-machine/main.bicep new file mode 100644 index 00000000..d812111d --- /dev/null +++ b/scenarios/Agents/setup/virtual-machine/main.bicep @@ -0,0 +1,283 @@ +@description('The public SSH key for the VM administrator account.') +param subnetName string +@description('The name of the existing subnet in the virtual network.') +param virtualNetworkName string +@description('The name of the existing virtual network to use.') +param virtualMachineName string + +// New parameters +@description('The name of the existing virtual network to use.') +param location string = resourceGroup().location +@description('The name of the existing virtual network to use.') +param adminUsername string + +@description('The username for the VM administrator account.') +@secure() +param adminPublicKey string + +param vmSize string = 'Standard_D2s_v3' +param bastionName string = '${virtualNetworkName}-bastion' + +@description('User assigned identity for the VM.') +param userAssignedIdentity string + +// Create a short, unique suffix, that will be unique to each resource group +param uniqueSuffix string = substring(uniqueString(resourceGroup().id), 0, 4) + +resource existingVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: virtualNetworkName + scope: resourceGroup() +} + +// Get existing subnet in the virtual network +resource cxSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = { + parent: existingVirtualNetwork + name: subnetName +} + +resource umiExisting 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = { + name: userAssignedIdentity + scope: resourceGroup() +} + +// Create AzureBastionSubnet in the existing virtual network +resource bastionSubnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = { + parent: existingVirtualNetwork + name: 'AzureBastionSubnet' // This specific name is required by Azure Bastion + properties: { + addressPrefix: '172.16.1.0/26' // Using the same prefix as your reference + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } +} + +// Create new public IP for Bastion following Azure best practices +resource bastionPublicIP 'Microsoft.Network/publicIPAddresses@2023-05-01' = { + name: '${bastionName}-pip-${uniqueSuffix}' + location: location + sku: { + name: 'Standard' // Required for Azure Bastion + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' // Required for Azure Bastion + publicIPAddressVersion: 'IPv4' + idleTimeoutInMinutes: 4 + dnsSettings: { + domainNameLabel: toLower('${bastionName}-${uniqueString(resourceGroup().id)}') + } + } + zones: ['1', '2', '3'] // Zone redundant for high availability +} + +// Create Azure Bastion with Standard SKU for enhanced features +resource bastion 'Microsoft.Network/bastionHosts@2023-05-01' = { + name: '${bastionName}-${uniqueSuffix}' + location: location + sku: { + name: 'Standard' // Standard SKU supports file copy, native client, and tunneling + } + properties: { + enableFileCopy: true + enableTunneling: true + enableIpConnect: true + scaleUnits: 2 // For better performance + ipConfigurations: [ + { + name: 'IpConf' + properties: { + publicIPAddress: { + id: bastionPublicIP.id + } + subnet: { + id: bastionSubnet.id + } + } + } + ] + } +} + +// Create VM NSG with Azure best practice security rules +resource vmNsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = { + name: '${virtualMachineName}-nsg-${uniqueSuffix}' + location: location + properties: { + securityRules: [ + { + name: 'AllowSSHFromBastion' + properties: { + priority: 100 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + description: 'Allow SSH access from Bastion only' + } + } + { + name: 'DenyAllInbound' + properties: { + priority: 4096 + protocol: '*' + access: 'Deny' + direction: 'Inbound' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '*' + description: 'Deny all other inbound traffic' + } + } + ] + } + tags: { + vm: virtualMachineName + } +} + +// Create public IP for VM with Standard SKU +resource vmPublicIP 'Microsoft.Network/publicIPAddresses@2023-05-01' = { + name: '${virtualMachineName}-pip-${uniqueSuffix}' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + dnsSettings: { + domainNameLabel: toLower('${virtualMachineName}-${uniqueString(resourceGroup().id)}') + } + } + zones: ['1', '2', '3'] // Zone redundant for high availability + tags: { + vm: virtualMachineName + } +} + +// Create network interface for VM with NSG +resource vmNic 'Microsoft.Network/networkInterfaces@2023-05-01' = { + name: '${virtualMachineName}-nic-${uniqueSuffix}' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: cxSubnet.id + } + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: { + id: vmPublicIP.id + } + } + } + ] + networkSecurityGroup: { + id: vmNsg.id + } + enableAcceleratedNetworking: true // For better network performance + } + tags: { + vm: virtualMachineName + } +} + +// Create Ubuntu 24.04 virtual machine with secure configurations +resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: '${virtualMachineName}-${uniqueSuffix}' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${umiExisting.id}': {} + } + } + location: location + properties: { + hardwareProfile: { + vmSize: vmSize + } + storageProfile: { + imageReference: { + publisher: 'canonical' + offer: 'ubuntu-24_04-lts' + sku: 'server' + version: 'latest' + } + osDisk: { + name: '${virtualMachineName}-osdisk' + createOption: 'FromImage' + managedDisk: { + storageAccountType: 'Premium_LRS' + } + caching: 'ReadWrite' + deleteOption: 'Delete' // Clean up disk when VM is deleted + } + } + networkProfile: { + networkInterfaces: [ + { + id: vmNic.id + properties: { + deleteOption: 'Delete' // Clean up NIC when VM is deleted + } + } + ] + } + osProfile: { + computerName: virtualMachineName + adminUsername: adminUsername + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminPublicKey + } + ] + } + patchSettings: { + patchMode: 'ImageDefault' // Enable automatic OS updates + assessmentMode: 'ImageDefault' + } + } + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true // Enable boot diagnostics for troubleshooting + } + } + } + tags: { + application: 'agents' + environment: 'development' + } +} + +// Add Azure Monitor agent for better monitoring and insights +resource monitoringAgent 'Microsoft.Compute/virtualMachines/extensions@2023-09-01' = { + parent: virtualMachine + name: 'AzureMonitorLinuxAgent' + location: location + properties: { + publisher: 'Microsoft.Azure.Monitor' + type: 'AzureMonitorLinuxAgent' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: true + } +} + +// Outputs +output vmId string = virtualMachine.id +output vmName string = virtualMachine.name +output vmFqdn string = vmPublicIP.properties.dnsSettings.fqdn +output bastionId string = bastion.id +output bastionFqdn string = bastionPublicIP.properties.dnsSettings.fqdn diff --git a/scenarios/Agents/setup/virtual-machine/main.parameters.json b/scenarios/Agents/setup/virtual-machine/main.parameters.json new file mode 100644 index 00000000..8a048919 --- /dev/null +++ b/scenarios/Agents/setup/virtual-machine/main.parameters.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "subnetName": { + "value": "hub-subnet-vfmk" + }, + "virtualNetworkName": { + "value": "agents-vnet-vfmk" + }, + "virtualMachineName": { + "value": "ubuntu24-vm" + }, + "adminUsername": { + "value": "azureuser" + }, + "adminPublicKey": { + "value": "ssh-rsa" + }, + "vmSize": { + "value": "Standard_D2s_v3" + }, + "bastionName": { + "value": "agents-vnet-vfmk-bastion" + }, + "userAssignedIdentity":{ + "value": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/agents-vnet-vfmk/providers/Microsoft.ManagedIdentity/userAssignedIdentities/agents-vm-identity" + } + } + } \ No newline at end of file