diff --git a/infra/core/ai/acr-role-assignment.bicep b/infra/core/ai/acr-role-assignment.bicep new file mode 100644 index 0000000..3e0c2b2 --- /dev/null +++ b/infra/core/ai/acr-role-assignment.bicep @@ -0,0 +1,27 @@ +targetScope = 'resourceGroup' + +@description('Name of the existing container registry') +param acrName string + +@description('Principal ID to grant AcrPull role') +param principalId string + +@description('Full resource ID of the ACR (for generating unique GUID)') +param acrResourceId string + +// Reference the existing ACR in this resource group +resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { + name: acrName +} + +// Grant AcrPull role to the AI project's managed identity +resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: acr + name: guid(acrResourceId, principalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + properties: { + principalId: principalId + principalType: 'ServicePrincipal' + // AcrPull role + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } +} diff --git a/infra/core/ai/ai-project.bicep b/infra/core/ai/ai-project.bicep index d0e8753..76ad057 100644 --- a/infra/core/ai/ai-project.bicep +++ b/infra/core/ai/ai-project.bicep @@ -34,12 +34,36 @@ param enableMonitoring bool = true @description('Enable hosted agent deployment') param enableHostedAgents bool = false +@description('Optional. Existing container registry resource ID. If provided, a connection will be created to this ACR instead of creating a new one.') +param existingContainerRegistryResourceId string = '' + +@description('Optional. Existing container registry login server (e.g., myregistry.azurecr.io). Required if existingContainerRegistryResourceId is provided.') +param existingContainerRegistryEndpoint string = '' + +@description('Optional. Name of an existing ACR connection on the Foundry project. If provided, no new ACR or connection will be created.') +param existingAcrConnectionName string = '' + +@description('Optional. Existing Application Insights connection string. If provided, a connection will be created but no new App Insights resource.') +param existingApplicationInsightsConnectionString string = '' + +@description('Optional. Existing Application Insights resource ID. Used for connection metadata when providing an existing App Insights.') +param existingApplicationInsightsResourceId string = '' + +@description('Optional. Name of an existing Application Insights connection on the Foundry project. If provided, no new App Insights or connection will be created.') +param existingAppInsightsConnectionName string = '' + // Load abbreviations var abbrs = loadJsonContent('../../abbreviations.json') // Determine which resources to create based on connections var hasStorageConnection = length(filter(additionalDependentResources, conn => conn.resource == 'storage')) > 0 var hasAcrConnection = length(filter(additionalDependentResources, conn => conn.resource == 'registry')) > 0 +var hasExistingAcr = !empty(existingContainerRegistryResourceId) +var hasExistingAcrConnection = !empty(existingAcrConnectionName) +var hasExistingAppInsightsConnection = !empty(existingAppInsightsConnectionName) +var hasExistingAppInsightsConnectionString = !empty(existingApplicationInsightsConnectionString) +// Only create new App Insights resources if monitoring enabled and no existing connection/connection string +var shouldCreateAppInsights = enableMonitoring && !hasExistingAppInsightsConnection && !hasExistingAppInsightsConnectionString var hasSearchConnection = length(filter(additionalDependentResources, conn => conn.resource == 'azure_ai_search')) > 0 var hasBingConnection = length(filter(additionalDependentResources, conn => conn.resource == 'bing_grounding')) > 0 var hasBingCustomConnection = length(filter(additionalDependentResources, conn => conn.resource == 'bing_custom_grounding')) > 0 @@ -52,7 +76,7 @@ var bingConnectionName = hasBingConnection ? filter(additionalDependentResources var bingCustomConnectionName = hasBingCustomConnection ? filter(additionalDependentResources, conn => conn.resource == 'bing_custom_grounding')[0].connectionName : '' // Enable monitoring via Log Analytics and Application Insights -module logAnalytics '../monitor/loganalytics.bicep' = if (enableMonitoring) { +module logAnalytics '../monitor/loganalytics.bicep' = if (shouldCreateAppInsights) { name: 'logAnalytics' params: { location: location @@ -61,7 +85,7 @@ module logAnalytics '../monitor/loganalytics.bicep' = if (enableMonitoring) { } } -module applicationInsights '../monitor/applicationinsights.bicep' = if (enableMonitoring) { +module applicationInsights '../monitor/applicationinsights.bicep' = if (shouldCreateAppInsights) { name: 'applicationInsights' params: { location: location @@ -134,8 +158,8 @@ resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } -// Create connection towards appinsights -resource appInsightConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = { +// Create connection towards appinsights - only if we created a new App Insights resource +resource appInsightConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = if (shouldCreateAppInsights) { parent: aiAccount::project name: 'appi-connection' properties: { @@ -153,6 +177,25 @@ resource appInsightConnection 'Microsoft.CognitiveServices/accounts/projects/con } } +// Create connection to existing App Insights - if user provided connection string but no existing connection +resource existingAppInsightConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = if (enableMonitoring && hasExistingAppInsightsConnectionString && !hasExistingAppInsightsConnection) { + parent: aiAccount::project + name: 'appi-connection' + properties: { + category: 'AppInsights' + target: existingApplicationInsightsResourceId + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: existingApplicationInsightsConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: existingApplicationInsightsResourceId + } + } +} + // Create additional connections from ai.yaml configuration module aiConnections './connection.bicep' = [for (connection, index) in connections: { name: 'connection-${connection.name}' @@ -233,6 +276,49 @@ module acr '../host/acr.bicep' = if (hasAcrConnection) { } } +// Connection for existing ACR - create if user provided an existing ACR resource ID but no existing connection +module existingAcrConnection './connection.bicep' = if (hasExistingAcr && !hasExistingAcrConnection) { + name: 'existing-acr-connection' + params: { + aiServicesAccountName: aiAccount.name + aiProjectName: aiAccount::project.name + connectionConfig: { + name: 'acr-connection' + category: 'ContainerRegistry' + target: existingContainerRegistryEndpoint + authType: 'ManagedIdentity' + credentials: { + clientId: aiAccount::project.identity.principalId + resourceId: existingContainerRegistryResourceId + } + isSharedToAll: true + metadata: { + ResourceId: existingContainerRegistryResourceId + } + } + } +} + +// Extract resource group name from the existing ACR resource ID +// Resource ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/{name} +var existingAcrResourceGroup = hasExistingAcr ? split(existingContainerRegistryResourceId, '/')[4] : '' +var existingAcrName = hasExistingAcr ? last(split(existingContainerRegistryResourceId, '/')) : '' + +// Grant AcrPull role to the AI project's managed identity on the existing ACR +// This allows the hosted agents to pull images from the user-provided registry +// Note: User must have permission to assign roles on the existing ACR (Owner or User Access Administrator) +// Using a module allows scoping to a different resource group if the ACR isn't in the same RG +// Skip if connection already exists (role assignment should already be in place) +module existingAcrRoleAssignment './acr-role-assignment.bicep' = if (hasExistingAcr && !hasExistingAcrConnection) { + name: 'existing-acr-role-assignment' + scope: resourceGroup(existingAcrResourceGroup) + params: { + acrName: existingAcrName + acrResourceId: existingContainerRegistryResourceId + principalId: aiAccount::project.identity.principalId + } +} + // Bing Search grounding module - deploy if Bing connection is defined in ai.yaml or parameter is enabled module bingGrounding '../search/bing_grounding.bicep' = if (hasBingConnection) { name: 'bing-grounding' @@ -274,7 +360,6 @@ module azureAiSearch '../search/azure_ai_search.bicep' = if (hasSearchConnection } } - // Outputs output AZURE_AI_PROJECT_ENDPOINT string = aiAccount::project.properties.endpoints['AI Foundry API'] output AZURE_OPENAI_ENDPOINT string = aiAccount.properties.endpoints['OpenAI Language Model Instance API'] @@ -285,14 +370,14 @@ output aiServicesAccountName string = aiAccount.name output aiServicesProjectName string = aiAccount::project.name output aiServicesPrincipalId string = aiAccount.identity.principalId output projectName string = aiAccount::project.name -output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString +output APPLICATIONINSIGHTS_CONNECTION_STRING string = shouldCreateAppInsights ? applicationInsights.outputs.connectionString : (hasExistingAppInsightsConnectionString ? existingApplicationInsightsConnectionString : '') // Grouped dependent resources outputs output dependentResources object = { registry: { name: hasAcrConnection ? acr!.outputs.containerRegistryName : '' - loginServer: hasAcrConnection ? acr!.outputs.containerRegistryLoginServer : '' - connectionName: hasAcrConnection ? acr!.outputs.containerRegistryConnectionName : '' + loginServer: hasAcrConnection ? acr!.outputs.containerRegistryLoginServer : ((hasExistingAcr || hasExistingAcrConnection) ? existingContainerRegistryEndpoint : '') + connectionName: hasAcrConnection ? acr!.outputs.containerRegistryConnectionName : (hasExistingAcrConnection ? existingAcrConnectionName : (hasExistingAcr ? 'acr-connection' : '')) } bing_grounding: { name: (hasBingConnection) ? bingGrounding!.outputs.bingGroundingName : '' diff --git a/infra/main.bicep b/infra/main.bicep index 3675047..66e7327 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -84,6 +84,24 @@ param enableHostedAgents bool @description('Enable monitoring for the AI project') param enableMonitoring bool = true +@description('Optional. Existing container registry resource ID. If provided, no new ACR will be created and a connection to this ACR will be established.') +param existingContainerRegistryResourceId string = '' + +@description('Optional. Existing container registry endpoint (login server). Required if existingContainerRegistryResourceId is provided.') +param existingContainerRegistryEndpoint string = '' + +@description('Optional. Name of an existing ACR connection on the Foundry project. If provided, no new ACR or connection will be created.') +param existingAcrConnectionName string = '' + +@description('Optional. Existing Application Insights connection string. If provided, a connection will be created but no new App Insights resource.') +param existingApplicationInsightsConnectionString string = '' + +@description('Optional. Existing Application Insights resource ID. Used for connection metadata when providing an existing App Insights.') +param existingApplicationInsightsResourceId string = '' + +@description('Optional. Name of an existing Application Insights connection on the Foundry project. If provided, no new App Insights or connection will be created.') +param existingAppInsightsConnectionName string = '' + // Tags that should be applied to all resources. // // Note that 'azd-service-name' tags should be applied separately to service host resources. @@ -102,8 +120,10 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { // Build dependent resources array conditionally // Check if ACR already exists in the user-provided array to avoid duplicates +// Also skip if user provided an existing container registry endpoint or connection name var hasAcr = contains(map(aiProjectDependentResources, r => r.resource), 'registry') -var dependentResources = (enableHostedAgents) && !hasAcr ? union(aiProjectDependentResources, [ +var shouldCreateAcr = enableHostedAgents && !hasAcr && empty(existingContainerRegistryResourceId) && empty(existingAcrConnectionName) +var dependentResources = shouldCreateAcr ? union(aiProjectDependentResources, [ { resource: 'registry' connectionName: 'acr-connection' @@ -126,6 +146,12 @@ module aiProject 'core/ai/ai-project.bicep' = { additionalDependentResources: dependentResources enableMonitoring: enableMonitoring enableHostedAgents: enableHostedAgents + existingContainerRegistryResourceId: existingContainerRegistryResourceId + existingContainerRegistryEndpoint: existingContainerRegistryEndpoint + existingAcrConnectionName: existingAcrConnectionName + existingApplicationInsightsConnectionString: existingApplicationInsightsConnectionString + existingApplicationInsightsResourceId: existingApplicationInsightsResourceId + existingAppInsightsConnectionName: existingAppInsightsConnectionName } } @@ -165,4 +191,3 @@ output AZURE_AI_SEARCH_SERVICE_NAME string = aiProject.outputs.dependentResource // Azure Storage output AZURE_STORAGE_CONNECTION_NAME string = aiProject.outputs.dependentResources.storage.connectionName output AZURE_STORAGE_ACCOUNT_NAME string = aiProject.outputs.dependentResources.storage.accountName - diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 323829e..926e0e2 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -40,6 +40,24 @@ }, "enableHostedAgents": { "value": "${ENABLE_HOSTED_AGENTS=false}" + }, + "existingContainerRegistryResourceId": { + "value": "${AZURE_CONTAINER_REGISTRY_RESOURCE_ID=}" + }, + "existingContainerRegistryEndpoint": { + "value": "${AZURE_CONTAINER_REGISTRY_ENDPOINT=}" + }, + "existingAcrConnectionName": { + "value": "${AZURE_AI_PROJECT_ACR_CONNECTION_NAME=}" + }, + "existingApplicationInsightsConnectionString": { + "value": "${APPLICATIONINSIGHTS_CONNECTION_STRING=}" + }, + "existingApplicationInsightsResourceId": { + "value": "${APPLICATIONINSIGHTS_RESOURCE_ID=}" + }, + "existingAppInsightsConnectionName": { + "value": "${APPLICATIONINSIGHTS_CONNECTION_NAME=}" } } }