Skip to content
27 changes: 27 additions & 0 deletions infra/core/ai/acr-role-assignment.bicep
Original file line number Diff line number Diff line change
@@ -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')
}
}
101 changes: 93 additions & 8 deletions infra/core/ai/ai-project.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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: {
Expand All @@ -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}'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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']
Expand All @@ -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 : ''
Expand Down
29 changes: 27 additions & 2 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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'
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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

18 changes: 18 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=}"
}
}
}