diff --git a/scenarios/Agents/setup/standard-agent-UMI/azuredeploy.parameters.json b/scenarios/Agents/setup/standard-agent-UMI/azuredeploy.parameters.json new file mode 100644 index 00000000..209c5a87 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/azuredeploy.parameters.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "aiHubName": { + "value": "hub-demo" + }, + "aiHubFriendlyName": { + "value": "Agents Hub resource" + }, + "aiHubDescription": { + "value": "This is an example AI resource for use in Azure AI Studio." + }, + "aiProjectName": { + "value": "project-demo" + }, + "aiProjectFriendlyName": { + "value": "Agents Project resource" + }, + "aiProjectDescription": { + "value": "This is an example AI Project resource for use in Azure AI Studio." + }, + "aiSearchName": { + "value": "agent-ai-search" + }, + "capabilityHostName": { + "value": "caphost1" + }, + "storageName": { + "value": "agent-storage" + }, + "aiServicesName": { + "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": "eastus" + }, + "aiServiceAccountResourceId": { + "value": "" + }, + "aiSearchServiceResourceId": { + "value": "" + }, + "aiStorageAccountResourceId":{ + "value": "" + }, + "aiServiceKind": { + "value": "AIServices" + }, + "deploymentTimestamp": { + "value": "vhvb1989" + } + } + } + \ No newline at end of file diff --git a/scenarios/Agents/setup/standard-agent-UMI/main.bicep b/scenarios/Agents/setup/standard-agent-UMI/main.bicep new file mode 100644 index 00000000..2a40efb1 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/main.bicep @@ -0,0 +1,219 @@ +// Execute this main file to deploy Enterprise Standard Agent setup resources + +module userDefinedTypes 'modules-standard/types.bicep' = {} + +// Parameters +@minLength(2) +@maxLength(12) +@description('Name for the AI resource and used to derive name of dependent resources.') +param aiHubName string = 'hub-demo' + +@description('Friendly name for your Hub resource') +param aiHubFriendlyName string = 'Agents Hub resource' + +@description('Description of your Azure AI resource displayed in AI studio') +param aiHubDescription string = 'This is an example AI resource for use in Azure AI Studio.' + +@description('Name for the AI project resources.') +param aiProjectName string = 'project-demo' + +@description('Friendly name for your Azure AI resource') +param aiProjectFriendlyName string = 'Agents Project resource' + +@description('Description of your Azure AI resource displayed in AI studio') +param aiProjectDescription string = 'This is an example AI Project resource for use in Azure AI Studio.' + +@description('Azure region used for the deployment of all resources.') +param location string = resourceGroup().location + +@description('Set of tags to apply to all resources.') +param tags object = {} + +@description('Name of the Azure AI Search account') +param aiSearchName string = 'agent-ai-search' + +@description('Name for capabilityHost.') +param capabilityHostName string = 'caphost1' + +@description('Name of the storage account') +param storageName string = 'agent-storage' + +@description('Name of the Azure AI Services account') +param aiServicesName string = 'agent-ai-services' + +@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 = 'eastus' + +@description('AI Service Account kind: either AzureOpenAI or AIServices') +param aiServiceKind string = 'AIServices' + +@description('The AI Service Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiServiceAccountResourceId string = '' + +@description('The Ai Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiSearchServiceResourceId string = '' + +@description('The Ai Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiStorageAccountResourceId string = '' + +@description('The User Assigned Identity name (within the same resource group) for AI cognitive services. This is an optional field, and if not provided, the resource will be created.') +param userManagedIdentityName string = '' + +// Variables +var name = toLower('${aiHubName}') +var projectName = toLower('${aiProjectName}') + +// Create a short, unique suffix, that will be unique to each resource group +// var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) +param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') +var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) + +var aiServiceExists = aiServiceAccountResourceId != '' +var acsExists = aiSearchServiceResourceId != '' + +var aiServiceParts = split(aiServiceAccountResourceId, '/') +var aiServiceAccountSubscriptionId = aiServiceExists ? aiServiceParts[2] : subscription().subscriptionId +var aiServiceAccountResourceGroupName = aiServiceExists ? aiServiceParts[4] : resourceGroup().name + +var acsParts = split(aiSearchServiceResourceId, '/') +var aiSearchServiceSubscriptionId = acsExists ? acsParts[2] : subscription().subscriptionId +var aiSearchServiceResourceGroupName = acsExists ? acsParts[4] : resourceGroup().name + +var aiServicesUserAssignedIdentityExists = userManagedIdentityName != '' + +resource aiExistingServicesUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (aiServicesUserAssignedIdentityExists) { + name: userManagedIdentityName +} + +resource aiServicesUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = if (!aiServicesUserAssignedIdentityExists) { + name: 'umi-${projectName}-${uniqueSuffix}' + location: location + tags: tags +} + +var umi = { + id: aiServicesUserAssignedIdentityExists ? aiExistingServicesUserManagedIdentity.id : aiServicesUserManagedIdentity.id + principalId: aiServicesUserAssignedIdentityExists ? aiExistingServicesUserManagedIdentity.properties.principalId : aiServicesUserManagedIdentity.properties.principalId +} + +// Dependent resources for the Azure Machine Learning workspace +module aiDependencies 'modules-standard/standard-dependent-resources.bicep' = { + name: 'dependencies-${name}-${uniqueSuffix}-deployment' + params: { + location: location + storageName: '${storageName}${uniqueSuffix}' + keyvaultName: 'kv-${name}-${uniqueSuffix}' + aiServicesName: '${aiServicesName}${uniqueSuffix}' + aiSearchName: '${aiSearchName}-${uniqueSuffix}' + tags: tags + + // Model deployment parameters + modelName: modelName + modelFormat: modelFormat + modelVersion: modelVersion + modelSkuName: modelSkuName + modelCapacity: modelCapacity + modelLocation: modelLocation + + aiServiceAccountResourceId: aiServiceAccountResourceId + aiSearchServiceResourceId: aiSearchServiceResourceId + aiStorageAccountResourceId: aiStorageAccountResourceId + + userAssignedIdentity: umi + } +} + +module aiHub 'modules-standard/standard-ai-hub.bicep' = { + name: '${name}-${uniqueSuffix}-deployment' + params: { + userAssignedIdentity: umi + + // workspace organization + aiHubName: '${name}-${uniqueSuffix}' + aiHubFriendlyName: aiHubFriendlyName + aiHubDescription: aiHubDescription + location: location + tags: tags + + aiSearchName: aiDependencies.outputs.aiSearchName + aiSearchId: aiDependencies.outputs.aisearchID + aiSearchServiceResourceGroupName: aiDependencies.outputs.aiSearchServiceResourceGroupName + aiSearchServiceSubscriptionId: aiDependencies.outputs.aiSearchServiceSubscriptionId + + aiServicesName: aiDependencies.outputs.aiServicesName + aiServiceKind: aiServiceKind + aiServicesId: aiDependencies.outputs.aiservicesID + aiServicesTarget: aiDependencies.outputs.aiservicesTarget + aiServiceAccountResourceGroupName:aiDependencies.outputs.aiServiceAccountResourceGroupName + aiServiceAccountSubscriptionId:aiDependencies.outputs.aiServiceAccountSubscriptionId + + keyVaultId: aiDependencies.outputs.keyvaultId + storageAccountId: aiDependencies.outputs.storageId + } +} + + +module aiProject 'modules-standard/standard-ai-project.bicep' = { + name: '${projectName}-${uniqueSuffix}-deployment' + params: { + // workspace organization + aiProjectName: '${projectName}-${uniqueSuffix}' + aiProjectFriendlyName: aiProjectFriendlyName + aiProjectDescription: aiProjectDescription + location: location + tags: tags + aiHubId: aiHub.outputs.aiHubID + userAssignedIdentity: umi + } +} + +module aiServiceRoleAssignments 'modules-standard/ai-service-role-assignments.bicep' = { + name: 'ai-service-role-assignments-${projectName}-${uniqueSuffix}-deployment' + scope: resourceGroup(aiServiceAccountSubscriptionId, aiServiceAccountResourceGroupName) + params: { + aiServicesName: aiDependencies.outputs.aiServicesName + aiProjectPrincipalId: aiProject.outputs.aiProjectPrincipalId + aiProjectId: aiProject.outputs.aiProjectResourceId + } +} + +module aiSearchRoleAssignments 'modules-standard/ai-search-role-assignments.bicep' = { + name: 'ai-search-role-assignments-${projectName}-${uniqueSuffix}-deployment' + scope: resourceGroup(aiSearchServiceSubscriptionId, aiSearchServiceResourceGroupName) + params: { + aiSearchName: aiDependencies.outputs.aiSearchName + aiProjectPrincipalId: aiProject.outputs.aiProjectPrincipalId + aiProjectId: aiProject.outputs.aiProjectResourceId + } +} + +module addCapabilityHost 'modules-standard/add-capability-host.bicep' = { + name: 'capabilityHost-configuration--${uniqueSuffix}-deployment' + params: { + capabilityHostName: '${uniqueSuffix}-${capabilityHostName}' + aiHubName: aiHub.outputs.aiHubName + aiProjectName: aiProject.outputs.aiProjectName + acsConnectionName: aiHub.outputs.acsConnectionName + aoaiConnectionName: aiHub.outputs.aoaiConnectionName + } + dependsOn: [ + aiSearchRoleAssignments,aiServiceRoleAssignments + ] +} + +output PROJECT_CONNECTION_STRING string = aiProject.outputs.projectConnectionString diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/add-capability-host.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/add-capability-host.bicep new file mode 100644 index 00000000..098a4ad1 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/add-capability-host.bicep @@ -0,0 +1,50 @@ +@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 + +var storageConnections = ['${aiProjectName}/workspaceblobstore'] +var aiSearchConnection = ['${acsConnectionName}'] +var aiServiceConnections = ['${aoaiConnectionName}'] + +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: { + capabilityHostKind: 'Agents' + } +} + +#disable-next-line BCP081 +resource projectCapabilityHost 'Microsoft.MachineLearningServices/workspaces/capabilityHosts@2024-10-01-preview' = { + name: '${aiProjectName}-${capabilityHostName}' + parent: aiProject + properties: { + capabilityHostKind: 'Agents' + aiServicesConnections: aiServiceConnections + vectorStoreConnections: aiSearchConnection + storageConnections: storageConnections + } + dependsOn: [ + hubCapabilityHost + ] +} diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-search-role-assignments.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-search-role-assignments.bicep new file mode 100644 index 00000000..eb8e8b5e --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-search-role-assignments.bicep @@ -0,0 +1,46 @@ +// Assigns the necessary roles to the AI project + +@description('Name of the AI Search resource') +param aiSearchName string + +@description('Principal ID of the AI project') +param aiProjectPrincipalId string + +@description('Resource ID of the AI project') +param aiProjectId string + +resource searchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = { + name: aiSearchName + scope: resourceGroup() +} + +// search roles +resource searchIndexDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + scope: resourceGroup() +} + +resource searchIndexDataContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + name: guid(aiProjectId, searchIndexDataContributorRole.id, searchService.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: searchIndexDataContributorRole.id + principalType: 'ServicePrincipal' + } +} + +resource searchServiceContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' + scope: resourceGroup() +} + +resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + name: guid(aiProjectId, searchServiceContributorRole.id, searchService.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: searchServiceContributorRole.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-service-role-assignments.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-service-role-assignments.bicep new file mode 100644 index 00000000..f9afd3e9 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/ai-service-role-assignments.bicep @@ -0,0 +1,61 @@ +// Assigns the necessary roles to the AI project + +@description('Name of the AI Services resource') +param aiServicesName string + +@description('Principal ID of the AI project') +param aiProjectPrincipalId string + +@description('Resource ID of the AI project') +param aiProjectId string + +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = { + name: aiServicesName + scope: resourceGroup() +} + +resource cognitiveServicesContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + scope: resourceGroup() + +} + +resource cognitiveServicesContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01'= { + scope: aiServices + name: guid(aiServices.id, cognitiveServicesContributorRole.id, aiProjectId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesContributorRole.id + principalType: 'ServicePrincipal' + } +} + + +resource cognitiveServicesOpenAIUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + scope: resourceGroup() +} +resource cognitiveServicesOpenAIUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + name: guid(aiProjectId, cognitiveServicesOpenAIUserRole.id, aiServices.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesOpenAIUserRole.id + principalType: 'ServicePrincipal' + } +} + +resource cognitiveServicesUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: 'a97b65f3-24c7-4388-baec-2e87135dc908' + scope: resourceGroup() +} + +resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + name: guid(aiProjectId, cognitiveServicesUserRole.id, aiServices.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: cognitiveServicesUserRole.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/keyvault-role-assignments.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/keyvault-role-assignments.bicep new file mode 100644 index 00000000..e10dda44 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/keyvault-role-assignments.bicep @@ -0,0 +1,93 @@ +/* +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 + +/* -------------------------------------------- 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) + 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) + 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/standard-agent-UMI/modules-standard/standard-ai-hub.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-ai-hub.bicep new file mode 100644 index 00000000..5cba7481 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-ai-hub.bicep @@ -0,0 +1,146 @@ +// Creates an Azure AI resource with proxied endpoints for the Azure AI services provider +import { + managedIdentity +} from './types.bicep' + +@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 resource for storing connection strings') +param keyVaultId string + +@description('Resource ID of the storage account resource for storing experimentation outputs') +param storageAccountId string + +@description('Resource ID of the AI Services resource') +param aiServicesId string + +@description('Resource ID of the AI Services endpoint') +param aiServicesTarget string + +@description('Name AI Services resource') +param aiServicesName string + +@description('Resource Group name of the AI Services resource') +param aiServiceAccountResourceGroupName string + +@description('Subscription ID of the AI Services resource') +param aiServiceAccountSubscriptionId string + +@description('Name AI Search resource') +param aiSearchName string + +@description('Resource ID of the AI Search resource') +param aiSearchId string + +@description('Resource Group name of the AI Search resource') +param aiSearchServiceResourceGroupName string + +@description('Subscription ID of the AI Search resource') +param aiSearchServiceSubscriptionId string + +/* @description('Name for capabilityHost.') +param capabilityHostName string */ + +@description('AI Service Account kind: either OpenAI or AIServices') +param aiServiceKind string + +@description('User managed identity resource ID') +param userAssignedIdentity managedIdentity + +var acsConnectionName = '${aiHubName}-connection-AISearch' + +var aoaiConnection = '${aiHubName}-connection-AIServices_aoai' + +var kindAIServicesExists = aiServiceKind == 'AIServices' + +var aiServiceConnectionName = kindAIServicesExists ? '${aiHubName}-connection-AIServices' : aoaiConnection + +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: aiServicesName + scope: resourceGroup(aiServiceAccountSubscriptionId, aiServiceAccountResourceGroupName) +} + +resource searchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = { + name: aiSearchName + scope: resourceGroup(aiSearchServiceSubscriptionId, aiSearchServiceResourceGroupName) +} + +resource aiHub 'Microsoft.MachineLearningServices/workspaces@2024-10-01-preview' = { + name: aiHubName + location: location + tags: tags + identity: { + type: 'UserAssigned' + userAssignedIdentities: json('{\'${userAssignedIdentity.id}\': {}}') + } + properties: { + // organization + friendlyName: aiHubFriendlyName + description: aiHubDescription + + // dependent resources + keyVault: keyVaultId + storageAccount: storageAccountId + systemDatastoresAuthMode: 'identity' + primaryUserAssignedIdentity: userAssignedIdentity.id + } + kind: 'hub' + + resource aiServicesConnection 'connections@2024-07-01-preview' = { + name: aiServiceConnectionName + properties: { + category: aiServiceKind // either AIServices or AzureOpenAI + target: aiServicesTarget + authType: 'AAD' + isSharedToAll: true + metadata: { + ApiType: 'Azure' + ResourceId: aiServicesId + location: aiServices.location + } + } + } + resource hub_connection_azureai_search 'connections@2024-07-01-preview' = { + name: acsConnectionName + properties: { + category: 'CognitiveSearch' + target: 'https://${aiSearchName}.search.windows.net' + authType: 'AAD' + //useWorkspaceManagedIdentity: false + isSharedToAll: true + metadata: { + ApiType: 'Azure' + ResourceId: aiSearchId + location: searchService.location + } + } + } + + // Resource definition for the capability host + #disable-next-line BCP081 + /* resource capabilityHost 'capabilityHosts@2024-10-01-preview' = { + name: '${aiHubName}-${capabilityHostName}' + properties: { + capabilityHostKind: 'Agents' + } + } */ + +} + +output aiHubID string = aiHub.id +output aiHubName string = aiHub.name +output aoaiConnectionName string = aoaiConnection +output acsConnectionName string = acsConnectionName diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-ai-project.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-ai-project.bicep new file mode 100644 index 00000000..74879d2c --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-ai-project.bicep @@ -0,0 +1,74 @@ +// Creates an Azure AI resource with proxied endpoints for the Azure AI services provider +import { + managedIdentity +} from './types.bicep' + +@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('User managed identity resource ID') +param userAssignedIdentity managedIdentity + +/* @description('Name for capabilityHost.') +param capabilityHostName string + +@description('Name for ACS connection.') +param acsConnectionName string + +@description('Name for ACS connection.') +param aoaiConnectionName string */ + +//for constructing endpoint +var subscriptionId = subscription().subscriptionId +var resourceGroupName = resourceGroup().name + +var projectConnectionString = '${location}.api.azureml.ms;${subscriptionId};${resourceGroupName};${aiProjectName}' + + +/* var storageConnections = ['${aiProjectName}/workspaceblobstore'] +var aiSearchConnection = ['${acsConnectionName}'] +var aiServiceConnections = ['${aoaiConnectionName}'] */ + + +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2025-01-01-preview' = { + name: aiProjectName + location: location + tags: union(tags, { + ProjectConnectionString: projectConnectionString + }) + identity: { + type: 'UserAssigned' + userAssignedIdentities: {'${userAssignedIdentity.id}': {}} + } + properties: { + // organization + friendlyName: aiProjectFriendlyName + description: aiProjectDescription + + // dependent resources + hubResourceId: aiHubId + primaryUserAssignedIdentity: userAssignedIdentity.id + } + kind: 'project' +} + +output aiProjectName string = aiProject.name +output aiProjectResourceId string = aiProject.id +output aiProjectPrincipalId string = userAssignedIdentity.principalId +output aiProjectWorkspaceId string = aiProject.properties.workspaceId +output projectConnectionString string = aiProject.tags.ProjectConnectionString diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-dependent-resources.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-dependent-resources.bicep new file mode 100644 index 00000000..cd62cd0d --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/standard-dependent-resources.bicep @@ -0,0 +1,223 @@ +// Creates Azure dependent resources for Azure AI Agent Service standard agent setup +import { + managedIdentity +} from './types.bicep' + +@description('Azure region of the deployment') +param location string = resourceGroup().location + +@description('Tags to add to the resources') +param tags object = {} + +@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 + +var storageNameCleaned = replace(storageName, '-', '') + +@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 AI Service Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiServiceAccountResourceId string + +@description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiSearchServiceResourceId string + +@description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiStorageAccountResourceId string + +var aiServiceExists = aiServiceAccountResourceId != '' +var acsExists = aiSearchServiceResourceId != '' +var aiStorageExists = aiStorageAccountResourceId != '' + +@description('User managed identity resource ID') +param userAssignedIdentity managedIdentity + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyvaultName + location: location + tags: tags + properties: { + createMode: 'default' + enabledForDeployment: false + enabledForDiskEncryption: false + enabledForTemplateDeployment: false + enableSoftDelete: true + enableRbacAuthorization: true + enablePurgeProtection: true + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + } + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} + + +var aiServiceParts = split(aiServiceAccountResourceId, '/') + +resource existingAIServiceAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (aiServiceExists) { + name: aiServiceParts[8] + scope: resourceGroup(aiServiceParts[2], aiServiceParts[4]) +} + +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-10-01' = if(!aiServiceExists) { + name: aiServicesName + location: modelLocation + sku: { + name: 'S0' + } + kind: 'AIServices' // or 'OpenAI' + identity: { + type: 'SystemAssigned' + } + properties: { + customSubDomainName: toLower('${(aiServicesName)}') + apiProperties: { + statisticsEnabled: false + } + publicNetworkAccess: 'Enabled' + } +} +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01'= if(!aiServiceExists){ + parent: aiServices + name: modelName + sku : { + capacity: modelCapacity + name: modelSkuName + } + properties: { + model:{ + name: modelName + format: modelFormat + version: modelVersion + } + } +} + +var acsParts = split(aiSearchServiceResourceId, '/') + +resource existingSearchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = if (acsExists) { + name: acsParts[8] + scope: resourceGroup(acsParts[2], acsParts[4]) +} +resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = if(!acsExists) { + name: aiSearchName + location: location + tags: tags + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + properties: { + disableLocalAuth: false + authOptions: { aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge'}} + encryptionWithCmk: { + enforcement: 'Unspecified' + } + hostingMode: 'default' + partitionCount: 1 + publicNetworkAccess: 'enabled' + replicaCount: 1 + semanticSearch: 'disabled' + } + sku: { + name: 'standard' + } +} + +var aiStorageParts = split(aiStorageAccountResourceId, '/') + +resource existingAIStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = if (aiStorageExists) { + name: aiStorageParts[8] + scope: resourceGroup(aiStorageParts[2], aiStorageParts[4]) +} + +// Some regions doesn't support Standard Zone-Redundant storage, need to use Geo-redundant storage +param noZRSRegions array = ['southindia', 'westus'] +param sku object = contains(noZRSRegions, location) ? { name: 'Standard_GRS' } : { name: 'Standard_ZRS' } + +resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = if(!aiStorageExists) { + name: storageNameCleaned + location: location + kind: 'StorageV2' + sku: sku + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + publicNetworkAccess: 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + virtualNetworkRules: [] + } + allowSharedKeyAccess: false + } +} + +module storageAccessAssignment './storage-role-assignments.bicep' = if(!aiStorageExists){ + name: 'dependencies-storage-rbac' + params: { + storageName: storageNameCleaned + UAIPrincipalId: userAssignedIdentity.principalId + } + dependsOn: [ storage ] +} + +module keyVaultAccessAssignment './keyvault-role-assignments.bicep' = { + name: 'dependencies-keyvault-rbac' + params: { + keyvaultName: keyvaultName + UAIPrincipalId: userAssignedIdentity.principalId + } + dependsOn: [ keyVault ] +} + +output aiServicesName string = aiServiceExists ? existingAIServiceAccount.name : aiServicesName +output aiservicesID string = aiServiceExists ? existingAIServiceAccount.id : aiServices.id +output aiservicesTarget string = aiServiceExists ? existingAIServiceAccount.properties.endpoint : aiServices.properties.endpoint +output aiServiceAccountResourceGroupName string = aiServiceExists ? aiServiceParts[4] : resourceGroup().name +output aiServiceAccountSubscriptionId string = aiServiceExists ? aiServiceParts[2] : subscription().subscriptionId + +output aiSearchName string = acsExists ? existingSearchService.name : aiSearch.name +output aisearchID string = acsExists ? existingSearchService.id : aiSearch.id +output aiSearchServiceResourceGroupName string = acsExists ? acsParts[4] : resourceGroup().name +output aiSearchServiceSubscriptionId string = acsExists ? acsParts[2] : subscription().subscriptionId + +output storageAccountName string = aiStorageExists ? existingAIStorageAccount.name : storage.name +output storageId string = aiStorageExists ? existingAIStorageAccount.id : storage.id +output storageAccountResourceGroupName string = aiStorageExists ? aiStorageParts[4] : resourceGroup().name +output storageAccountSubscriptionId string = aiStorageExists ? aiStorageParts[2] : subscription().subscriptionId + +output keyvaultId string = keyVault.id diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/storage-role-assignments.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/storage-role-assignments.bicep new file mode 100644 index 00000000..ee66fbd2 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/storage-role-assignments.bicep @@ -0,0 +1,67 @@ +/* +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 + - 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 + +// 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) + 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) + properties: { + principalId: UAIPrincipalId + roleDefinitionId: storageQueueDataContributor.id + principalType: 'ServicePrincipal' + } +} diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/types.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/types.bicep new file mode 100644 index 00000000..09130073 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/types.bicep @@ -0,0 +1,5 @@ +@export() +type managedIdentity = { + id: string + principalId: string +} diff --git a/scenarios/Agents/setup/standard-agent-UMI/modules-standard/wait-script.bicep b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/wait-script.bicep new file mode 100644 index 00000000..39bd9934 --- /dev/null +++ b/scenarios/Agents/setup/standard-agent-UMI/modules-standard/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