diff --git a/infra/main.bicep b/infra/main.bicep index 548d49be..9bb56e69 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -95,6 +95,9 @@ param gptModelVersion string = '2024-08-06' param existingLogAnalyticsWorkspaceId string = '' +@description('Use this parameter to use an existing AI project resource ID') +param azureExistingAIProjectResourceId string = '' + var allTags = union( { 'azd-env-name': solutionName @@ -214,16 +217,15 @@ module network 'modules/network.bicep' = if (enablePrivateNetworking) { } module aiServices 'modules/ai-foundry/main.bicep' = { - name: take('aiservices-${resourcesName}-deployment', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [logAnalyticsWorkspace, network] // required due to optional flags that could change dependency + name: take('avm.res.cognitive-services.account.${resourcesName}', 64) params: { name: 'ais-${resourcesName}' location: aiDeploymentsLocation sku: 'S0' kind: 'AIServices' - deployments: [modelDeployment] - projectName: 'proj-${resourcesName}' + deployments: [ modelDeployment ] + projectName: 'aifp-${resourcesName}' + projectDescription: 'aifp-${resourcesName}' logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' privateNetworking: enablePrivateNetworking ? { @@ -231,6 +233,22 @@ module aiServices 'modules/ai-foundry/main.bicep' = { subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId } : null + existingFoundryProjectResourceId: azureExistingAIProjectResourceId + disableLocalAuth: true //Should be set to true for WAF aligned configuration + customSubDomainName: 'ais-${resourcesName}' + apiProperties: { + //staticsEnabled: false + } + allowProjectManagement: true + managedIdentities: { + systemAssigned: true + } + publicNetworkAccess: 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + } + privateEndpoints: [] roleAssignments: [ { principalId: appIdentity.outputs.principalId @@ -307,9 +325,9 @@ module keyVault 'modules/keyVault.bicep' = { : null roleAssignments: [ { - principalId: aiServices.outputs.?systemAssignedMIPrincipalId ?? '' + principalId: aiServices.outputs.?systemAssignedMIPrincipalId ?? appIdentity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Key Vault Reader' + roleDefinitionIdOrName: 'Key Vault Administrator' } ] tags: allTags @@ -466,15 +484,15 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = { } { name: 'AI_PROJECT_ENDPOINT' - value: aiServices.outputs.project.apiEndpoint // or equivalent + value: aiServices.outputs.aiProjectInfo.apiEndpoint // or equivalent } { name: 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING' // This was not really used in code. - value: aiServices.outputs.project.apiEndpoint + value: aiServices.outputs.aiProjectInfo.apiEndpoint } { name: 'AZURE_AI_AGENT_PROJECT_NAME' - value: aiServices.outputs.project.name + value: aiServices.outputs.aiProjectInfo.name } { name: 'AZURE_AI_AGENT_RESOURCE_GROUP_NAME' @@ -486,7 +504,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = { } { name: 'AZURE_AI_AGENT_ENDPOINT' - value: aiServices.outputs.project.apiEndpoint + value: aiServices.outputs.aiProjectInfo.apiEndpoint } { name: 'AZURE_CLIENT_ID' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index e1a4d73d..25e3ff00 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -26,6 +26,9 @@ "existingLogAnalyticsWorkspaceId": { "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" }, + "azureExistingAIProjectResourceId": { + "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + }, "secondaryLocation": { "value": "${AZURE_ENV_COSMOS_SECONDARY_LOCATION}" }, diff --git a/infra/modules/ai-foundry/ai-services.bicep b/infra/modules/ai-foundry/ai-services.bicep index bb601e0b..2786f880 100644 --- a/infra/modules/ai-foundry/ai-services.bicep +++ b/infra/modules/ai-foundry/ai-services.bicep @@ -60,6 +60,9 @@ param sku string = 'S0' @description('Optional. Location for all Resources.') param location string = resourceGroup().location +@description('Optional. Use this parameter to use an existing Cognitive Services resource ID from different resource group') +param azureExistingCognitiveServiceResourceId string = '' + 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[]? @@ -123,6 +126,13 @@ param managedIdentities managedIdentityAllType? @description('Optional. Array of deployments about cognitive service accounts to create.') param deployments deploymentType[]? +// Determine if we should reuse existing Cognitive Services resource +var useExistingCognitiveService = !empty(azureExistingCognitiveServiceResourceId) +var existingCogServiceName = useExistingCognitiveService ? last(split(azureExistingCognitiveServiceResourceId, '/')) : '' +var existingCogServiceRgName = useExistingCognitiveService ? split(azureExistingCognitiveServiceResourceId, '/')[4] : '' +var existingCogServiceSubscriptionId = useExistingCognitiveService ? split(azureExistingCognitiveServiceResourceId, '/')[2] : '' +var existingCogServiceEndpoint = useExistingCognitiveService ? format('https://{0}.cognitiveservices.azure.com/', existingCogServiceName) : '' + var enableReferencedModulesTelemetry = false var formattedUserAssignedIdentities = reduce( @@ -260,7 +270,14 @@ var formattedRoleAssignments = [ }) ] -resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = { +// Reference to existing Cognitive Services account +resource existingCognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (useExistingCognitiveService) { + name: existingCogServiceName + scope: resourceGroup(existingCogServiceSubscriptionId, existingCogServiceRgName) +} + +// Create new Cognitive Services account only if not reusing existing one +resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if (!useExistingCognitiveService) { name: name kind: kind identity: identity @@ -307,7 +324,7 @@ resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-previ @batchSize(1) resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = [ - for (deployment, index) in (deployments ?? []): { + for (deployment, index) in (useExistingCognitiveService ? [] : (deployments ?? [])): { parent: cognitiveService name: deployment.?name ?? '${name}-deployments' properties: { @@ -327,7 +344,7 @@ resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/depl #disable-next-line use-recent-api-versions resource cognitiveService_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [ - for (diagnosticSetting, index) in (diagnosticSettings ?? []): { + for (diagnosticSetting, index) in (useExistingCognitiveService ? [] : (diagnosticSettings ?? [])): { name: diagnosticSetting.?name ?? '${name}-diagnosticSettings' properties: { storageAccountId: diagnosticSetting.?storageAccountResourceId @@ -354,9 +371,9 @@ resource cognitiveService_diagnosticSettings 'Microsoft.Insights/diagnosticSetti scope: cognitiveService } ] - +// module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [ - for (privateEndpoint, index) in (privateEndpoints ?? []): { + for (privateEndpoint, index) in (useExistingCognitiveService ? [] : (privateEndpoints ?? [])): { name: take('${uniqueString(deployment().name, location)}-cognitiveService-PrivateEndpoint-${index}', 64) scope: resourceGroup( split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2], @@ -410,7 +427,7 @@ module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endp ] resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ - for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + for (roleAssignment, index) in (useExistingCognitiveService ? [] : (formattedRoleAssignments ?? [])): { name: roleAssignment.?name ?? guid(cognitiveService.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) properties: { roleDefinitionId: roleAssignment.roleDefinitionId @@ -425,26 +442,41 @@ resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignmen } ] +// Role assignments for existing Cognitive Services from different resource group +module existingCognitiveService_roleAssignments 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.2' = [ + for (roleAssignment, i) in (useExistingCognitiveService ? formattedRoleAssignments : []): { + name: 'existing-cog-role-${i}-${take(uniqueString(azureExistingCognitiveServiceResourceId, roleAssignment.roleDefinitionId, roleAssignment.principalId), 8)}' + scope: resourceGroup(existingCogServiceSubscriptionId, existingCogServiceRgName) + params: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + principalType: roleAssignment.?principalType ?? 'ServicePrincipal' + resourceId: azureExistingCognitiveServiceResourceId + enableTelemetry: enableReferencedModulesTelemetry + } + } +] + @description('The name of the cognitive services account.') -output name string = cognitiveService.name +output name string = useExistingCognitiveService ? existingCogServiceName : cognitiveService.name @description('The resource ID of the cognitive services account.') -output resourceId string = cognitiveService.id +output resourceId string = useExistingCognitiveService ? azureExistingCognitiveServiceResourceId : cognitiveService.id @description('The resource group the cognitive services account was deployed into.') -output resourceGroupName string = resourceGroup().name +output resourceGroupName string = useExistingCognitiveService ? existingCogServiceRgName : resourceGroup().name @description('The service endpoint of the cognitive services account.') -output endpoint string = cognitiveService.properties.endpoint +output endpoint string = useExistingCognitiveService ? existingCogServiceEndpoint : cognitiveService.properties.endpoint @description('All endpoints available for the cognitive services account, types depends on the cognitive service kind.') -output endpoints endpointType = cognitiveService.properties.endpoints +output endpoints endpointType = useExistingCognitiveService ? {} : cognitiveService.properties.endpoints @description('The principal ID of the system assigned identity.') -output systemAssignedMIPrincipalId string? = cognitiveService.?identity.?principalId +output systemAssignedMIPrincipalId string? = useExistingCognitiveService ? null : cognitiveService.?identity.?principalId @description('The location the resource was deployed into.') -output location string = cognitiveService.location +output location string = useExistingCognitiveService ? reference(azureExistingCognitiveServiceResourceId, '2025-04-01-preview', 'Full').location : cognitiveService.location @description('The private endpoints of the congitive services account.') output privateEndpoints privateEndpointOutputType[] = [ diff --git a/infra/modules/ai-foundry/dependencies.bicep b/infra/modules/ai-foundry/dependencies.bicep new file mode 100644 index 00000000..0042ad7b --- /dev/null +++ b/infra/modules/ai-foundry/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/ai-foundry/keyVaultExport.bicep b/infra/modules/ai-foundry/keyVaultExport.bicep new file mode 100644 index 00000000..a54cc557 --- /dev/null +++ b/infra/modules/ai-foundry/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/ai-foundry/main.bicep b/infra/modules/ai-foundry/main.bicep index 1058d2cc..895e14bf 100644 --- a/infra/modules/ai-foundry/main.bicep +++ b/infra/modules/ai-foundry/main.bicep @@ -1,13 +1,24 @@ -metadata name = 'AI Services and Project Module' -metadata description = 'This module creates an AI Services resource and an AI Foundry project within it. It supports private networking, OpenAI deployments, and role assignments.' +metadata name = 'Cognitive Services' +metadata description = 'This module deploys a Cognitive Service.' -@description('Required. Name of the Cognitive Services resource. Must be unique in the resource group.') +@description('Required. The name of Cognitive Services account.') param name string -@description('Optional. The location of the Cognitive Services resource.') -param location string // this should be passed +@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. The resource ID of the Log Analytics workspace to use for diagnostic settings.') +param logAnalyticsWorkspaceResourceId string? + +@description('Optional. Values to establish private networking for the AI Services resource.') +param privateNetworking aiServicesPrivateNetworkingType? + +param existingFoundryProjectResourceId string = '' -@description('Optional. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@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' @@ -34,13 +45,19 @@ param location string // this should be passed 'TextAnalytics' 'TextTranslation' ]) -param kind string = 'AIServices' +param kind string -@description('Optional. The SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@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' @@ -48,36 +65,147 @@ param kind string = 'AIServices' 'S6' 'S7' 'S8' + 'S9' ]) param sku string = 'S0' -@description('Required. The name of the AI Foundry project to create.') -param projectName string +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location -@description('Optional. The description of the AI Foundry project to create.') -param projectDescription string = projectName +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. The resource ID of the Log Analytics workspace to use for diagnostic settings.') -param logAnalyticsWorkspaceResourceId string? +@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? -import { deploymentType } from 'br/public:avm/res/cognitive-services/account:0.10.2' -@description('Optional. Specifies the OpenAI deployments to create.') -param deployments deploymentType[] = [] +@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[] = [] +param roleAssignments roleAssignmentType[]? -@description('Optional. Values to establish private networking for the AI Services resource.') -param privateNetworking aiServicesPrivateNetworkingType? +@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 -@description('Optional. Tags to be applied to the resources.') -param tags object = {} +@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 -module cognitiveServicesPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId)) { +@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) + +module cognitiveServicesPrivateDnsZone '../privateDnsZone.bicep' = if (!useExistingService && privateNetworking != null && empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId)) { name: take('${name}-cognitiveservices-pdns-deployment', 64) params: { name: 'privatelink.cognitiveservices.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}' @@ -86,7 +214,7 @@ module cognitiveServicesPrivateDnsZone '../privateDnsZone.bicep' = if (privateNe } } -module openAiPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?openAIPrivateDnsZoneResourceId)) { +module openAiPrivateDnsZone '../privateDnsZone.bicep' = if (!useExistingService && privateNetworking != null && empty(privateNetworking.?openAIPrivateDnsZoneResourceId)) { name: take('${name}-openai-pdns-deployment', 64) params: { name: 'privatelink.openai.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}' @@ -95,7 +223,7 @@ module openAiPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != } } -module aiServicesPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?aiServicesPrivateDnsZoneResourceId)) { +module aiServicesPrivateDnsZone '../privateDnsZone.bicep' = if (!useExistingService && privateNetworking != null && empty(privateNetworking.?aiServicesPrivateDnsZoneResourceId)) { name: take('${name}-ai-services-pdns-deployment', 64) params: { name: 'privatelink.services.ai.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}' @@ -121,29 +249,68 @@ var aiServicesPrivateDnsZoneResourceId = privateNetworking != null : privateNetworking.?aiServicesPrivateDnsZoneResourceId) : '' -module cognitiveService 'ai-services.bicep' = { - name: take('${name}-aiservices-deployment', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [cognitiveServicesPrivateDnsZone, openAiPrivateDnsZone, aiServicesPrivateDnsZone] // required due to optional flags that could change dependency +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: true // allows project management for Cognitive Services accounts in AI Foundry - FDP updates + customSubDomainName: name + 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 cognitive_service_dependencies './dependencies.bicep' = if(!useExistingService) { + name: take('${name}-cognitive-service-${cognitiveServiceNew.name}-dependencies', 64) params: { - name: name + projectName: projectName + projectDescription: projectDescription + name: cognitiveServiceNew.name location: location - tags: tags - sku: sku - kind: kind - managedIdentities: { - systemAssigned: true - } deployments: deployments - customSubDomainName: name - disableLocalAuth: false - publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled' - // rules to allow firewall and virtual network access - networkAcls: { - defaultAction: 'Allow' - virtualNetworkRules: [] - ipRules: [] - } diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [ { @@ -151,8 +318,8 @@ module cognitiveService 'ai-services.bicep' = { } ] : [] - roleAssignments: roleAssignments - privateEndpoints: privateNetworking != null + lock: lock + privateEndpoints: privateNetworking != null ? [ { name:'pep-${name}-aiservices' // private endpoint name @@ -174,60 +341,161 @@ module cognitiveService 'ai-services.bicep' = { } ] : [] + roleAssignments: roleAssignments + secretsExportConfiguration: secretsExportConfiguration + sku: sku + tags: tags } } - -module aiProject 'project.bicep' = { - name: take('${name}-ai-project-${projectName}-deployment', 64) +module existing_cognitive_service_dependencies './dependencies.bicep' = if(useExistingService) { + name: take('existing-${name}-cognitive-service-${cognitiveServiceExisting.name}-dependencies', 64) params: { - name: projectName - desc: projectDescription - aiServicesName: cognitiveService.outputs.name + 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 - enableTelemetry: enableTelemetry } + 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_cognitive_service_dependencies.outputs.exportedSecrets : cognitive_service_dependencies.outputs.exportedSecrets + +@description('The private endpoints of the congitive services account.') +output privateEndpoints privateEndpointOutputType[] = useExistingService ? existing_cognitive_service_dependencies.outputs.privateEndpoints : cognitive_service_dependencies.outputs.privateEndpoints + +import { aiProjectOutputType } from './project.bicep' +output aiProjectInfo aiProjectOutputType = useExistingService ? existing_cognitive_service_dependencies.outputs.aiProjectInfo : cognitive_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[] } -@description('The resource group the resources were deployed into.') -output resourceGroupName string = resourceGroup().name +@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('Name of the Cognitive Services resource.') -output name string = cognitiveService.outputs.name + @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('Resource ID of the Cognitive Services resource.') -output resourceId string = cognitiveService.outputs.resourceId + @description('Optional. The tier of the resource model definition representing SKU.') + tier: string? -@description('Principal ID of the system assigned managed identity for the Cognitive Services resource. This is only available if the resource has a system assigned managed identity.') -output systemAssignedMIPrincipalId string? = cognitiveService.outputs.?systemAssignedMIPrincipalId + @description('Optional. The size of the resource model definition representing SKU.') + size: string? -@description('The endpoint of the Cognitive Services resource.') -output endpoint string = cognitiveService.outputs.endpoint + @description('Optional. The family of the resource model definition representing SKU.') + family: string? + }? -import { aiProjectOutputType } from 'project.bicep' -@description('AI Foundry Project information.') -output project aiProjectOutputType = { - name: aiProject.name - resourceId: aiProject.outputs.resourceId - apiEndpoint: aiProject.outputs.apiEndpoint + @description('Optional. The name of RAI policy.') + raiPolicyName: string? + + @description('Optional. The version upgrade option.') + versionUpgradeOption: string? } @export() -@description('A custom AVM-aligned type for a role assignment for AI Services and Project.') -type aiServicesRoleAssignmentType = { - @description('Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated.') +@description('The type for a cognitive services account endpoint.') +type endpointType = { + @description('Type of the endpoint.') name: string? + @description('The endpoint URI.') + endpoint: string? +} - @description('Required. The role to assign. You can provide either the role definition GUID or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.') - roleDefinitionId: 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('Required. The principal ID of the principal (user/group/identity) to assign the role to.') - principalId: string + @description('Optional. The name for the accessKey1 secret to create.') + accessKey1Name: string? - @description('Optional. The principal type of the assigned principal ID.') - principalType: ('ServicePrincipal' | 'Group' | 'User' | 'ForeignGroup' | 'Device')? + @description('Optional. The name for the accessKey2 secret to create.') + accessKey2Name: string? } @export() diff --git a/infra/modules/ai-foundry/project.bicep b/infra/modules/ai-foundry/project.bicep index 17b4475f..0e1a6c6f 100644 --- a/infra/modules/ai-foundry/project.bicep +++ b/infra/modules/ai-foundry/project.bicep @@ -10,52 +10,24 @@ param desc string = name @description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') param aiServicesName string -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 to be applied to the resources.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// using a few built-in roles here that makes sense for Foundry projects only -var builtInRoleNames = { - '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' - ) - 'Azure AI Developer': subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '64702f94-c441-49e6-a78b-ef80e0188fee' - ) - 'Azure AI User': subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '53ca6127-db72-4b80-b1b0-d745d6d5456d' - ) -} +@description('Optional. Use this parameter to use an existing AI project resource ID from different resource group') +param azureExistingAIProjectResourceId string = '' -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)) - }) -] +// // 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 } -resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { +// 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 @@ -69,27 +41,12 @@ resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-pre } } -module aiProjectRoleAssignement 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.2' = [ - for (roleAssignment, i) in formattedRoleAssignments: { - name: 'avm.ptn.authorization.resource-role-assignment.${uniqueString(name, roleAssignment.roleDefinitionId, roleAssignment.principalId)}' - params: { - roleDefinitionId: roleAssignment.roleDefinitionId - principalId: roleAssignment.principalId - principalType: 'ServicePrincipal' - resourceId: aiProject.id - enableTelemetry: enableTelemetry - } - } -] - -@description('Name of the AI Foundry project.') -output name string = aiProject.name - -@description('Resource ID of the AI Foundry project.') -output resourceId string = aiProject.id - -@description('API endpoint for the AI Foundry project.') -output apiEndpoint string = aiProject.properties.endpoints['AI Foundry API'] +@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.')