From 07e148ffc49e3eb354863aabe85b3ab4ce4080e4 Mon Sep 17 00:00:00 2001 From: Karthik Saligrama Date: Wed, 10 Jun 2026 19:31:02 -0700 Subject: [PATCH 1/2] feat: add optional ACR with Private Endpoint to Bicep VNet scenarios (#519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add optional ACR with Private Endpoint to all Bicep VNet scenarios Add an enableContainerRegistry parameter (bool, default false) to all 8 Bicep VNet infrastructure scenarios. When enabled, creates: - Azure Container Registry (Premium SKU, no public access, no admin user) - Private Endpoint in the PE subnet (subresource: registry) - Private DNS Zone (privatelink.azurecr.io) with VNet link - DNS Zone Group for the Private Endpoint Implemented as a shared reusable module (container-registry.bicep) in each scenario's modules-network-secured/ directory. Scenarios updated: - 10-private-network-basic - 11-private-network-basic-vnet - 15-private-network-standard-agent-setup - 15a-private-network-evaluation-only-setup - 16-private-network-standard-agent-apim-setup - 17-private-network-standard-user-assigned-identity-agent-setup - 18-managed-virtual-network - 19-private-network-agent-tools Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove ACR from scenarios without network injection (10, 18) The optional ACR with Private Endpoint should only be available in scenarios that have network injection (agent VNet integration), not in PE-only or managed-network scenarios. Removed from: - 10-private-network-basic (PE-only, no agent subnet) - 18-managed-virtual-network (managed network, no agent subnet injection) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: default enableContainerRegistry to true for network injection scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add canadacentral to allowed locations in scenario 11 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add optional developer IP CIDR allowlist for ACR push access Adds a developerIpCidr parameter (string, default empty) to the ACR module. When provided, enables public network access on the ACR with a network rule that only allows the specified CIDR (e.g., /26). When empty, public access remains fully disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: clarify developerIpCidr accepts any CIDR size Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make uniqueSuffix deterministic for idempotent re-deploys (scenario 11) Remove utcNow() from the uniqueSuffix calculation so that re-deploying to the same resource group produces the same resource names. This makes deployments idempotent (patch-on-existing) rather than creating new resources on each run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make all VNet templates idempotent by removing utcNow from suffix Remove deploymentTimestamp/utcNow() from uniqueSuffix calculation in all templates. The suffix is now derived solely from resourceGroup().id, making deployments idempotent — re-deploying to the same RG updates existing resources in-place rather than creating duplicates. Affected templates: 15, 15a, 16, 17, 18, 19 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: grant AcrPull role to project managed identity on ACR Adds an AcrPull role assignment in the container-registry module so the project's managed identity can pull images from the ACR. The principal ID is passed from each scenario's main.bicep (system-assigned identity for most, user-assigned for scenario 17). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: rebuild compiled JSON templates to include canadacentral Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add canadacentral to allowed locations in scenario 19 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add canadacentral to allowed locations in scenario 15 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Karthik Saligrama Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azuredeploy.json | 327 ++++++++++++++++-- .../11-private-network-basic-vnet/main.bicep | 40 ++- .../11-private-network-basic-vnet/main.json | 327 ++++++++++++++++-- .../container-registry.bicep | 145 ++++++++ .../main.bicep | 40 ++- .../container-registry.bicep | 145 ++++++++ .../main.bicep | 32 +- .../container-registry.bicep | 145 ++++++++ .../main.bicep | 46 ++- .../container-registry.bicep | 145 ++++++++ .../main.bicep | 36 +- .../container-registry.bicep | 145 ++++++++ .../18-managed-virtual-network/main.bicep | 4 +- .../19-private-network-agent-tools/main.bicep | 32 +- .../container-registry.bicep | 145 ++++++++ 15 files changed, 1685 insertions(+), 69 deletions(-) create mode 100644 infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/container-registry.bicep create mode 100644 infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/container-registry.bicep create mode 100644 infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/container-registry.bicep create mode 100644 infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/container-registry.bicep create mode 100644 infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/container-registry.bicep create mode 100644 infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/container-registry.bicep diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json index b744d76c4..479d4be92 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "7766740854543235375" + "version": "0.43.8.12551", + "templateHash": "1439992269587931650" } }, "parameters": { @@ -28,6 +28,7 @@ "australiaeast", "swedencentral", "canadaeast", + "canadacentral", "westeurope", "westus3", "uksouth", @@ -83,10 +84,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -157,6 +154,20 @@ "description": "Address prefix for the private endpoint subnet" } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, "dnsZonesSubscriptionId": { "type": "string", "defaultValue": "", @@ -169,12 +180,25 @@ "defaultValue": { "privatelink.services.ai.azure.com": "", "privatelink.openai.azure.com": "", - "privatelink.cognitiveservices.azure.com": "" + "privatelink.cognitiveservices.azure.com": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" } }, + "dnsZoneNames": { + "type": "array", + "defaultValue": [ + "privatelink.services.ai.azure.com", + "privatelink.openai.azure.com", + "privatelink.cognitiveservices.azure.com", + "privatelink.azurecr.io" + ], + "metadata": { + "description": "Zone Names for Validation of existing Private Dns Zones" + } + }, "projectCapHost": { "type": "string", "defaultValue": "caphostproj", @@ -184,8 +208,9 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]", "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]", @@ -260,8 +285,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "17355345610868234879" + "version": "0.43.8.12551", + "templateHash": "13272353057643143267" } }, "parameters": { @@ -374,8 +399,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "10901913041029806810" + "version": "0.43.8.12551", + "templateHash": "6615847355060514198" } }, "parameters": { @@ -547,8 +572,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "10509554109014084911" + "version": "0.43.8.12551", + "templateHash": "16683796064862859220" } }, "parameters": { @@ -637,8 +662,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13091507001124186389" + "version": "0.43.8.12551", + "templateHash": "11349014942847443024" } }, "parameters": { @@ -720,8 +745,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13091507001124186389" + "version": "0.43.8.12551", + "templateHash": "11349014942847443024" } }, "parameters": { @@ -773,7 +798,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]" + ] } ], "outputs": { @@ -787,11 +815,11 @@ }, "agentSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "peSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "virtualNetworkName": { "type": "string", @@ -892,8 +920,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "14040588815014793423" + "version": "0.43.8.12551", + "templateHash": "1400943577065869287" } }, "parameters": { @@ -1038,8 +1066,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "9941350741815038708" + "version": "0.43.8.12551", + "templateHash": "17632575172686207895" } }, "parameters": { @@ -1246,6 +1274,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName')), '2025-04-01-preview', 'full').identity.principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.43.8.12551", + "templateHash": "8801411756251170105" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName'))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1272,8 +1545,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "4929793693928679874" + "version": "0.43.8.12551", + "templateHash": "6174217467753452418" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep index 3ee3085c2..03fb6c997 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep @@ -30,6 +30,7 @@ are used instead. 'australiaeast' 'swedencentral' 'canadaeast' + 'canadacentral' 'westeurope' 'westus3' 'uksouth' @@ -59,9 +60,10 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Uses only resourceGroup().id for determinism — re-deploys target the same resources +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') +var acrName = toLower('acr${uniqueSuffix}') @description('Name for your project resource.') param firstProjectName string = 'project' @@ -94,6 +96,12 @@ param agentSubnetPrefix string = '' @description('Address prefix for the private endpoint subnet') param peSubnetPrefix string = '' +@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet.') +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 = '' + // DNS zone parameters @description('Subscription ID where existing private DNS zones are located. Leave empty to use current subscription.') param dnsZonesSubscriptionId string = '' @@ -103,8 +111,17 @@ param existingDnsZones object = { 'privatelink.services.ai.azure.com': '' 'privatelink.openai.azure.com': '' 'privatelink.cognitiveservices.azure.com': '' + 'privatelink.azurecr.io': '' } +@description('Zone Names for Validation of existing Private Dns Zones') +param dnsZoneNames array = [ + 'privatelink.services.ai.azure.com' + 'privatelink.openai.azure.com' + 'privatelink.cognitiveservices.azure.com' + 'privatelink.azurecr.io' +] + @description('The name of the project capability host to be created') param projectCapHost string = 'caphostproj' @@ -185,6 +202,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b } } +// Optional: Azure Container Registry with Private Endpoint +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'] + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: project.identity.principalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Step 4: Create a Project - Sub-resource of the AI Services account diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json index 9652d3b63..479d4be92 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "3777570653041274440" + "version": "0.43.8.12551", + "templateHash": "1439992269587931650" } }, "parameters": { @@ -28,6 +28,7 @@ "australiaeast", "swedencentral", "canadaeast", + "canadacentral", "westeurope", "westus3", "uksouth", @@ -83,10 +84,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -157,6 +154,20 @@ "description": "Address prefix for the private endpoint subnet" } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, "dnsZonesSubscriptionId": { "type": "string", "defaultValue": "", @@ -169,12 +180,25 @@ "defaultValue": { "privatelink.services.ai.azure.com": "", "privatelink.openai.azure.com": "", - "privatelink.cognitiveservices.azure.com": "" + "privatelink.cognitiveservices.azure.com": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" } }, + "dnsZoneNames": { + "type": "array", + "defaultValue": [ + "privatelink.services.ai.azure.com", + "privatelink.openai.azure.com", + "privatelink.cognitiveservices.azure.com", + "privatelink.azurecr.io" + ], + "metadata": { + "description": "Zone Names for Validation of existing Private Dns Zones" + } + }, "projectCapHost": { "type": "string", "defaultValue": "caphostproj", @@ -184,8 +208,9 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]", "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]", @@ -260,8 +285,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "9522150535342725348" + "version": "0.43.8.12551", + "templateHash": "13272353057643143267" } }, "parameters": { @@ -374,8 +399,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "1725519900292599397" + "version": "0.43.8.12551", + "templateHash": "6615847355060514198" } }, "parameters": { @@ -547,8 +572,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "12834877200138369468" + "version": "0.43.8.12551", + "templateHash": "16683796064862859220" } }, "parameters": { @@ -637,8 +662,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "16684585228229443328" + "version": "0.43.8.12551", + "templateHash": "11349014942847443024" } }, "parameters": { @@ -720,8 +745,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "16684585228229443328" + "version": "0.43.8.12551", + "templateHash": "11349014942847443024" } }, "parameters": { @@ -773,7 +798,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]" + ] } ], "outputs": { @@ -787,11 +815,11 @@ }, "agentSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "peSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "virtualNetworkName": { "type": "string", @@ -892,8 +920,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "481468620289904255" + "version": "0.43.8.12551", + "templateHash": "1400943577065869287" } }, "parameters": { @@ -1038,8 +1066,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "11715965448884311497" + "version": "0.43.8.12551", + "templateHash": "17632575172686207895" } }, "parameters": { @@ -1246,6 +1274,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName')), '2025-04-01-preview', 'full').identity.principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.43.8.12551", + "templateHash": "8801411756251170105" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName'))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1272,8 +1545,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.40.2.10011", - "templateHash": "17607216443982035670" + "version": "0.43.8.12551", + "templateHash": "6174217467753452418" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/container-registry.bicep @@ -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 diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep index e3ee2f5a1..af360f299 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep @@ -19,6 +19,7 @@ Standard Setup Network Secured Steps for main.bicep 'australiaeast' 'swedencentral' 'canadaeast' + 'canadacentral' 'westeurope' 'westus3' 'uksouth' @@ -48,8 +49,8 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name for your project resource.') @@ -106,6 +107,12 @@ param existingAiFoundryAccountResourceId string = '' @description('Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments.') param skipModelDeployment bool = false +@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet.') +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 = '' + // Account-level capability host is auto-created by the platform when // `networkInjections.scenario='agent'` is set on a NEW account. Only one // capability host per account is allowed, so this flag must stay false in that @@ -114,6 +121,13 @@ param skipModelDeployment bool = false @description('Optional. Create the account-level capability host explicitly. Leave false for fresh deployments (platform auto-creates it via networkInjections). Set true only for a BYO account with no capability host, or to recreate after running deleteCapHost.sh.') param createAccountCapabilityHost bool = false +// Re-derive BYO account context at main.bicep level so we can scope the +// account-level capabilityHost module to the right RG/subscription. +var useExistingAccount = !empty(existingAiFoundryAccountResourceId) +var existingAccountIdParts = split(existingAiFoundryAccountResourceId, '/') +var existingAccountSubscriptionId = useExistingAccount ? existingAccountIdParts[2] : subscription().subscriptionId +var existingAccountResourceGroupName = useExistingAccount ? existingAccountIdParts[4] : resourceGroup().name + @description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') param aiSearchResourceId string = '' @description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') @@ -136,6 +150,7 @@ param existingDnsZones object = { 'privatelink.search.windows.net': '' 'privatelink.blob.core.windows.net': '' 'privatelink.documents.azure.com': '' + 'privatelink.azurecr.io': '' } @description('Zone Names for Validation of existing Private Dns Zones') @@ -146,6 +161,7 @@ param dnsZoneNames array = [ 'privatelink.search.windows.net' 'privatelink.blob.core.windows.net' 'privatelink.documents.azure.com' + 'privatelink.azurecr.io' ] @@ -164,6 +180,7 @@ var cosmosDBNameRaw = toLower('${aiServices}${uniqueSuffix}cosmosdb') var cosmosDBName = length(cosmosDBNameRaw) > 44 ? substring(cosmosDBNameRaw, 0, 44) : cosmosDBNameRaw var aiSearchName = toLower('${aiServices}${uniqueSuffix}search') +var acrName = toLower('acr${uniqueSuffix}') // Check if existing resources have been passed in var storagePassedIn = azureStorageAccountResourceId != '' @@ -346,6 +363,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b ] } +// Optional: Azure Container Registry with Private Endpoint +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'] + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: aiProject.outputs.projectPrincipalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/container-registry.bicep @@ -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 diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep index 4c88201d4..ead2cb198 100644 --- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep @@ -52,8 +52,8 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name for your project resource.') @@ -91,6 +91,12 @@ param peSubnetPrefix string = '' @description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') param azureStorageAccountResourceId string = '' +@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet.') +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 = '' + @description('Subscription ID where existing private DNS zones are located. Leave empty to use current subscription.') param dnsZonesSubscriptionId string = '' @@ -100,6 +106,7 @@ param existingDnsZones object = { 'privatelink.openai.azure.com': '' 'privatelink.cognitiveservices.azure.com': '' 'privatelink.blob.core.windows.net': '' + 'privatelink.azurecr.io': '' } @description('Zone Names for Validation of existing Private Dns Zones') @@ -108,11 +115,13 @@ param dnsZoneNames array = [ 'privatelink.openai.azure.com' 'privatelink.cognitiveservices.azure.com' 'privatelink.blob.core.windows.net' + 'privatelink.azurecr.io' ] var projectName = toLower('${firstProjectName}${uniqueSuffix}') var azureStorageName = toLower('${aiServices}${uniqueSuffix}storage') +var acrName = toLower('acr${uniqueSuffix}') // Check if existing resources have been passed in var storagePassedIn = azureStorageAccountResourceId != '' @@ -222,6 +231,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b ] } +// Optional: Azure Container Registry with Private Endpoint +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'] + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: aiProject.outputs.projectPrincipalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/container-registry.bicep @@ -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 diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep index d98370eb6..562849d8f 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep @@ -48,8 +48,8 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name for your project resource.') @@ -93,6 +93,15 @@ 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.') +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 = '' + +@description('Subscription ID where existing private DNS zones are located. Leave empty to use current subscription.') +param dnsZonesSubscriptionId 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 = '' @@ -105,7 +114,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') @@ -117,12 +127,14 @@ 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 acrName = toLower('acr${uniqueSuffix}') var azureStorageName = toLower('${aiServices}${uniqueSuffix}storage') // Check if existing resources have been passed in @@ -150,6 +162,15 @@ var vnetResourceGroupName = existingVnetPassedIn ? vnetParts[4] : resourceGroup( var existingVnetName = existingVnetPassedIn ? last(vnetParts) : vnetName var trimVnetName = trim(existingVnetName) +// Resolve DNS zones subscription ID - use current subscription if not specified. +// Accept either form: bare GUID or "/subscriptions/". +var normalizedDnsZonesSubscriptionId = empty(dnsZonesSubscriptionId) + ? '' + : (startsWith(toLower(dnsZonesSubscriptionId), '/subscriptions/') + ? split(dnsZonesSubscriptionId, '/')[2] + : dnsZonesSubscriptionId) +var resolvedDnsZonesSubscriptionId = empty(normalizedDnsZonesSubscriptionId) ? subscription().subscriptionId : normalizedDnsZonesSubscriptionId + @description('The name of the project capability host to be created') param projectCapHost string = 'caphostproj' @@ -280,6 +301,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b ] } +// Optional: Azure Container Registry with Private Endpoint +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'] + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: aiProject.outputs.projectPrincipalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/container-registry.bicep @@ -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 diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep index 212c5f554..30422c1a6 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep @@ -48,8 +48,8 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name for your project resource.') @@ -93,11 +93,19 @@ param azureCosmosDBAccountResourceId string = '' @description('The user assigned identity full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') param userAssignedIdentityResourceId string = '' +@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet.') +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 = '' +@description('Subscription ID where existing private DNS zones are located. Leave empty to use current subscription.') +param dnsZonesSubscriptionId string = '' + @description('Object mapping DNS zone names to their resource group, or empty string to indicate creation') param existingDnsZones object = { 'privatelink.services.ai.azure.com': '' @@ -106,6 +114,7 @@ param existingDnsZones object = { 'privatelink.search.windows.net': '' 'privatelink.blob.core.windows.net': '' 'privatelink.documents.azure.com': '' + 'privatelink.azurecr.io': '' } @description('Zone Names for Validation of existing Private Dns Zones') @@ -116,6 +125,7 @@ param dnsZoneNames array = [ 'privatelink.search.windows.net' 'privatelink.blob.core.windows.net' 'privatelink.documents.azure.com' + 'privatelink.azurecr.io' ] @description('User Assigned Identity Name') @@ -126,6 +136,7 @@ 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 != '' @@ -152,6 +163,8 @@ var vnetResourceGroupName = existingVnetPassedIn ? vnetParts[4] : resourceGroup( var existingVnetName = existingVnetPassedIn ? last(vnetParts) : vnetName var trimVnetName = trim(existingVnetName) +var resolvedDnsZonesSubscriptionId = empty(dnsZonesSubscriptionId) ? subscription().subscriptionId : dnsZonesSubscriptionId + @description('The name of the project capability host to be created') param projectCapHost string = 'caphostproj' @@ -288,6 +301,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b ] } +// Optional: Azure Container Registry with Private Endpoint +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'] + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: identity.outputs.uaiPrincipalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/container-registry.bicep @@ -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 diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep index 14e51d249..43647fe57 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep @@ -28,8 +28,8 @@ param aiServices string = 'aiservices' // param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name for your project resource.') diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep index 2ad04c322..b962a3036 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep @@ -34,6 +34,7 @@ Architecture: 'australiaeast' 'swedencentral' 'canadaeast' + 'canadacentral' 'westeurope' 'westus3' 'uksouth' @@ -65,8 +66,8 @@ param modelSkuName string = 'GlobalStandard' param modelCapacity int = 30 // Create a short, unique suffix, that will be unique to each resource group -param deploymentTimestamp string = utcNow('yyyyMMddHHmmss') -var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4) +// Deterministic suffix for idempotent re-deploys (same RG = same names) +var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 4) var accountName = toLower('${aiServices}${uniqueSuffix}') @description('Name prefix for your Foundry project. A 4-character random suffix will be appended.') @@ -128,6 +129,12 @@ param existingAzureCosmosDBAccountResourceId string = '' @description('The Microsoft Fabric Workspace full ARM Resource ID. Optional — enables Fabric private link connectivity.') param existingFabricWorkspaceResourceId string = '' +@description('Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet.') +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 = '' @@ -141,12 +148,14 @@ param existingDnsZones object = { 'privatelink.blob.${environment().suffixes.storage}': { subscriptionId: '', resourceGroup: '' } 'privatelink.documents.azure.com': { subscriptionId: '', resourceGroup: '' } 'privatelink.fabric.microsoft.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.azurecr.io': { subscriptionId: '', resourceGroup: '' } } 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 = existingAzureStorageAccountResourceId != '' @@ -293,6 +302,25 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b // (e.g. aiAccount.outputs, aiDependencies.outputs.*, vnet.outputs.*). } +// Optional: Azure Container Registry with Private Endpoint +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'].resourceGroup + dnsZonesSubscriptionId: empty(existingDnsZones['privatelink.azurecr.io'].subscriptionId) ? subscription().subscriptionId : existingDnsZones['privatelink.azurecr.io'].subscriptionId + developerIpCidr: developerIpCidr + projectPrincipalId: aiProject.outputs.projectPrincipalId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/container-registry.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/container-registry.bicep new file mode 100644 index 000000000..d141d6172 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/container-registry.bicep @@ -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 From 3f6778ce8b91a7b1822e0d56ded4d84d16a7d57e Mon Sep 17 00:00:00 2001 From: foundry-samples automation Date: Thu, 11 Jun 2026 03:45:32 +0000 Subject: [PATCH 2/2] Automatic fixes --- .../azuredeploy.json | 40 +- .../azuredeploy.json | 357 +++++++++++++-- .../azuredeploy.json | 341 +++++++++++++-- .../azuredeploy.json | 364 ++++++++++++++-- .../azuredeploy.json | 377 +++++++++++++--- .../azuredeploy.json | 399 ++++++++++++++--- .../azuredeploy.json | 411 ++++++++++++++---- 7 files changed, 1940 insertions(+), 349 deletions(-) diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json index 479d4be92..49a8bfbf9 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "1439992269587931650" + "version": "0.44.1.10279", + "templateHash": "6646415653041588209" } }, "parameters": { @@ -285,8 +285,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "13272353057643143267" + "version": "0.44.1.10279", + "templateHash": "8059749215340377460" } }, "parameters": { @@ -399,8 +399,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6615847355060514198" + "version": "0.44.1.10279", + "templateHash": "1661897848166677911" } }, "parameters": { @@ -572,8 +572,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16683796064862859220" + "version": "0.44.1.10279", + "templateHash": "13739336036803555258" } }, "parameters": { @@ -662,8 +662,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -745,8 +745,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -920,8 +920,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "1400943577065869287" + "version": "0.44.1.10279", + "templateHash": "8297556890969361359" } }, "parameters": { @@ -1066,8 +1066,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "17632575172686207895" + "version": "0.44.1.10279", + "templateHash": "1726454167409390985" } }, "parameters": { @@ -1319,8 +1319,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "8801411756251170105" + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" } }, "parameters": { @@ -1545,8 +1545,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6174217467753452418" + "version": "0.44.1.10279", + "templateHash": "11310150420706974479" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json index c0b7f1214..7715317d6 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "3697601458917982261" + "version": "0.44.1.10279", + "templateHash": "12392809195493929439" } }, "parameters": { @@ -28,6 +28,7 @@ "australiaeast", "swedencentral", "canadaeast", + "canadacentral", "westeurope", "westus3", "uksouth", @@ -83,10 +84,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -178,6 +175,20 @@ "description": "Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments." } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, "createAccountCapabilityHost": { "type": "bool", "defaultValue": false, @@ -221,7 +232,8 @@ "privatelink.cognitiveservices.azure.com": "", "privatelink.search.windows.net": "", "privatelink.blob.core.windows.net": "", - "privatelink.documents.azure.com": "" + "privatelink.documents.azure.com": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" @@ -235,7 +247,8 @@ "privatelink.cognitiveservices.azure.com", "privatelink.search.windows.net", "privatelink.blob.core.windows.net", - "privatelink.documents.azure.com" + "privatelink.documents.azure.com", + "privatelink.azurecr.io" ], "metadata": { "description": "Zone Names for Validation of existing Private Dns Zones" @@ -250,8 +263,12 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", + "useExistingAccount": "[not(empty(parameters('existingAiFoundryAccountResourceId')))]", + "existingAccountIdParts": "[split(parameters('existingAiFoundryAccountResourceId'), '/')]", + "existingAccountSubscriptionId": "[if(variables('useExistingAccount'), variables('existingAccountIdParts')[2], subscription().subscriptionId)]", + "existingAccountResourceGroupName": "[if(variables('useExistingAccount'), variables('existingAccountIdParts')[4], resourceGroup().name)]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "aiServicesSanitized": "[toLower(replace(parameters('aiServices'), '-', ''))]", "storagePrefixMax": 18, @@ -260,6 +277,7 @@ "cosmosDBNameRaw": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", "cosmosDBName": "[if(greater(length(variables('cosmosDBNameRaw')), 44), substring(variables('cosmosDBNameRaw'), 0, 44), variables('cosmosDBNameRaw'))]", "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]", "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]", @@ -332,8 +350,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "2638034982264747132" + "version": "0.44.1.10279", + "templateHash": "7682622501509208415" } }, "parameters": { @@ -453,8 +471,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6615847355060514198" + "version": "0.44.1.10279", + "templateHash": "1661897848166677911" } }, "parameters": { @@ -629,8 +647,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11782697558662523210" + "version": "0.44.1.10279", + "templateHash": "18043074178365270428" } }, "parameters": { @@ -727,8 +745,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -811,8 +829,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -992,8 +1010,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "2832622634761787610" + "version": "0.44.1.10279", + "templateHash": "16030204193894738620" } }, "parameters": { @@ -1155,8 +1173,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "680740366057404341" + "version": "0.44.1.10279", + "templateHash": "2665767763577212089" } }, "parameters": { @@ -1293,8 +1311,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "5881130554558405559" + "version": "0.44.1.10279", + "templateHash": "13163302344020621777" } }, "parameters": { @@ -1376,8 +1394,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "14267398792843698911" + "version": "0.44.1.10279", + "templateHash": "2281026031668820702" } }, "parameters": { @@ -1659,8 +1677,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16240696292444732987" + "version": "0.44.1.10279", + "templateHash": "698272236681194869" } }, "parameters": { @@ -2135,6 +2153,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -2194,8 +2457,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "5316230502381000043" + "version": "0.44.1.10279", + "templateHash": "6117256850627512682" } }, "parameters": { @@ -2370,8 +2633,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16292565234555392040" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -2425,8 +2688,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10985949897065090550" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -2483,8 +2746,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "17331576029113373855" + "version": "0.44.1.10279", + "templateHash": "15360680621305170360" } }, "parameters": { @@ -2547,8 +2810,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "15580039133385622424" + "version": "0.44.1.10279", + "templateHash": "6103261316579938375" } }, "parameters": { @@ -2621,8 +2884,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "7220206622113249406" + "version": "0.44.1.10279", + "templateHash": "1869373948197367565" } }, "parameters": { @@ -2705,8 +2968,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9752585897450875181" + "version": "0.44.1.10279", + "templateHash": "10353446425348386407" } }, "parameters": { @@ -2800,8 +3063,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "13556924300142468207" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { @@ -2879,8 +3142,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9820545713493490823" + "version": "0.44.1.10279", + "templateHash": "10552108385245653621" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json index c161879c6..5f5892a58 100644 --- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16354343630624836697" + "version": "0.44.1.10279", + "templateHash": "16032808877948152637" } }, "parameters": { @@ -83,10 +83,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -164,6 +160,20 @@ "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, "dnsZonesSubscriptionId": { "type": "string", "defaultValue": "", @@ -177,7 +187,8 @@ "privatelink.services.ai.azure.com": "", "privatelink.openai.azure.com": "", "privatelink.cognitiveservices.azure.com": "", - "privatelink.blob.core.windows.net": "" + "privatelink.blob.core.windows.net": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" @@ -189,7 +200,8 @@ "privatelink.services.ai.azure.com", "privatelink.openai.azure.com", "privatelink.cognitiveservices.azure.com", - "privatelink.blob.core.windows.net" + "privatelink.blob.core.windows.net", + "privatelink.azurecr.io" ], "metadata": { "description": "Zone Names for Validation of existing Private Dns Zones" @@ -197,10 +209,11 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]", "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]", @@ -261,8 +274,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11296469007124090352" + "version": "0.44.1.10279", + "templateHash": "8059749215340377460" } }, "parameters": { @@ -375,8 +388,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6615847355060514198" + "version": "0.44.1.10279", + "templateHash": "1661897848166677911" } }, "parameters": { @@ -548,8 +561,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "8341621792914014201" + "version": "0.44.1.10279", + "templateHash": "13739336036803555258" } }, "parameters": { @@ -638,8 +651,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -721,8 +734,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "11349014942847443024" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -774,7 +787,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]" + ] } ], "outputs": { @@ -788,11 +804,11 @@ }, "agentSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "peSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "virtualNetworkName": { "type": "string", @@ -893,8 +909,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "1400943577065869287" + "version": "0.44.1.10279", + "templateHash": "8297556890969361359" } }, "parameters": { @@ -1027,8 +1043,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "14681023185116728141" + "version": "0.44.1.10279", + "templateHash": "18428162180343544793" } }, "parameters": { @@ -1120,8 +1136,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "12551648450543487356" + "version": "0.44.1.10279", + "templateHash": "3194425532011524628" } }, "parameters": { @@ -1257,8 +1273,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "4199539764441797485" + "version": "0.44.1.10279", + "templateHash": "11525768866797919522" } }, "parameters": { @@ -1555,6 +1571,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1596,8 +1857,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "17079120784923696162" + "version": "0.44.1.10279", + "templateHash": "14164971324918652057" } }, "parameters": { @@ -1709,8 +1970,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16292565234555392040" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -1764,8 +2025,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10985949897065090550" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -1820,8 +2081,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "4519973268515204861" + "version": "0.44.1.10279", + "templateHash": "12566418346689420792" } }, "parameters": { @@ -1887,8 +2148,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "13556924300142468207" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json index ec4c0d0a3..58d3d3c06 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "8413960230154535001" + "version": "0.44.1.10279", + "templateHash": "16534706758296881327" } }, "parameters": { @@ -83,10 +83,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -185,6 +181,27 @@ "description": "The API Management Service full ARM Resource ID. This is an optional field for existing API Management services." } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located. Leave empty to use current subscription." + } + }, "existingDnsZones": { "type": "object", "defaultValue": { @@ -194,7 +211,8 @@ "privatelink.search.windows.net": "", "privatelink.blob.core.windows.net": "", "privatelink.documents.azure.com": "", - "privatelink.azure-api.net": "" + "privatelink.azure-api.net": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" @@ -209,7 +227,8 @@ "privatelink.search.windows.net", "privatelink.blob.core.windows.net", "privatelink.documents.azure.com", - "privatelink.azure-api.net" + "privatelink.azure-api.net", + "privatelink.azurecr.io" ], "metadata": { "description": "Zone Names for Validation of existing Private Dns Zones" @@ -224,11 +243,12 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]", "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]", @@ -247,7 +267,9 @@ "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]", "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]", "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]", - "trimVnetName": "[trim(variables('existingVnetName'))]" + "trimVnetName": "[trim(variables('existingVnetName'))]", + "normalizedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), '', if(startsWith(toLower(parameters('dnsZonesSubscriptionId')), '/subscriptions/'), split(parameters('dnsZonesSubscriptionId'), '/')[2], parameters('dnsZonesSubscriptionId')))]", + "resolvedDnsZonesSubscriptionId": "[if(empty(variables('normalizedDnsZonesSubscriptionId')), subscription().subscriptionId, variables('normalizedDnsZonesSubscriptionId'))]" }, "resources": [ { @@ -297,8 +319,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "17355345610868234879" + "version": "0.44.1.10279", + "templateHash": "8059749215340377460" } }, "parameters": { @@ -411,8 +433,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "10901913041029806810" + "version": "0.44.1.10279", + "templateHash": "1661897848166677911" } }, "parameters": { @@ -584,8 +606,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "10509554109014084911" + "version": "0.44.1.10279", + "templateHash": "13739336036803555258" } }, "parameters": { @@ -674,8 +696,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13091507001124186389" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -757,8 +779,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13091507001124186389" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -810,7 +832,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]" + ] } ], "outputs": { @@ -824,11 +849,11 @@ }, "agentSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "peSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "virtualNetworkName": { "type": "string", @@ -929,8 +954,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "14040588815014793423" + "version": "0.44.1.10279", + "templateHash": "8297556890969361359" } }, "parameters": { @@ -1069,8 +1094,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "5338128208054238484" + "version": "0.44.1.10279", + "templateHash": "18120053315158002238" } }, "parameters": { @@ -1248,8 +1273,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "18286092773595477616" + "version": "0.44.1.10279", + "templateHash": "2281026031668820702" } }, "parameters": { @@ -1537,8 +1562,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "884548399218956240" + "version": "0.44.1.10279", + "templateHash": "17319375375259513982" } }, "parameters": { @@ -2100,6 +2125,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -2159,8 +2429,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "1726962209014429687" + "version": "0.44.1.10279", + "templateHash": "6117256850627512682" } }, "parameters": { @@ -2334,8 +2604,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "4283212754622059328" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -2389,8 +2659,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "11244759636908765413" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -2447,8 +2717,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "8930392032077836844" + "version": "0.44.1.10279", + "templateHash": "1319098613063424337" } }, "parameters": { @@ -2511,8 +2781,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "15161371673245917725" + "version": "0.44.1.10279", + "templateHash": "6103261316579938375" } }, "parameters": { @@ -2596,8 +2866,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13412710540818916374" + "version": "0.44.1.10279", + "templateHash": "10353446425348386407" } }, "parameters": { @@ -2690,8 +2960,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "13970727149060615708" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { @@ -2769,8 +3039,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.1.21952", - "templateHash": "15639047449398800481" + "version": "0.44.1.10279", + "templateHash": "10552108385245653621" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json index 96525e295..ad2fb1cc0 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "9271946558272429054" + "version": "0.44.1.10279", + "templateHash": "14940490256573158788" } }, "parameters": { @@ -83,10 +83,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -185,6 +181,27 @@ "description": "The user assigned identity full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located. Leave empty to use current subscription." + } + }, "existingDnsZones": { "type": "object", "defaultValue": { @@ -193,7 +210,8 @@ "privatelink.cognitiveservices.azure.com": "", "privatelink.search.windows.net": "", "privatelink.blob.core.windows.net": "", - "privatelink.documents.azure.com": "" + "privatelink.documents.azure.com": "", + "privatelink.azurecr.io": "" }, "metadata": { "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation" @@ -207,7 +225,8 @@ "privatelink.cognitiveservices.azure.com", "privatelink.search.windows.net", "privatelink.blob.core.windows.net", - "privatelink.documents.azure.com" + "privatelink.documents.azure.com", + "privatelink.azurecr.io" ], "metadata": { "description": "Zone Names for Validation of existing Private Dns Zones" @@ -229,13 +248,14 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", "uaiName": "[toLower(format('{0}{1}', parameters('userAssignedIdentityName'), variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]", "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]", "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]", "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]", @@ -254,7 +274,8 @@ "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]", "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]", "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]", - "trimVnetName": "[trim(variables('existingVnetName'))]" + "trimVnetName": "[trim(variables('existingVnetName'))]", + "resolvedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), subscription().subscriptionId, parameters('dnsZonesSubscriptionId'))]" }, "resources": [ { @@ -283,8 +304,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "12473591672685297473" + "version": "0.44.1.10279", + "templateHash": "11672049314525648729" } }, "parameters": { @@ -384,8 +405,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "8505298823279202405" + "version": "0.44.1.10279", + "templateHash": "8059749215340377460" } }, "parameters": { @@ -498,8 +519,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "4954184648131521061" + "version": "0.44.1.10279", + "templateHash": "1661897848166677911" } }, "parameters": { @@ -671,8 +692,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "3152324712046183852" + "version": "0.44.1.10279", + "templateHash": "13739336036803555258" } }, "parameters": { @@ -761,8 +782,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "17043822047386586435" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -844,8 +865,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "17043822047386586435" + "version": "0.44.1.10279", + "templateHash": "4074506216227302224" } }, "parameters": { @@ -897,7 +918,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]" + ] } ], "outputs": { @@ -911,11 +935,11 @@ }, "agentSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "peSubnetId": { "type": "string", - "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]" }, "virtualNetworkName": { "type": "string", @@ -1019,8 +1043,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "12620781326236378852" + "version": "0.44.1.10279", + "templateHash": "4814971409754124721" } }, "parameters": { @@ -1159,8 +1183,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "7641310640078958122" + "version": "0.44.1.10279", + "templateHash": "7741237225867053087" } }, "parameters": { @@ -1311,8 +1335,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "2754228344238136934" + "version": "0.44.1.10279", + "templateHash": "2281026031668820702" } }, "parameters": { @@ -1591,8 +1615,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "8094529554453089222" + "version": "0.44.1.10279", + "templateHash": "2899384091456590202" } }, "parameters": { @@ -2061,6 +2085,251 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io']]" + }, + "dnsZonesSubscriptionId": { + "value": "[variables('resolvedDnsZonesSubscriptionId')]" + }, + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('uaiName'))), '2025-04-01').outputs.uaiPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('uaiName')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -2123,8 +2392,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "3622791801420135420" + "version": "0.44.1.10279", + "templateHash": "12643788828183838953" } }, "parameters": { @@ -2301,8 +2570,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "6910483561575524105" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -2356,8 +2625,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "14683840003859985069" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -2372,7 +2641,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('azureStorageName'))]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName'))]", "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]", "properties": { "principalId": "[parameters('projectPrincipalId')]", @@ -2414,8 +2683,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "25128059954858801" + "version": "0.44.1.10279", + "templateHash": "15360680621305170360" } }, "parameters": { @@ -2436,7 +2705,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]", "properties": { "principalId": "[parameters('projectPrincipalId')]", @@ -2478,8 +2747,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "7968115481508840" + "version": "0.44.1.10279", + "templateHash": "6103261316579938375" } }, "parameters": { @@ -2500,7 +2769,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]", "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", "properties": { "principalId": "[parameters('projectPrincipalId')]", @@ -2511,7 +2780,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]", "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", "properties": { "principalId": "[parameters('projectPrincipalId')]", @@ -2563,8 +2832,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "17458377866351620215" + "version": "0.44.1.10279", + "templateHash": "10353446425348386407" } }, "parameters": { @@ -2657,8 +2926,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "13874725855824693255" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { @@ -2688,7 +2957,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", @@ -2736,8 +3005,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "17187611271934567223" + "version": "0.44.1.10279", + "templateHash": "10552108385245653621" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json index 83f80f7ff..d594e0761 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json @@ -4,38 +4,16 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "10726822887583020348" + "version": "0.44.1.10279", + "templateHash": "4404295085759197571" } }, "parameters": { "location": { "type": "string", - "defaultValue": "eastus2", - "allowedValues": [ - "spaincentral", - "westus", - "eastus", - "japaneast", - "francecentral", - "eastus2", - "uaenorth", - "brazilsouth", - "germanywestcentral", - "italynorth", - "southcentralus", - "westcentralus", - "australiaeast", - "swedencentral", - "canadaeast", - "southafricanorth", - "westeurope", - "westus3", - "southindia", - "uksouth" - ], + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Location for all resources." + "description": "Location for all resources. Defaults to the resource group location." } }, "isolationMode": { @@ -56,10 +34,6 @@ "description": "Name for your AI Services resource." } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -183,7 +157,7 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", @@ -250,8 +224,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "5439878820994143115" + "version": "0.44.1.10279", + "templateHash": "1907979108654803661" } }, "parameters": { @@ -344,8 +318,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "16620010525527418603" + "version": "0.44.1.10279", + "templateHash": "8755662122767726126" } }, "parameters": { @@ -474,8 +448,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "11496589636440152813" + "version": "0.44.1.10279", + "templateHash": "6156118219142588611" } }, "parameters": { @@ -547,8 +521,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "8762424559716292746" + "version": "0.44.1.10279", + "templateHash": "615461089373883997" } }, "parameters": { @@ -685,8 +659,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "9631735419288057649" + "version": "0.44.1.10279", + "templateHash": "1242706645341609299" } }, "parameters": { @@ -799,8 +773,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "15549652612714258938" + "version": "0.44.1.10279", + "templateHash": "18120053315158002238" } }, "parameters": { @@ -978,8 +952,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "16516106179017566598" + "version": "0.44.1.10279", + "templateHash": "2281026031668820702" } }, "parameters": { @@ -1193,6 +1167,201 @@ "[resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix')))]" ] }, + { + "condition": "[variables('storagePassedIn')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('network-approver-storage-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('azureStorageSubscriptionId')]", + "resourceGroup": "[variables('azureStorageResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiAccountPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountPrincipalId.value]" + }, + "aiAccountResourceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountID.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "10236762556073968346" + } + }, + "parameters": { + "aiAccountPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the AI Services account managed identity" + } + }, + "aiAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Services account resource ID (used for deterministic GUID generation)" + } + } + }, + "variables": { + "networkConnectionApproverRoleId": "b556d68e-0be0-4f35-a333-ad7ee1ce17ea" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('aiAccountResourceId'), variables('networkConnectionApproverRoleId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('networkConnectionApproverRoleId'))]", + "principalId": "[parameters('aiAccountPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]" + ] + }, + { + "condition": "[variables('cosmosPassedIn')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('network-approver-cosmos-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('cosmosDBSubscriptionId')]", + "resourceGroup": "[variables('cosmosDBResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiAccountPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountPrincipalId.value]" + }, + "aiAccountResourceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountID.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "10236762556073968346" + } + }, + "parameters": { + "aiAccountPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the AI Services account managed identity" + } + }, + "aiAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Services account resource ID (used for deterministic GUID generation)" + } + } + }, + "variables": { + "networkConnectionApproverRoleId": "b556d68e-0be0-4f35-a333-ad7ee1ce17ea" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('aiAccountResourceId'), variables('networkConnectionApproverRoleId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('networkConnectionApproverRoleId'))]", + "principalId": "[parameters('aiAccountPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]" + ] + }, + { + "condition": "[variables('searchPassedIn')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('network-approver-search-{0}-deployment', variables('uniqueSuffix'))]", + "subscriptionId": "[variables('aiSearchServiceSubscriptionId')]", + "resourceGroup": "[variables('aiSearchServiceResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiAccountPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountPrincipalId.value]" + }, + "aiAccountResourceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountID.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "10236762556073968346" + } + }, + "parameters": { + "aiAccountPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the AI Services account managed identity" + } + }, + "aiAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Services account resource ID (used for deterministic GUID generation)" + } + } + }, + "variables": { + "networkConnectionApproverRoleId": "b556d68e-0be0-4f35-a333-ad7ee1ce17ea" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('aiAccountResourceId'), variables('networkConnectionApproverRoleId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('networkConnectionApproverRoleId'))]", + "principalId": "[parameters('aiAccountPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1208,6 +1377,15 @@ }, "isolationMode": { "value": "[parameters('isolationMode')]" + }, + "storageAccountResourceId": { + "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value)]" + }, + "cosmosDBResourceId": { + "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value)]" + }, + "aiSearchResourceId": { + "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceSubscriptionId.value, reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceResourceGroupName.value), 'Microsoft.Search/searchServices', reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value)]" } }, "template": { @@ -1216,8 +1394,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "9263659451530831183" + "version": "0.44.1.10279", + "templateHash": "13117200103483543551" } }, "parameters": { @@ -1237,6 +1415,24 @@ "metadata": { "description": "The isolation mode for the managed network" } + }, + "storageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Storage Account for outbound PE rule" + } + }, + "cosmosDBResourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Cosmos DB Account for outbound PE rule" + } + }, + "aiSearchResourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Search Service for outbound PE rule" + } } }, "resources": [ @@ -1251,6 +1447,73 @@ "provisionNetworkNow": true } } + }, + { + "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules", + "apiVersion": "2025-10-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), 'default', 'aiservices-account-rule')]", + "properties": { + "type": "PrivateEndpoint", + "destination": { + "serviceResourceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]", + "subresourceTarget": "account" + }, + "category": "UserDefined" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks', parameters('accountName'), 'default')]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules", + "apiVersion": "2025-10-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), 'default', 'storage-blob-rule')]", + "properties": { + "type": "PrivateEndpoint", + "destination": { + "serviceResourceId": "[parameters('storageAccountResourceId')]", + "subresourceTarget": "blob" + }, + "category": "UserDefined" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules', parameters('accountName'), 'default', 'aiservices-account-rule')]", + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks', parameters('accountName'), 'default')]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules", + "apiVersion": "2025-10-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), 'default', 'cosmos-sql-rule')]", + "properties": { + "type": "PrivateEndpoint", + "destination": { + "serviceResourceId": "[parameters('cosmosDBResourceId')]", + "subresourceTarget": "Sql" + }, + "category": "UserDefined" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks', parameters('accountName'), 'default')]", + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules', parameters('accountName'), 'default', 'storage-blob-rule')]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules", + "apiVersion": "2025-10-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), 'default', 'aisearch-rule')]", + "properties": { + "type": "PrivateEndpoint", + "destination": { + "serviceResourceId": "[parameters('aiSearchResourceId')]", + "subresourceTarget": "searchService" + }, + "category": "UserDefined" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules', parameters('accountName'), 'default', 'cosmos-sql-rule')]", + "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks', parameters('accountName'), 'default')]" + ] } ], "outputs": { @@ -1263,7 +1526,10 @@ }, "dependsOn": [ "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]", - "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]" + "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.Resources/deployments', format('network-approver-cosmos-{0}-deployment', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiSearchServiceSubscriptionId'), variables('aiSearchServiceResourceGroupName')), 'Microsoft.Resources/deployments', format('network-approver-search-{0}-deployment', variables('uniqueSuffix')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('network-approver-storage-{0}-deployment', variables('uniqueSuffix')))]" ] }, { @@ -1340,8 +1606,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "7099263758639578851" + "version": "0.44.1.10279", + "templateHash": "17319375375259513982" } }, "parameters": { @@ -1963,8 +2229,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "18174839353195527759" + "version": "0.44.1.10279", + "templateHash": "9290800260455731843" } }, "parameters": { @@ -2140,8 +2406,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "5729541472937730744" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -2195,8 +2461,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "6805612394357697185" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -2253,8 +2519,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "3731695714569887273" + "version": "0.44.1.10279", + "templateHash": "1319098613063424337" } }, "parameters": { @@ -2317,8 +2583,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "167668741936896543" + "version": "0.44.1.10279", + "templateHash": "6103261316579938375" } }, "parameters": { @@ -2402,8 +2668,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "10627022964818938294" + "version": "0.44.1.10279", + "templateHash": "10353446425348386407" } }, "parameters": { @@ -2464,6 +2730,7 @@ "[resourceId('Microsoft.Resources/deployments', format('ai-{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiSearchServiceSubscriptionId'), variables('aiSearchServiceResourceGroupName')), 'Microsoft.Resources/deployments', format('ai-search-ra-{0}-deployment', variables('uniqueSuffix')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.Resources/deployments', format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('managed-network-{0}-deployment', variables('uniqueSuffix')))]", "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-ra-{0}-deployment', variables('uniqueSuffix')))]" ] @@ -2496,8 +2763,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "14861833638743424490" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { @@ -2575,8 +2842,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "9661397591572983652" + "version": "0.44.1.10279", + "templateHash": "15057086489726002053" } }, "parameters": { diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json index 12575125b..c2aa228bd 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "12812848625846814930" + "version": "0.44.1.10279", + "templateHash": "8682967860099869832" } }, "parameters": { @@ -29,6 +29,7 @@ "australiaeast", "swedencentral", "canadaeast", + "canadacentral", "westeurope", "westus3", "uksouth", @@ -86,10 +87,6 @@ "description": "The tokens per minute (TPM) of your model deployment" } }, - "deploymentTimestamp": { - "type": "string", - "defaultValue": "[utcNow('yyyyMMddHHmmss')]" - }, "firstProjectName": { "type": "string", "defaultValue": "project", @@ -225,6 +222,20 @@ "description": "The Microsoft Fabric Workspace full ARM Resource ID. Optional — enables Fabric private link connectivity." } }, + "enableContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable Azure Container Registry with Private Endpoint. When true, creates an ACR (Premium SKU) with a PE in the private endpoints subnet." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, "existingDnsZones": { "type": "object", "defaultValue": { @@ -255,6 +266,10 @@ "privatelink.fabric.microsoft.com": { "subscriptionId": "", "resourceGroup": "" + }, + "privatelink.azurecr.io": { + "subscriptionId": "", + "resourceGroup": "" } }, "metadata": { @@ -270,12 +285,13 @@ } }, "variables": { - "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]", + "uniqueSuffix": "[substring(uniqueString(resourceGroup().id), 0, 4)]", "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]", "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]", "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]", "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]", "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]", + "acrName": "[toLower(format('acr{0}', variables('uniqueSuffix')))]", "storagePassedIn": "[not(equals(parameters('existingAzureStorageAccountResourceId'), ''))]", "searchPassedIn": "[not(equals(parameters('existingAiSearchResourceId'), ''))]", "cosmosPassedIn": "[not(equals(parameters('existingAzureCosmosDBAccountResourceId'), ''))]", @@ -367,8 +383,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "3367717386889798767" + "version": "0.44.1.10279", + "templateHash": "4129256220206315078" } }, "parameters": { @@ -522,8 +538,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "3231703172659113421" + "version": "0.44.1.10279", + "templateHash": "3937207574817375598" } }, "parameters": { @@ -747,8 +763,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "8635420737989534099" + "version": "0.44.1.10279", + "templateHash": "7954541854501949460" } }, "parameters": { @@ -875,8 +891,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10805545016086603042" + "version": "0.44.1.10279", + "templateHash": "3555831451184574880" } }, "parameters": { @@ -970,8 +986,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10805545016086603042" + "version": "0.44.1.10279", + "templateHash": "3555831451184574880" } }, "parameters": { @@ -1075,8 +1091,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10805545016086603042" + "version": "0.44.1.10279", + "templateHash": "3555831451184574880" } }, "parameters": { @@ -1275,8 +1291,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "1400943577065869287" + "version": "0.44.1.10279", + "templateHash": "8297556890969361359" } }, "parameters": { @@ -1427,8 +1443,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6113332263495979803" + "version": "0.44.1.10279", + "templateHash": "1694214023787228679" } }, "parameters": { @@ -1707,8 +1723,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9883330111104552489" + "version": "0.44.1.10279", + "templateHash": "11341165364231087441" } }, "parameters": { @@ -1819,42 +1835,44 @@ }, "existingDnsZones": { "type": "object", - "defaultValue": { - "privatelink.services.ai.azure.com": { - "subscriptionId": "", - "resourceGroup": "" - }, - "privatelink.openai.azure.com": { - "subscriptionId": "", - "resourceGroup": "" - }, - "privatelink.cognitiveservices.azure.com": { - "subscriptionId": "", - "resourceGroup": "" - }, - "privatelink.search.windows.net": { - "subscriptionId": "", - "resourceGroup": "" - }, - "[format('privatelink.blob.{0}', environment().suffixes.storage)]": { - "subscriptionId": "", - "resourceGroup": "" - }, - "privatelink.documents.azure.com": { - "subscriptionId": "", - "resourceGroup": "" - }, - "privatelink.fabric.microsoft.com": { - "subscriptionId": "", - "resourceGroup": "" - } - }, + "defaultValue": {}, "metadata": { "description": "Map of DNS zone FQDNs to an object describing where the zone lives. Each value must be an object with optional `subscriptionId` and `resourceGroup` properties. Empty `resourceGroup` means \"create the zone in this deployment's resource group\". Non-empty `resourceGroup` references an existing zone; empty `subscriptionId` defaults to the current subscription." } } }, "variables": { + "requiredDnsZones": { + "privatelink.services.ai.azure.com": { + "subscriptionId": "", + "resourceGroup": "" + }, + "privatelink.openai.azure.com": { + "subscriptionId": "", + "resourceGroup": "" + }, + "privatelink.cognitiveservices.azure.com": { + "subscriptionId": "", + "resourceGroup": "" + }, + "privatelink.search.windows.net": { + "subscriptionId": "", + "resourceGroup": "" + }, + "[format('privatelink.blob.{0}', environment().suffixes.storage)]": { + "subscriptionId": "", + "resourceGroup": "" + }, + "privatelink.documents.azure.com": { + "subscriptionId": "", + "resourceGroup": "" + }, + "privatelink.fabric.microsoft.com": { + "subscriptionId": "", + "resourceGroup": "" + } + }, + "effectiveDnsZones": "[union(variables('requiredDnsZones'), parameters('existingDnsZones'))]", "fabricPassedIn": "[not(equals(parameters('fabricWorkspaceResourceId'), ''))]", "fabricParts": "[split(parameters('fabricWorkspaceResourceId'), '/')]", "fabricWorkspaceName": "[if(variables('fabricPassedIn'), last(variables('fabricParts')), '')]", @@ -1865,7 +1883,7 @@ "storageDnsZoneName": "[format('privatelink.blob.{0}', environment().suffixes.storage)]", "cosmosDBDnsZoneName": "privatelink.documents.azure.com", "fabricDnsZoneName": "privatelink.fabric.microsoft.com", - "dnsZoneEntries": "[items(parameters('existingDnsZones'))]", + "dnsZoneEntries": "[items(variables('effectiveDnsZones'))]", "dnsZoneKeys": "[map(variables('dnsZoneEntries'), lambda('e', lambdaVariables('e').key))]" }, "resources": [ @@ -2008,8 +2026,8 @@ }, "dependsOn": [ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]", - "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiServicesDnsZoneName'))].key, '.', '-'), parameters('suffix')))]", "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('openAiDnsZoneName'))].key, '.', '-'), parameters('suffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiServicesDnsZoneName'))].key, '.', '-'), parameters('suffix')))]", "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('cognitiveServicesDnsZoneName'))].key, '.', '-'), parameters('suffix')))]" ] }, @@ -2127,8 +2145,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9671566114533960613" + "version": "0.44.1.10279", + "templateHash": "10882085353688836777" } }, "parameters": { @@ -2224,6 +2242,249 @@ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" ] }, + { + "condition": "[parameters('enableContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}-deployment', variables('uniqueSuffix'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acrName": { + "value": "[variables('acrName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "peSubnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetId.value]" + }, + "vnetId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkId.value]" + }, + "suffix": { + "value": "[variables('uniqueSuffix')]" + }, + "existingDnsZoneResourceGroup": { + "value": "[parameters('existingDnsZones')['privatelink.azurecr.io'].resourceGroup]" + }, + "dnsZonesSubscriptionId": "[if(empty(parameters('existingDnsZones')['privatelink.azurecr.io'].subscriptionId), createObject('value', subscription().subscriptionId), createObject('value', parameters('existingDnsZones')['privatelink.azurecr.io'].subscriptionId))]", + "developerIpCidr": { + "value": "[parameters('developerIpCidr')]" + }, + "projectPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "4699943037135843809" + } + }, + "parameters": { + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for the ACR" + } + }, + "peSubnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Private Endpoint subnet" + } + }, + "vnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Virtual Network" + } + }, + "suffix": { + "type": "string", + "metadata": { + "description": "Suffix for unique resource names" + } + }, + "existingDnsZoneResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource group name for existing ACR DNS zone. Empty string means create a new zone." + } + }, + "dnsZonesSubscriptionId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "Subscription ID where existing private DNS zones are located." + } + }, + "developerIpCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "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." + } + }, + "projectPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Principal ID of the project managed identity to grant AcrPull role. When empty, no role assignment is created." + } + } + }, + "variables": { + "acrDnsZoneName": "privatelink.azurecr.io", + "acrDnsZoneId": "[if(empty(parameters('existingDnsZoneResourceGroup')), resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), parameters('existingDnsZoneResourceGroup')), 'Microsoft.Network/privateDnsZones', variables('acrDnsZoneName')))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "adminUserEnabled": false, + "publicNetworkAccess": "[if(empty(parameters('developerIpCidr')), 'Disabled', 'Enabled')]", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": "[if(empty(parameters('developerIpCidr')), null(), createObject('defaultAction', 'Deny', 'ipRules', createArray(createObject('action', 'Allow', 'value', parameters('developerIpCidr')))))]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[format('{0}-private-endpoint', parameters('acrName'))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('peSubnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('{0}-private-link-service-connection', parameters('acrName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('acrDnsZoneName')]", + "location": "global" + }, + { + "condition": "[empty(parameters('existingDnsZoneResourceGroup'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "location": "global", + "properties": { + "virtualNetwork": { + "id": "[parameters('vnetId')]" + }, + "registrationEnabled": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('acrName')), format('{0}-dns-group', parameters('acrName')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-dns-config', parameters('acrName'))]", + "properties": { + "privateDnsZoneId": "[variables('acrDnsZoneId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('acrDnsZoneName'), format('acr-{0}-link', parameters('suffix')))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('acrDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('acrName')))]" + ] + }, + { + "condition": "[not(empty(parameters('projectPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('projectPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('projectPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + ] + } + ], + "outputs": { + "acrId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Azure Container Registry" + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "acrName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Container Registry" + }, + "value": "[parameters('acrName')]" + }, + "acrLoginServer": { + "type": "string", + "metadata": { + "description": "Login server URL of the Azure Container Registry" + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -2283,8 +2544,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "5316230502381000043" + "version": "0.44.1.10279", + "templateHash": "6117256850627512682" } }, "parameters": { @@ -2458,8 +2719,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "16292565234555392040" + "version": "0.44.1.10279", + "templateHash": "3421890076151369374" } }, "parameters": { @@ -2513,8 +2774,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "10985949897065090550" + "version": "0.44.1.10279", + "templateHash": "5322883668661759738" } }, "parameters": { @@ -2571,8 +2832,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "17331576029113373855" + "version": "0.44.1.10279", + "templateHash": "15360680621305170360" } }, "parameters": { @@ -2635,8 +2896,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "15580039133385622424" + "version": "0.44.1.10279", + "templateHash": "6103261316579938375" } }, "parameters": { @@ -2720,8 +2981,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9752585897450875181" + "version": "0.44.1.10279", + "templateHash": "10353446425348386407" } }, "parameters": { @@ -2813,8 +3074,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "13556924300142468207" + "version": "0.44.1.10279", + "templateHash": "8509907656517137061" } }, "parameters": { @@ -2893,8 +3154,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "12588788614140985947" + "version": "0.44.1.10279", + "templateHash": "15057086489726002053" } }, "parameters": {