Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ param azureCosmosDBAccountResourceId string = ''
@description('The API Management Service full ARM Resource ID. This is an optional field for existing API Management services.')
param apiManagementResourceId string = ''

@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet and a managed network outbound rule.')
param enableContainerRegistry bool = true

@description('Optional developer IP CIDR to allowlist for ACR push access (e.g., 203.0.113.0/26 or 10.0.0.0/16). When empty, public access remains disabled.')
param developerIpCidr string = ''

//New Param for resource group of Private DNS zones
//@description('Optional: Resource group containing existing private DNS zones. If specified, DNS zones will not be created.')
//param existingDnsZonesResourceGroup string = ''
Expand All @@ -79,7 +85,8 @@ param existingDnsZones object = {
'privatelink.search.windows.net': ''
'privatelink.blob.core.windows.net': ''
'privatelink.documents.azure.com': ''
'privatelink.azure-api.net': ''
'privatelink.azure-api.net': ''
'privatelink.azurecr.io': ''
}

@description('Zone Names for Validation of existing Private Dns Zones')
Expand All @@ -91,13 +98,15 @@ param dnsZoneNames array = [
'privatelink.blob.core.windows.net'
'privatelink.documents.azure.com'
'privatelink.azure-api.net'
'privatelink.azurecr.io'
]


var projectName = toLower('${firstProjectName}${uniqueSuffix}')
var cosmosDBName = toLower('${aiServices}${uniqueSuffix}cosmosdb')
var aiSearchName = toLower('${aiServices}${uniqueSuffix}search')
var azureStorageName = toLower('${aiServices}${uniqueSuffix}storage')
var acrName = toLower('acr${uniqueSuffix}')

// Check if existing resources have been passed in
var storagePassedIn = azureStorageAccountResourceId != ''
Expand Down Expand Up @@ -244,6 +253,28 @@ module networkApproverRoleSearch 'modules-network-secured/network-connection-app
}
}

// Contributor role on the ACR for managed PE connection approval
// The "Network Connection Approver" role does not include Microsoft.ContainerRegistry actions,
// so the AI account identity needs Contributor on the ACR to approve the managed VNet outbound PE connection
var contributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'

resource acrForRoleAssignment 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = if (enableContainerRegistry) {
name: acrName
}

resource acrContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableContainerRegistry) {
name: guid(acrName, accountName, contributorRoleId)
scope: acrForRoleAssignment
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', contributorRoleId)
principalId: aiAccount.outputs.accountPrincipalId
principalType: 'ServicePrincipal'
}
dependsOn: [
acr
]
}

// Azure Monitor Private Link Scope (AMPLS) for Application Insights telemetry
// This enables hosted agents to export traces/telemetry to App Insights via private network
module ampls 'modules-network-secured/azure-monitor-private-link.bicep' = {
Expand All @@ -258,6 +289,22 @@ module ampls 'modules-network-secured/azure-monitor-private-link.bicep' = {
}
}

// Optional: Azure Container Registry with Private Endpoint
// Creates an ACR accessible only via private endpoint (not publicly accessible)
// Also adds a managed network outbound rule so hosted agents can pull images
module acr 'modules-network-secured/container-registry.bicep' = if (enableContainerRegistry) {
name: 'acr-${uniqueSuffix}-deployment'
params: {
acrName: acrName
location: location
peSubnetId: vnet.outputs.peSubnetId
vnetId: vnet.outputs.virtualNetworkId
suffix: uniqueSuffix
existingDnsZoneResourceGroup: existingDnsZones['privatelink.azurecr.io']
developerIpCidr: developerIpCidr
}
}

// Configure Managed Network for AI Services Account
// This module sets up the managed virtual network and outbound PE rules to allow
// secure communication from hosted agents to customer resources (Storage, AI Search, Cosmos DB)
Expand All @@ -270,12 +317,14 @@ module managedNetwork 'modules-network-secured/managed-network.bicep' = {
cosmosDBResourceId: cosmosDB.id
aiSearchResourceId: aiSearch.id
amplsResourceId: ampls.outputs.amplsResourceId
acrResourceId: enableContainerRegistry ? acr.outputs.acrId : ''
}
dependsOn: [
aiDependencies // Ensure dependent resources (Storage, CosmosDB, Search) are fully created
networkApproverRoleStorage
networkApproverRoleCosmos
networkApproverRoleSearch
acrContributorRoleAssignment
ampls // Ensure AMPLS is created before adding the outbound rule
]
}
Expand Down Expand Up @@ -460,3 +509,20 @@ dependsOn: [
storageContainersRoleAssignment
]
}

// ---- AcrPull Role Assignment ----
// Grants the project managed identity pull access to the ACR (assigned after project is created)
var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull built-in role

resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableContainerRegistry) {
name: guid(acrForRoleAssignment.id, acrPullRoleId, resourceGroup().id)
scope: acrForRoleAssignment
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleId)
principalId: aiProject.outputs.projectPrincipalId
principalType: 'ServicePrincipal'
}
dependsOn: [
acr
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
Azure Container Registry with Private Endpoint Module
------------------------------------------------------
This module creates an Azure Container Registry (Premium SKU) with:
1. Private Endpoint in the specified PE subnet
2. Private DNS Zone (privatelink.azurecr.io) — created or referenced from existing
3. VNet link for the DNS zone
4. DNS Zone Group for the Private Endpoint

Prerequisites:
- Premium SKU is required for Private Endpoint support
- The PE subnet must already exist
*/

@description('Name of the Azure Container Registry')
param acrName string

@description('Azure region for the ACR')
param location string

@description('Resource ID of the Private Endpoint subnet')
param peSubnetId string

@description('Resource ID of the Virtual Network')
param vnetId string

@description('Suffix for unique resource names')
param suffix string

@description('Resource group name for existing ACR DNS zone. Empty string means create a new zone.')
param existingDnsZoneResourceGroup string = ''

@description('Subscription ID where existing private DNS zones are located.')
param dnsZonesSubscriptionId string = subscription().subscriptionId

@description('Optional developer IP CIDR to allowlist for ACR push access (e.g., 203.0.113.0/26 or 10.0.0.0/16). When empty, public access remains disabled.')
param developerIpCidr string = ''

@description('Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created.')
param projectPrincipalId string = ''

// ---- ACR Resource ----
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
name: acrName
location: location
sku: {
name: 'Premium'
}
properties: {
adminUserEnabled: false
publicNetworkAccess: empty(developerIpCidr) ? 'Disabled' : 'Enabled'
networkRuleBypassOptions: 'AzureServices'
networkRuleSet: empty(developerIpCidr) ? null : {
defaultAction: 'Deny'
ipRules: [
{
action: 'Allow'
value: developerIpCidr
}
]
}
}
}

// ---- Private Endpoint ----
resource acrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = {
name: '${acrName}-private-endpoint'
location: location
properties: {
subnet: { id: peSubnetId }
privateLinkServiceConnections: [
{
name: '${acrName}-private-link-service-connection'
properties: {
privateLinkServiceId: containerRegistry.id
groupIds: [ 'registry' ]
}
}
]
}
}

// ---- Private DNS Zone ----
var acrDnsZoneName = 'privatelink.azurecr.io'

resource acrPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(existingDnsZoneResourceGroup)) {
name: acrDnsZoneName
location: 'global'
}

resource existingAcrPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(existingDnsZoneResourceGroup)) {
name: acrDnsZoneName
scope: resourceGroup(dnsZonesSubscriptionId, existingDnsZoneResourceGroup)
}

var acrDnsZoneId = empty(existingDnsZoneResourceGroup) ? acrPrivateDnsZone.id : existingAcrPrivateDnsZone.id

// ---- VNet Link ----
resource acrDnsVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(existingDnsZoneResourceGroup)) {
parent: acrPrivateDnsZone
location: 'global'
name: 'acr-${suffix}-link'
properties: {
virtualNetwork: { id: vnetId }
registrationEnabled: false
}
}

// ---- DNS Zone Group ----
resource acrDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
parent: acrPrivateEndpoint
name: '${acrName}-dns-group'
properties: {
privateDnsZoneConfigs: [
{ name: '${acrName}-dns-config', properties: { privateDnsZoneId: acrDnsZoneId } }
]
}
dependsOn: [
empty(existingDnsZoneResourceGroup) ? acrDnsVnetLink : null
]
}

// ---- AcrPull Role Assignment ----
// Grants the project managed identity pull access to the ACR
var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull built-in role

resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(projectPrincipalId)) {
name: guid(containerRegistry.id, projectPrincipalId, acrPullRoleId)
scope: containerRegistry
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleId)
principalId: projectPrincipalId
principalType: 'ServicePrincipal'
}
}

// ---- Outputs ----
@description('Resource ID of the Azure Container Registry')
output acrId string = containerRegistry.id

@description('Name of the Azure Container Registry')
output acrName string = containerRegistry.name

@description('Login server URL of the Azure Container Registry')
output acrLoginServer string = containerRegistry.properties.loginServer
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ param aiSearchResourceId string
@description('Resource ID of the Azure Monitor Private Link Scope for telemetry')
param amplsResourceId string

@description('Resource ID of the Azure Container Registry for outbound PE rule. When empty, no ACR outbound rule is created.')
param acrResourceId string = ''

// Reference the existing AI Services account in the same resource group
resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
name: accountName
Expand Down Expand Up @@ -120,4 +123,21 @@ resource amplsOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks
dependsOn: [aiSearchOutboundRule]
}

// Outbound PE rule for Azure Container Registry
// This allows the hosted agent to pull container images from the private ACR
#disable-next-line BCP081
resource acrOutboundRule 'Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview' = if (!empty(acrResourceId)) {
parent: managedNetwork
name: 'acr-registry-rule'
properties: {
type: 'PrivateEndpoint'
destination: {
serviceResourceId: acrResourceId
subresourceTarget: 'registry'
}
category: 'UserDefined'
}
dependsOn: [amplsOutboundRule]
}

output managedNetworkSettingsName string = managedNetwork.name
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# `agent-framework[foundry]` is required because main.py imports
# `from agent_framework.foundry import FoundryChatClient`. In 1.3+ the
# foundry submodule is an optional extra; installing the bare package
# leaves agent_framework.foundry unimportable and the container crashes
# at startup so /readiness never returns 200 -> 424 session_not_ready.
agent-framework[foundry]>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
httpx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
agent-framework>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
agent-framework
agent-framework-foundry
agent-framework-foundry-hosting
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
agent-framework>=1.2.2
agent-framework-foundry-hosting
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
agent-framework>=1.2.2
agent-framework-foundry-hosting
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
agent-framework>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp>=1.24.0,<2
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# `agent-framework[foundry]` is required because main.py imports
# `from agent_framework.foundry import FoundryChatClient`. In 1.3+ the
# foundry submodule is an optional extra; installing the bare package
# leaves agent_framework.foundry unimportable and the container crashes
# at startup so /readiness never returns 200 -> 424 session_not_ready.
agent-framework[foundry]>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
agent-framework>=1.2.2
agent-framework-foundry-hosting
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
agent-framework
agent-framework-foundry-hosting
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
agent-framework>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# `agent-framework[foundry]` is required because main.py imports
# `from agent_framework.foundry import FoundryChatClient`. In 1.3+ the
# foundry submodule is an optional extra; installing the bare package
# leaves agent_framework.foundry unimportable and the container crashes
# at startup so /readiness never returns 200 -> 424 session_not_ready.
agent-framework[foundry]>=1.2.2
# Use the narrow Foundry subpackages to keep dependencies light.
agent-framework-foundry
agent-framework-foundry-hosting
mcp<2,>=1.24.0
Loading
Loading