From fb4827e52ffc0ebc358518bc8afea18d524a3250 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Fri, 4 Jul 2025 16:39:55 +0530 Subject: [PATCH 1/2] Reuse Ai Foundry for MACAE --- infra/main.bicep | 167 ++---- infra/main.parameters.json | 3 + infra/modules/account/main.bicep | 421 +++++++++++++++ .../account/modules/dependencies.bicep | 479 ++++++++++++++++++ .../account/modules/keyVaultExport.bicep | 43 ++ infra/modules/account/modules/project.bicep | 61 +++ infra/modules/role.bicep | 63 +++ 7 files changed, 1100 insertions(+), 137 deletions(-) create mode 100644 infra/modules/account/main.bicep create mode 100644 infra/modules/account/modules/dependencies.bicep create mode 100644 infra/modules/account/modules/keyVaultExport.bicep create mode 100644 infra/modules/account/modules/project.bicep create mode 100644 infra/modules/role.bicep diff --git a/infra/main.bicep b/infra/main.bicep index 4c5c3dd1f..621564b07 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -4,6 +4,9 @@ metadata description = 'This module contains the resources required to deploy th @description('Set to true if you want to deploy WAF-aligned infrastructure.') param useWafAlignedArchitecture bool +@description('Use this parameter to use an existing AI project resource ID') +param existingFoundryProjectResourceId string = '' + @description('Optional. The prefix to add in the default names given to all deployed Azure resources.') @maxLength(19) param solutionPrefix string = 'macae${uniqueString(deployer().objectId, deployer().tenantId, subscription().subscriptionId, resourceGroup().id)}' @@ -45,10 +48,6 @@ param gptModelCapacity int = 150 @description('Set the image tag for the container images used in the solution. Default is "latest".') param imageTag string = 'latest' -// @description('Set this if you want to deploy to a different region than the resource group. Otherwise, it will use the resource group location by default.') -// param AZURE_LOCATION string='' -// param solutionLocation string = empty(AZURE_LOCATION) ? resourceGroup().location - @description('Optional. The tags to apply to all deployed Azure resources.') param tags object = { app: solutionPrefix @@ -233,32 +232,6 @@ param webSiteConfiguration webSiteConfigurationType = { environmentResourceId: null //Default value set on module configuration } -// -// Add your parameters here -// - -// ============== // -// Resources // -// ============== // - -/* #disable-next-line no-deployments-resources -resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { - name: '46d3xbcp.[[REPLACE WITH TELEMETRY IDENTIFIER]].${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' - properties: { - mode: 'Incremental' - template: { - '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' - contentVersion: '1.0.0.0' - resources: [] - outputs: { - telemetry: { - type: 'String' - value: 'For more information, see https://aka.ms/avm/TelemetryInfo' - } - } - } - } -} */ // ========== Log Analytics Workspace ========== // // WAF best practices for Log Analytics: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-log-analytics @@ -595,8 +568,6 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = if (vi name: 'administration' addressPrefix: '10.0.0.32/27' networkSecurityGroupResourceId: networkSecurityGroupAdministration.outputs.resourceId - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access - //natGatewayResourceId: natGateway.outputs.resourceId } { // For Azure Bastion resources deployed on or after November 2, 2021, the minimum AzureBastionSubnet size is /26 or larger (/25, /24, etc.). @@ -610,7 +581,6 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = if (vi // https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#custom-vnw-configuration name: 'containers' addressPrefix: '10.0.2.0/23' //subnet of size /23 is required for container app - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access delegation: 'Microsoft.App/environments' networkSecurityGroupResourceId: networkSecurityGroupContainers.outputs.resourceId privateEndpointNetworkPolicies: 'Disabled' @@ -640,9 +610,7 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = if (virtualN disableCopyPaste: false enableFileCopy: false enableIpConnect: true - //enableKerberos: bastionConfiguration.?enableKerberos enableShareableLink: true - //scaleUnits: bastionConfiguration.?scaleUnits } } @@ -664,8 +632,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v nicConfigurations: [ { name: 'nic-${virtualMachineResourceName}' - //networkSecurityGroupResourceId: virtualMachineConfiguration.?nicConfigurationConfiguration.networkSecurityGroupResourceId - //nicSuffix: 'nic-${virtualMachineResourceName}' diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspaceId }] ipConfigurations: [ { @@ -691,7 +657,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v diskSizeGB: 128 caching: 'ReadWrite' } - //patchMode: virtualMachineConfiguration.?patchMode osType: 'Windows' encryptionAtHost: false //The property 'securityProfile.encryptionAtHost' is not valid because the 'Microsoft.Compute/EncryptionAtHost' feature is not enabled for this subscription. zone: 0 @@ -699,10 +664,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v enabled: true typeHandlerVersion: '1.0' } - // extensionMonitoringAgentConfig: { - // enabled: true - // } - // maintenanceConfigurationResourceId: virtualMachineConfiguration.?maintenanceConfigurationResourceId } } @@ -750,17 +711,20 @@ var aiFoundryAiServicesModelDeployment = { raiPolicyName: 'Microsoft.Default' } -module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' = if (aiFoundryAIservicesEnabled) { +module aiFoundryAiServices 'modules/account/main.bicep' = if (aiFoundryAIservicesEnabled) { name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) params: { name: aiFoundryAiServicesResourceName tags: aiFoundryAiServicesConfiguration.?tags ?? tags location: aiFoundryAiServicesConfiguration.?location ?? aiDeploymentsLocation enableTelemetry: enableTelemetry + projectName: 'aifp-${solutionPrefix}' + projectDescription: 'aifp-${solutionPrefix}' + existingFoundryProjectResourceId: existingFoundryProjectResourceId diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspaceId }] sku: aiFoundryAiServicesConfiguration.?sku ?? 'S0' kind: 'AIServices' - disableLocalAuth: false //Should be set to true for WAF aligned configuration + disableLocalAuth: true //Should be set to true for WAF aligned configuration customSubDomainName: aiFoundryAiServicesResourceName apiProperties: { //staticsEnabled: false @@ -769,9 +733,13 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' managedIdentities: { systemAssigned: true } - //publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' - //publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' - publicNetworkAccess: 'Enabled' //TODO: connection via private endpoint is not working from containers network. Change this when fixed + publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: (virtualNetworkEnabled) ? 'Deny' : 'Allow' + } + + privateEndpoints: virtualNetworkEnabled ? ([ { @@ -787,18 +755,6 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' } ]) : [] - // roleAssignments: [ - // // { - // // principalId: userAssignedIdentity.outputs.principalId - // // principalType: 'ServicePrincipal' - // // roleDefinitionIdOrName: 'Cognitive Services OpenAI User' - // // } - // { - // principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - // principalType: 'ServicePrincipal' - // roleDefinitionIdOrName: 'Cognitive Services OpenAI User' - // } - // ] deployments: aiFoundryAiServicesConfiguration.?deployments ?? [ { name: aiFoundryAiServicesModelDeployment.name @@ -819,76 +775,34 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' // AI Foundry: AI Project // WAF best practices for Open AI: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-openai -// var aiFoundryAiProjectEnabled = aiFoundryAiProjectConfiguration.?enabled ?? true var aiFoundryAiProjectName = aiFoundryAiProjectConfiguration.?name ?? 'aifp-${solutionPrefix}' -var aiProjectDescription = 'AI Foundry Project' - -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { - name: aiFoundryAiServicesResourceName - dependsOn:[ - aiFoundryAiServices - ] -} - -resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { - parent: aiServices - name: aiFoundryAiProjectName - location: aiFoundryAiProjectConfiguration.?location ?? aiDeploymentsLocation - identity: { - type: 'SystemAssigned' - } - properties: { - description: aiProjectDescription - displayName: aiFoundryAiProjectName - } -} resource aiUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { name: '53ca6127-db72-4b80-b1b0-d745d6d5456d' } -resource aiUserAccessProj 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiFoundryProject.id, aiUser.id) - scope: aiFoundryProject - properties: { - roleDefinitionId: aiUser.id - principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - } -} +var useExistingResourceId = !empty(existingFoundryProjectResourceId) -resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, aiUser.id) - scope: aiServices - properties: { +module Newroles './modules/role.bicep' = if(!useExistingResourceId){ + params: { + name: 'new-${guid(containerApp.name, aiFoundryAiServices.outputs.resourceId, aiUser.id)}' roleDefinitionId: aiUser.id principalId: containerApp.outputs.?systemAssignedMIPrincipalId! + aiUserid: aiUser.id + aiServiceName: aiFoundryAiServices.outputs.name } + scope: resourceGroup(subscription().subscriptionId, resourceGroup().name) } -resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '64702f94-c441-49e6-a78b-ef80e0188fee' -} - -resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, aiDeveloper.id) - scope: aiFoundryProject - properties: { - roleDefinitionId: aiDeveloper.id - principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - } -} - -resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' -} - -resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, cognitiveServiceOpenAIUser.id) - scope: aiServices - properties: { - roleDefinitionId: cognitiveServiceOpenAIUser.id +module Existingroles './modules/role.bicep' = if(useExistingResourceId){ + params: { + name: 'reuse-${guid(containerApp.name, aiFoundryAiServices.outputs.aiProjectInfo.resourceId, aiUser.id)}' + roleDefinitionId: aiUser.id principalId: containerApp.outputs.?systemAssignedMIPrincipalId! + aiUserid: aiUser.id + aiServiceName: aiFoundryAiServices.outputs.name } + scope: resourceGroup( split(existingFoundryProjectResourceId, '/')[2], split(existingFoundryProjectResourceId, '/')[4]) } // ========== Cosmos DB ========== // @@ -966,7 +880,6 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.12.0' = if (co 'EnableServerless' ] sqlRoleAssignmentsPrincipalIds: [ - //userAssignedIdentity.outputs.principalId containerApp.outputs.?systemAssignedMIPrincipalId ] sqlRoleDefinitions: [ @@ -1003,13 +916,6 @@ module containerAppEnvironment 'modules/container-app-environment.bicep' = if (c subnetResourceId: virtualNetworkEnabled ? containerAppEnvironmentConfiguration.?subnetResourceId ?? virtualNetwork.?outputs.?subnetResourceIds[3] ?? '' : '' - //aspireDashboardEnabled: !virtualNetworkEnabled - // vnetConfiguration: virtualNetworkEnabled - // ? { - // internal: false - // infrastructureSubnetId: containerAppEnvironmentConfiguration.?subnetResourceId ?? virtualNetwork.?outputs.?subnetResourceIds[3] ?? '' - // } - // : {} } } @@ -1117,7 +1023,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.14.2' = if (container } { name: 'AZURE_AI_AGENT_ENDPOINT' - value: aiFoundryProject.properties.endpoints['AI Foundry API'] + value: aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint } { name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME' @@ -1189,19 +1095,6 @@ module webSite 'br/public:avm/res/web/site:0.15.1' = if (webSiteEnabled) { @description('The default url of the website to connect to the Multi-Agent Custom Automation Engine solution.') output webSiteDefaultHostname string = webSite.outputs.defaultHostname -// @description('The name of the resource.') -// output name string = .name - -// @description('The location the resource was deployed into.') -// output location string = .location - -// ================ // -// Definitions // -// ================ // -// -// Add your User-defined-types here, if any -// - @export() @description('The type for the Multi-Agent Custom Automation Engine Log Analytics Workspace resource configuration.') type logAnalyticsWorkspaceConfigurationType = { diff --git a/infra/main.parameters.json b/infra/main.parameters.json index a1d690070..5a22eb389 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -39,6 +39,9 @@ "gptModelCapacity": { "value": "${AZURE_ENV_MODEL_CAPACITY}" }, + "existingFoundryProjectResourceId": { + "value": "${AZURE_ENV_FOUNDRY_PROJECT_ID}" + }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG}" }, diff --git a/infra/modules/account/main.bicep b/infra/modules/account/main.bicep new file mode 100644 index 000000000..b1fad4456 --- /dev/null +++ b/infra/modules/account/main.bicep @@ -0,0 +1,421 @@ +metadata name = 'Cognitive Services' +metadata description = 'This module deploys a Cognitive Service.' + +@description('Required. The name of Cognitive Services account.') +param name string + +@description('Optional: Name for the project which needs to be created.') +param projectName string + +@description('Optional: Description for the project which needs to be created.') +param projectDescription string + +param existingFoundryProjectResourceId string = '' + +@description('Required. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'AIServices' + 'AnomalyDetector' + 'CognitiveServices' + 'ComputerVision' + 'ContentModerator' + 'ContentSafety' + 'ConversationalLanguageUnderstanding' + 'CustomVision.Prediction' + 'CustomVision.Training' + 'Face' + 'FormRecognizer' + 'HealthInsights' + 'ImmersiveReader' + 'Internal.AllInOne' + 'LUIS' + 'LUIS.Authoring' + 'LanguageAuthoring' + 'MetricsAdvisor' + 'OpenAI' + 'Personalizer' + 'QnAMaker.v2' + 'SpeechServices' + 'TextAnalytics' + 'TextTranslation' +]) +param kind string + +@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'C2' + 'C3' + 'C4' + 'F0' + 'F1' + 'S' + 'S0' + 'S1' + 'S10' + 'S2' + 'S3' + 'S4' + 'S5' + 'S6' + 'S7' + 'S8' + 'S9' +]) +param sku string = 'S0' + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The diagnostic settings of the service.') +param diagnosticSettings diagnosticSettingFullType[]? + +@description('Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set.') +@allowed([ + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string? + +@description('Conditional. Subdomain name used for token-based authentication. Required if \'networkAcls\' or \'privateEndpoints\' are set.') +param customSubDomainName string? + +@description('Optional. A collection of rules governing the accessibility from specific network locations.') +param networkAcls object? + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') +param privateEndpoints privateEndpointSingleServiceType[]? + +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The lock settings of the service.') +param lock lockType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +@description('Optional. Tags of the resource.') +param tags object? + +@description('Optional. List of allowed FQDN.') +param allowedFqdnList array? + +@description('Optional. The API properties for special APIs.') +param apiProperties object? + +@description('Optional. Allow only Azure AD authentication. Should be enabled for security reasons.') +param disableLocalAuth bool = true + +import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The customer managed key definition.') +param customerManagedKey customerManagedKeyType? + +@description('Optional. The flag to enable dynamic throttling.') +param dynamicThrottlingEnabled bool = false + +@secure() +@description('Optional. Resource migration token.') +param migrationToken string? + +@description('Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists.') +param restore bool = false + +@description('Optional. Restrict outbound network access.') +param restrictOutboundNetworkAccess bool = true + +@description('Optional. The storage accounts for this resource.') +param userOwnedStorage array? + +import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The managed identity definition for this resource.') +param managedIdentities managedIdentityAllType? + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Optional. Array of deployments about cognitive service accounts to create.') +param deployments deploymentType[]? + +@description('Optional. Key vault reference and secret settings for the module\'s secrets export.') +param secretsExportConfiguration secretsExportConfigurationType? + +@description('Optional. Enable/Disable project management feature for AI Foundry.') +param allowProjectManagement bool? + +var formattedUserAssignedIdentities = reduce( + map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), + {}, + (cur, next) => union(cur, next) +) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} } + +var identity = !empty(managedIdentities) + ? { + type: (managedIdentities.?systemAssigned ?? false) + ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') + : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : null) + userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null + } + : null + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { + name: '46d3xbcp.res.cognitiveservices-account.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +resource cMKKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId)) { + name: last(split(customerManagedKey.?keyVaultResourceId!, '/')) + scope: resourceGroup( + split(customerManagedKey.?keyVaultResourceId!, '/')[2], + split(customerManagedKey.?keyVaultResourceId!, '/')[4] + ) + + resource cMKKey 'keys@2023-07-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId) && !empty(customerManagedKey.?keyName)) { + name: customerManagedKey.?keyName! + } +} + +resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) { + name: last(split(customerManagedKey.?userAssignedIdentityResourceId!, '/')) + scope: resourceGroup( + split(customerManagedKey.?userAssignedIdentityResourceId!, '/')[2], + split(customerManagedKey.?userAssignedIdentityResourceId!, '/')[4] + ) +} + +var useExistingService = !empty(existingFoundryProjectResourceId) + +resource cognitiveServiceNew 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if(!useExistingService) { + name: name + kind: kind + identity: identity + location: location + tags: tags + sku: { + name: sku + } + properties: { + allowProjectManagement: allowProjectManagement // allows project management for Cognitive Services accounts in AI Foundry - FDP updates + customSubDomainName: customSubDomainName + networkAcls: !empty(networkAcls ?? {}) + ? { + defaultAction: networkAcls.?defaultAction + virtualNetworkRules: networkAcls.?virtualNetworkRules ?? [] + ipRules: networkAcls.?ipRules ?? [] + } + : null + publicNetworkAccess: publicNetworkAccess != null + ? publicNetworkAccess + : (!empty(networkAcls) ? 'Enabled' : 'Disabled') + allowedFqdnList: allowedFqdnList + apiProperties: apiProperties + disableLocalAuth: disableLocalAuth + encryption: !empty(customerManagedKey) + ? { + keySource: 'Microsoft.KeyVault' + keyVaultProperties: { + identityClientId: !empty(customerManagedKey.?userAssignedIdentityResourceId ?? '') + ? cMKUserAssignedIdentity.properties.clientId + : null + keyVaultUri: cMKKeyVault.properties.vaultUri + keyName: customerManagedKey!.keyName + keyVersion: !empty(customerManagedKey.?keyVersion ?? '') + ? customerManagedKey!.?keyVersion + : last(split(cMKKeyVault::cMKKey.properties.keyUriWithVersion, '/')) + } + } + : null + migrationToken: migrationToken + restore: restore + restrictOutboundNetworkAccess: restrictOutboundNetworkAccess + userOwnedStorage: userOwnedStorage + dynamicThrottlingEnabled: dynamicThrottlingEnabled + } +} + +var existingCognitiveServiceDetails = split(existingFoundryProjectResourceId, '/') + +resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if(useExistingService) { + name: existingCognitiveServiceDetails[8] + scope: resourceGroup(existingCognitiveServiceDetails[2], existingCognitiveServiceDetails[4]) +} + +module cognigive_service_dependencies 'modules/dependencies.bicep' = if(!useExistingService) { + params: { + projectName: projectName + projectDescription: projectDescription + name: cognitiveServiceNew.name + location: location + deployments: deployments + diagnosticSettings: diagnosticSettings + lock: lock + privateEndpoints: privateEndpoints + roleAssignments: roleAssignments + secretsExportConfiguration: secretsExportConfiguration + sku: sku + tags: tags + } +} + +module existing_cognigive_service_dependencies 'modules/dependencies.bicep' = if(useExistingService) { + params: { + name: cognitiveServiceExisting.name + projectName: projectName + projectDescription: projectDescription + azureExistingAIProjectResourceId: existingFoundryProjectResourceId + location: location + deployments: deployments + diagnosticSettings: diagnosticSettings + lock: lock + privateEndpoints: privateEndpoints + roleAssignments: roleAssignments + secretsExportConfiguration: secretsExportConfiguration + sku: sku + tags: tags + } + scope: resourceGroup(existingCognitiveServiceDetails[2], existingCognitiveServiceDetails[4]) +} + +var cognitiveService = useExistingService ? cognitiveServiceExisting : cognitiveServiceNew + +@description('The name of the cognitive services account.') +output name string = useExistingService ? cognitiveServiceExisting.name : cognitiveServiceNew.name + +@description('The resource ID of the cognitive services account.') +output resourceId string = useExistingService ? cognitiveServiceExisting.id : cognitiveServiceNew.id + +@description('The resource group the cognitive services account was deployed into.') +output subscriptionId string = useExistingService ? existingCognitiveServiceDetails[2] : subscription().subscriptionId + +@description('The resource group the cognitive services account was deployed into.') +output resourceGroupName string = useExistingService ? existingCognitiveServiceDetails[4] : resourceGroup().name + +@description('The service endpoint of the cognitive services account.') +output endpoint string = useExistingService ? cognitiveServiceExisting.properties.endpoint : cognitiveService.properties.endpoint + +@description('All endpoints available for the cognitive services account, types depends on the cognitive service kind.') +output endpoints endpointType = useExistingService ? cognitiveServiceExisting.properties.endpoints : cognitiveService.properties.endpoints + +@description('The principal ID of the system assigned identity.') +output systemAssignedMIPrincipalId string? = useExistingService ? cognitiveServiceExisting.identity.principalId : cognitiveService.?identity.?principalId + +@description('The location the resource was deployed into.') +output location string = useExistingService ? cognitiveServiceExisting.location : cognitiveService.location + +import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.') +output exportedSecrets secretsOutputType = useExistingService ? existing_cognigive_service_dependencies.outputs.exportedSecrets : cognigive_service_dependencies.outputs.exportedSecrets + +@description('The private endpoints of the congitive services account.') +output privateEndpoints privateEndpointOutputType[] = useExistingService ? existing_cognigive_service_dependencies.outputs.privateEndpoints : cognigive_service_dependencies.outputs.privateEndpoints + +import { aiProjectOutputType } from './modules/project.bicep' +output aiProjectInfo aiProjectOutputType = useExistingService ? existing_cognigive_service_dependencies.outputs.aiProjectInfo : cognigive_service_dependencies.outputs.aiProjectInfo + +// ================ // +// Definitions // +// ================ // + +@export() +@description('The type for the private endpoint output.') +type privateEndpointOutputType = { + @description('The name of the private endpoint.') + name: string + + @description('The resource ID of the private endpoint.') + resourceId: string + + @description('The group Id for the private endpoint Group.') + groupId: string? + + @description('The custom DNS configurations of the private endpoint.') + customDnsConfigs: { + @description('FQDN that resolves to private endpoint IP address.') + fqdn: string? + + @description('A list of private IP addresses of the private endpoint.') + ipAddresses: string[] + }[] + + @description('The IDs of the network interfaces associated with the private endpoint.') + networkInterfaceResourceIds: string[] +} + +@export() +@description('The type for a cognitive services account deployment.') +type deploymentType = { + @description('Optional. Specify the name of cognitive service account deployment.') + name: string? + + @description('Required. Properties of Cognitive Services account deployment model.') + model: { + @description('Required. The name of Cognitive Services account deployment model.') + name: string + + @description('Required. The format of Cognitive Services account deployment model.') + format: string + + @description('Required. The version of Cognitive Services account deployment model.') + version: string + } + + @description('Optional. The resource model definition representing SKU.') + sku: { + @description('Required. The name of the resource model definition representing SKU.') + name: string + + @description('Optional. The capacity of the resource model definition representing SKU.') + capacity: int? + + @description('Optional. The tier of the resource model definition representing SKU.') + tier: string? + + @description('Optional. The size of the resource model definition representing SKU.') + size: string? + + @description('Optional. The family of the resource model definition representing SKU.') + family: string? + }? + + @description('Optional. The name of RAI policy.') + raiPolicyName: string? + + @description('Optional. The version upgrade option.') + versionUpgradeOption: string? +} + +@export() +@description('The type for a cognitive services account endpoint.') +type endpointType = { + @description('Type of the endpoint.') + name: string? + @description('The endpoint URI.') + endpoint: string? +} + +@export() +@description('The type of the secrets exported to the provided Key Vault.') +type secretsExportConfigurationType = { + @description('Required. The key vault name where to store the keys and connection strings generated by the modules.') + keyVaultResourceId: string + + @description('Optional. The name for the accessKey1 secret to create.') + accessKey1Name: string? + + @description('Optional. The name for the accessKey2 secret to create.') + accessKey2Name: string? +} diff --git a/infra/modules/account/modules/dependencies.bicep b/infra/modules/account/modules/dependencies.bicep new file mode 100644 index 000000000..c2d7de6f8 --- /dev/null +++ b/infra/modules/account/modules/dependencies.bicep @@ -0,0 +1,479 @@ +@description('Required. The name of Cognitive Services account.') +param name string + +@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'C2' + 'C3' + 'C4' + 'F0' + 'F1' + 'S' + 'S0' + 'S1' + 'S10' + 'S2' + 'S3' + 'S4' + 'S5' + 'S6' + 'S7' + 'S8' + 'S9' +]) +param sku string = 'S0' + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +@description('Optional. Tags of the resource.') +param tags object? + +@description('Optional. Array of deployments about cognitive service accounts to create.') +param deployments deploymentType[]? + +@description('Optional. Key vault reference and secret settings for the module\'s secrets export.') +param secretsExportConfiguration secretsExportConfigurationType? + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') +param privateEndpoints privateEndpointSingleServiceType[]? + +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The lock settings of the service.') +param lock lockType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The diagnostic settings of the service.') +param diagnosticSettings diagnosticSettingFullType[]? + +@description('Optional: Name for the project which needs to be created.') +param projectName string + +@description('Optional: Description for the project which needs to be created.') +param projectDescription string + +@description('Optional: Provide the existing project resource id in case if it needs to be reused') +param azureExistingAIProjectResourceId string = '' + +var builtInRoleNames = { + 'Cognitive Services Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + ) + 'Cognitive Services Custom Vision Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3' + ) + 'Cognitive Services Custom Vision Deployment': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5c4089e1-6d96-4d2f-b296-c1bc7137275f' + ) + 'Cognitive Services Custom Vision Labeler': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '88424f51-ebe7-446f-bc41-7fa16989e96c' + ) + 'Cognitive Services Custom Vision Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '93586559-c37d-4a6b-ba08-b9f0940c2d73' + ) + 'Cognitive Services Custom Vision Trainer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b' + ) + 'Cognitive Services Data Reader (Preview)': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b59867f0-fa02-499b-be73-45a86b5b3e1c' + ) + 'Cognitive Services Face Recognizer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '9894cab4-e18a-44aa-828b-cb588cd6f2d7' + ) + 'Cognitive Services Immersive Reader User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b2de6794-95db-4659-8781-7e080d3f2b9d' + ) + 'Cognitive Services Language Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f07febfe-79bc-46b1-8b37-790e26e6e498' + ) + 'Cognitive Services Language Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7628b7b8-a8b2-4cdc-b46f-e9b35248918e' + ) + 'Cognitive Services Language Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8' + ) + 'Cognitive Services LUIS Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f72c8140-2111-481c-87ff-72b910f6e3f8' + ) + 'Cognitive Services LUIS Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18e81cdc-4e98-4e29-a639-e7d10c5a6226' + ) + 'Cognitive Services LUIS Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '6322a993-d5c9-4bed-b113-e49bbea25b27' + ) + 'Cognitive Services Metrics Advisor Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'cb43c632-a144-4ec5-977c-e80c4affc34a' + ) + 'Cognitive Services Metrics Advisor User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '3b20f47b-3825-43cb-8114-4bd2201156a8' + ) + 'Cognitive Services OpenAI Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a001fd3d-188f-4b5d-821b-7da978bf7442' + ) + 'Cognitive Services OpenAI User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + 'Cognitive Services QnA Maker Editor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f4cc2bf9-21be-47a1-bdf1-5c5804381025' + ) + 'Cognitive Services QnA Maker Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '466ccd10-b268-4a11-b098-b4849f024126' + ) + 'Cognitive Services Speech Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0e75ca1e-0464-4b4d-8b93-68208a576181' + ) + 'Cognitive Services Speech User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2dc8367-1007-4938-bd23-fe263f013447' + ) + 'Cognitive Services User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + 'Azure AI Developer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '64702f94-c441-49e6-a78b-ef80e0188fee' + ) + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'Role Based Access Control Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) +} + +var formattedRoleAssignments = [ + for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains( + roleAssignment.roleDefinitionIdOrName, + '/providers/Microsoft.Authorization/roleDefinitions/' + ) + ? roleAssignment.roleDefinitionIdOrName + : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName)) + }) +] + +var enableReferencedModulesTelemetry = false + +resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: name +} + +@batchSize(1) +resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = [ + for (deployment, index) in (deployments ?? []): { + parent: cognitiveService + name: deployment.?name ?? '${name}-deployments' + properties: { + model: deployment.model + raiPolicyName: deployment.?raiPolicyName + versionUpgradeOption: deployment.?versionUpgradeOption + } + sku: deployment.?sku ?? { + name: sku + capacity: sku.?capacity + tier: sku.?tier + size: sku.?size + family: sku.?family + } + } +] + +resource cognitiveService_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') { + name: lock.?name ?? 'lock-${name}' + properties: { + level: lock.?kind ?? '' + notes: lock.?kind == 'CanNotDelete' + ? 'Cannot delete resource or child resources.' + : 'Cannot delete or modify the resource or child resources.' + } + scope: cognitiveService +} + +resource cognitiveService_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [ + for (diagnosticSetting, index) in (diagnosticSettings ?? []): { + name: diagnosticSetting.?name ?? '${name}-diagnosticSettings' + properties: { + storageAccountId: diagnosticSetting.?storageAccountResourceId + workspaceId: diagnosticSetting.?workspaceResourceId + eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId + eventHubName: diagnosticSetting.?eventHubName + metrics: [ + for group in (diagnosticSetting.?metricCategories ?? [{ category: 'AllMetrics' }]): { + category: group.category + enabled: group.?enabled ?? true + timeGrain: null + } + ] + logs: [ + for group in (diagnosticSetting.?logCategoriesAndGroups ?? [{ categoryGroup: 'allLogs' }]): { + categoryGroup: group.?categoryGroup + category: group.?category + enabled: group.?enabled ?? true + } + ] + marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId + logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType + } + scope: cognitiveService + } +] + +module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [ + for (privateEndpoint, index) in (privateEndpoints ?? []): { + name: '${uniqueString(deployment().name, location)}-cognitiveService-PrivateEndpoint-${index}' + scope: resourceGroup( + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2], + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4] + ) + params: { + name: privateEndpoint.?name ?? 'pep-${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + privateLinkServiceConnections: privateEndpoint.?isManualConnection != true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + properties: { + privateLinkServiceId: cognitiveService.id + groupIds: [ + privateEndpoint.?service ?? 'account' + ] + } + } + ] + : null + manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + properties: { + privateLinkServiceId: cognitiveService.id + groupIds: [ + privateEndpoint.?service ?? 'account' + ] + requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.' + } + } + ] + : null + subnetResourceId: privateEndpoint.subnetResourceId + enableTelemetry: enableReferencedModulesTelemetry + location: privateEndpoint.?location ?? reference( + split(privateEndpoint.subnetResourceId, '/subnets/')[0], + '2020-06-01', + 'Full' + ).location + lock: privateEndpoint.?lock ?? lock + privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup + roleAssignments: privateEndpoint.?roleAssignments + tags: privateEndpoint.?tags ?? tags + customDnsConfigs: privateEndpoint.?customDnsConfigs + ipConfigurations: privateEndpoint.?ipConfigurations + applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds + customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName + } + } +] + +resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + name: roleAssignment.?name ?? guid(cognitiveService.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: cognitiveService + } +] + +module secretsExport './keyVaultExport.bicep' = if (secretsExportConfiguration != null) { + name: '${uniqueString(deployment().name, location)}-secrets-kv' + scope: resourceGroup( + split(secretsExportConfiguration.?keyVaultResourceId!, '/')[2], + split(secretsExportConfiguration.?keyVaultResourceId!, '/')[4] + ) + params: { + keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId!, '/')) + secretsToSet: union( + [], + contains(secretsExportConfiguration!, 'accessKey1Name') + ? [ + { + name: secretsExportConfiguration!.?accessKey1Name + value: cognitiveService.listKeys().key1 + } + ] + : [], + contains(secretsExportConfiguration!, 'accessKey2Name') + ? [ + { + name: secretsExportConfiguration!.?accessKey2Name + value: cognitiveService.listKeys().key2 + } + ] + : [] + ) + } +} + +module aiProject 'project.bicep' = if(!empty(projectName) || !empty(azureExistingAIProjectResourceId)) { + name: take('${name}-ai-project-${projectName}-deployment', 64) + params: { + name: projectName + desc: projectDescription + aiServicesName: cognitiveService.name + location: location + tags: tags + azureExistingAIProjectResourceId: azureExistingAIProjectResourceId + } +} + +import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.') +output exportedSecrets secretsOutputType = (secretsExportConfiguration != null) + ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret) + : {} + +@description('The private endpoints of the congitive services account.') +output privateEndpoints privateEndpointOutputType[] = [ + for (pe, index) in (privateEndpoints ?? []): { + name: cognitiveService_privateEndpoints[index].outputs.name + resourceId: cognitiveService_privateEndpoints[index].outputs.resourceId + groupId: cognitiveService_privateEndpoints[index].outputs.?groupId! + customDnsConfigs: cognitiveService_privateEndpoints[index].outputs.customDnsConfigs + networkInterfaceResourceIds: cognitiveService_privateEndpoints[index].outputs.networkInterfaceResourceIds + } +] + +import { aiProjectOutputType } from 'project.bicep' +output aiProjectInfo aiProjectOutputType = aiProject.outputs.aiProjectInfo + +// ================ // +// Definitions // +// ================ // + +@export() +@description('The type for the private endpoint output.') +type privateEndpointOutputType = { + @description('The name of the private endpoint.') + name: string + + @description('The resource ID of the private endpoint.') + resourceId: string + + @description('The group Id for the private endpoint Group.') + groupId: string? + + @description('The custom DNS configurations of the private endpoint.') + customDnsConfigs: { + @description('FQDN that resolves to private endpoint IP address.') + fqdn: string? + + @description('A list of private IP addresses of the private endpoint.') + ipAddresses: string[] + }[] + + @description('The IDs of the network interfaces associated with the private endpoint.') + networkInterfaceResourceIds: string[] +} + +@export() +@description('The type for a cognitive services account deployment.') +type deploymentType = { + @description('Optional. Specify the name of cognitive service account deployment.') + name: string? + + @description('Required. Properties of Cognitive Services account deployment model.') + model: { + @description('Required. The name of Cognitive Services account deployment model.') + name: string + + @description('Required. The format of Cognitive Services account deployment model.') + format: string + + @description('Required. The version of Cognitive Services account deployment model.') + version: string + } + + @description('Optional. The resource model definition representing SKU.') + sku: { + @description('Required. The name of the resource model definition representing SKU.') + name: string + + @description('Optional. The capacity of the resource model definition representing SKU.') + capacity: int? + + @description('Optional. The tier of the resource model definition representing SKU.') + tier: string? + + @description('Optional. The size of the resource model definition representing SKU.') + size: string? + + @description('Optional. The family of the resource model definition representing SKU.') + family: string? + }? + + @description('Optional. The name of RAI policy.') + raiPolicyName: string? + + @description('Optional. The version upgrade option.') + versionUpgradeOption: string? +} + +@export() +@description('The type for a cognitive services account endpoint.') +type endpointType = { + @description('Type of the endpoint.') + name: string? + @description('The endpoint URI.') + endpoint: string? +} + +@export() +@description('The type of the secrets exported to the provided Key Vault.') +type secretsExportConfigurationType = { + @description('Required. The key vault name where to store the keys and connection strings generated by the modules.') + keyVaultResourceId: string + + @description('Optional. The name for the accessKey1 secret to create.') + accessKey1Name: string? + + @description('Optional. The name for the accessKey2 secret to create.') + accessKey2Name: string? +} diff --git a/infra/modules/account/modules/keyVaultExport.bicep b/infra/modules/account/modules/keyVaultExport.bicep new file mode 100644 index 000000000..a54cc5576 --- /dev/null +++ b/infra/modules/account/modules/keyVaultExport.bicep @@ -0,0 +1,43 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. The name of the Key Vault to set the ecrets in.') +param keyVaultName string + +import { secretToSetType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Required. The secrets to set in the Key Vault.') +param secretsToSet secretToSetType[] + +// ============= // +// Resources // +// ============= // + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName +} + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [ + for secret in secretsToSet: { + name: secret.name + parent: keyVault + properties: { + value: secret.value + } + } +] + +// =========== // +// Outputs // +// =========== // + +import { secretSetOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('The references to the secrets exported to the provided Key Vault.') +output secretsSet secretSetOutputType[] = [ + #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value + for index in range(0, length(secretsToSet ?? [])): { + secretResourceId: secrets[index].id + secretUri: secrets[index].properties.secretUri + secretUriWithVersion: secrets[index].properties.secretUriWithVersion + } +] diff --git a/infra/modules/account/modules/project.bicep b/infra/modules/account/modules/project.bicep new file mode 100644 index 000000000..8ca346546 --- /dev/null +++ b/infra/modules/account/modules/project.bicep @@ -0,0 +1,61 @@ +@description('Required. Name of the AI Services project.') +param name string + +@description('Required. The location of the Project resource.') +param location string = resourceGroup().location + +@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.') +param desc string = name + +@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') +param aiServicesName string + +@description('Optional. Tags to be applied to the resources.') +param tags object = {} + +@description('Optional. Use this parameter to use an existing AI project resource ID from different resource group') +param azureExistingAIProjectResourceId string = '' + +// // Extract components from existing AI Project Resource ID if provided +var useExistingProject = !empty(azureExistingAIProjectResourceId) +var existingProjName = useExistingProject ? last(split(azureExistingAIProjectResourceId, '/')) : '' +var existingProjEndpoint = useExistingProject ? format('https://{0}.services.ai.azure.com/api/projects/{1}', aiServicesName, existingProjName) : '' +// Reference to cognitive service in current resource group for new projects +resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: aiServicesName +} + +// Create new AI project only if not reusing existing one +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = if(!useExistingProject) { + parent: cogServiceReference + name: name + tags: tags + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + description: desc + displayName: name + } +} + +@description('AI Project metadata including name, resource ID, and API endpoint.') +output aiProjectInfo aiProjectOutputType = { + name: useExistingProject ? existingProjName : aiProject.name + resourceId: useExistingProject ? azureExistingAIProjectResourceId : aiProject.id + apiEndpoint: useExistingProject ? existingProjEndpoint : aiProject.properties.endpoints['AI Foundry API'] +} + +@export() +@description('Output type representing AI project information.') +type aiProjectOutputType = { + @description('Required. Name of the AI project.') + name: string + + @description('Required. Resource ID of the AI project.') + resourceId: string + + @description('Required. API endpoint for the AI project.') + apiEndpoint: string +} diff --git a/infra/modules/role.bicep b/infra/modules/role.bicep new file mode 100644 index 000000000..70fec14c4 --- /dev/null +++ b/infra/modules/role.bicep @@ -0,0 +1,63 @@ +@description('The name of the role assignment resource. Typically generated using `guid()` for uniqueness.') +param name string + +@description('The ID of the role definition to assign. For example, a built-in role like "Cognitive Services User".') +param roleDefinitionId string + +@description('The object ID of the principal (user, group, or service principal) to whom the role will be assigned.') +param principalId string + +@description('The object ID of the user to be granted AI access (can be used for assigning multiple roles).') +param aiUserid string + +@description('The name of the existing Azure Cognitive Services account.') +param aiServiceName string + +resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServiceName +} + + +resource aiUserAccessProj 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'aiUserAccessProj') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: roleDefinitionId + principalId: principalId + } +} + +resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'aiUserAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: aiUserid + principalId: principalId + } +} + +resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '64702f94-c441-49e6-a78b-ef80e0188fee' +} + +resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'aiDeveloperAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: aiDeveloper.id + principalId: principalId + } +} + +resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' +} + +resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'cognitiveServiceOpenAIUserAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: cognitiveServiceOpenAIUser.id + principalId: principalId + } +} From d8a680652bc051d415f709c30e5e7d6524e90c8b Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Fri, 4 Jul 2025 17:52:32 +0530 Subject: [PATCH 2/2] Add AZURE_ENV_FOUNDRY_PROJECT_ID in CustomizingAzdParameters.md file --- docs/CustomizingAzdParameters.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 2dab381d9..0ac67244c 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -13,6 +13,7 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_OPENAI_LOCATION` | string | `swedencentral` | Specifies the region for OpenAI resource deployment. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | | `AZURE_ENV_MODEL_NAME` | string | `gpt-4o` | Specifies the name of the GPT model to be deployed. | +| `AZURE_ENV_FOUNDRY_PROJECT_ID` | string | `` | Set this if you want to reuse an AI Foundry Project instead of creating a new one. | | `AZURE_ENV_MODEL_VERSION` | string | `2024-08-06` | Version of the GPT model to be used for deployment. | | `AZURE_ENV_IMAGETAG` | string | `latest` | Docker image tag used for container deployments. | | `AZURE_ENV_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. |