From 7712d4a4fb1142791791ecdf9178de588b7bd198 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Tue, 7 Jul 2026 15:29:07 +0530 Subject: [PATCH 01/16] fix: update deployment scripts and templates for WAF support and private networking configuration --- azure.yaml | 8 +- infra/main.bicep | 16 +- infra/main.json | 376 ++++++++++++++++++++++++-- infra/modules/containerRegistry.bicep | 51 ++++ scripts/deploy_container_images.ps1 | 53 +++- scripts/deploy_container_images.sh | 38 +++ 6 files changed, 507 insertions(+), 35 deletions(-) diff --git a/azure.yaml b/azure.yaml index 27ec7d5d..26bc6e9c 100644 --- a/azure.yaml +++ b/azure.yaml @@ -12,8 +12,8 @@ hooks: posix: shell: sh run: | - echo "==> Building and pushing container images to the dedicated ACR (remote build)" - bash ./scripts/deploy_container_images.sh + echo "ℹ️ Container images are NOT built automatically. Run the build manually when ready:" + echo " bash ./scripts/deploy_container_images.sh" echo "-----" echo "🧭 Web App Details:" echo "βœ… Name: $CONTAINER_WEB_APP_NAME" @@ -29,8 +29,8 @@ hooks: windows: shell: pwsh run: | - Write-Host "==> Building and pushing container images to the dedicated ACR (remote build)" - ./scripts/deploy_container_images.ps1 + Write-Host "ℹ️ Container images are NOT built automatically. Run the build manually when ready:" + Write-Host " ./scripts/deploy_container_images.ps1" Write-Host "-----" Write-Host "🧭 Web App Details:" Write-Host "βœ… Name: $env:CONTAINER_WEB_APP_NAME" diff --git a/infra/main.bicep b/infra/main.bicep index a5216487..df834653 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -236,13 +236,17 @@ module containerRegistry './modules/containerRegistry.bicep' = { name: containerRegistryName location: solutionLocation tags: allTags - // Premium SKU in WAF/private-networking mode (supports higher throughput and - // future private endpoints). Public network access is kept Enabled in both - // modes so remote `az acr build` (ACR Tasks) and managed-identity pulls work; - // AzureServices bypass lets trusted ACR Tasks reach the registry. + // Premium SKU in WAF/private-networking mode (required for private endpoints + // and network rule sets). In WAF mode public network access is Disabled at + // rest; runtime pulls flow over a private endpoint and the post-deploy build + // script temporarily re-enables public access for the remote `az acr build`. sku: enablePrivateNetworking ? 'Premium' : 'Standard' - publicNetworkAccess: 'Enabled' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' networkRuleBypassOptions: 'AzureServices' + // WAF: host the registry private endpoint in the backend subnet and link it + // to the privatelink.azurecr.io DNS zone so image pulls resolve privately. + privateEndpointSubnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.backendSubnetResourceId : '' + privateDnsZoneResourceId: enablePrivateNetworking ? avmPrivateDnsZones[dnsZoneIndex.containerRegistry]!.outputs.resourceId : '' // Application managed identity gets AcrPull for identity-based image pulls. acrPullPrincipalIds: [ appIdentity.outputs.principalId @@ -605,6 +609,7 @@ var privateDnsZones = [ 'privatelink.blob.${environment().suffixes.storage}' 'privatelink.queue.${environment().suffixes.storage}' 'privatelink.azconfig.io' + 'privatelink.azurecr.io' ] // DNS Zone Index Constants @@ -616,6 +621,7 @@ var dnsZoneIndex = { storageBlob: 4 storageQueue: 5 appConfig: 6 + containerRegistry: 7 } // List of DNS zone indices that correspond to AI-related services. diff --git a/infra/main.json b/infra/main.json index d44fc4a0..838641d7 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.43.8.12551", - "templateHash": "2561563027340256551" + "templateHash": "17608527028333640769" } }, "parameters": { @@ -63,9 +63,9 @@ }, "containerRegistryEndpoint": { "type": "string", - "defaultValue": "containermigrationacr.azurecr.io", + "defaultValue": "", "metadata": { - "description": "Optional. The endpoint (excluding https://) of an existing container registry. This is the `loginServer` when using Azure Container Registry." + "description": "Optional. [Deprecated] The endpoint (excluding https://) of an existing container registry. Retained only for backward compatibility with existing parameter files/pipelines; each deployment now provisions its own dedicated Azure Container Registry and no longer depends on a shared/public registry." } }, "imageTag": { @@ -75,6 +75,13 @@ "description": "Optional. The image tag to use for container images. Defaults to \"latest_v2\"." } }, + "placeholderContainerImage": { + "type": "string", + "defaultValue": "mcr.microsoft.com/k8se/quickstart:latest", + "metadata": { + "description": "Optional. Placeholder container image used to initially provision the container apps.\nThe dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps quickstart image." + } + }, "deploymentType": { "type": "string", "defaultValue": "GlobalStandard", @@ -267,6 +274,7 @@ }, "replicaLocation": "[variables('replicaRegionPairs')[resourceGroup().location]]", "userAssignedIdentityResourceName": "[format('id-{0}', variables('solutionSuffix'))]", + "containerRegistryName": "[take(format('cr{0}', variables('solutionSuffix')), 50)]", "logAnalyticsWorkspaceResourceName": "[format('log-{0}', variables('solutionSuffix'))]", "applicationInsightsResourceName": "[format('appi-{0}', variables('solutionSuffix'))]", "bastionHostName": "[format('bas-{0}', variables('solutionSuffix'))]", @@ -282,7 +290,8 @@ "privatelink.documents.azure.com", "[format('privatelink.blob.{0}', environment().suffixes.storage)]", "[format('privatelink.queue.{0}', environment().suffixes.storage)]", - "privatelink.azconfig.io" + "privatelink.azconfig.io", + "privatelink.azurecr.io" ], "dnsZoneIndex": { "cognitiveServices": 0, @@ -291,7 +300,8 @@ "cosmosDB": 3, "storageBlob": 4, "storageQueue": 5, - "appConfig": 6 + "appConfig": 6, + "containerRegistry": 7 }, "aiRelatedDnsZoneIndices": [ "[variables('dnsZoneIndex').cognitiveServices]", @@ -840,6 +850,289 @@ } } }, + "containerRegistry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.container-registry.{0}', variables('solutionSuffix')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('containerRegistryName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[variables('allTags')]" + }, + "sku": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Premium'), createObject('value', 'Standard'))]", + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "networkRuleBypassOptions": { + "value": "AzureServices" + }, + "privateEndpointSubnetResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.backendSubnetResourceId.value), createObject('value', ''))]", + "privateDnsZoneResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)).outputs.resourceId.value), createObject('value', ''))]", + "acrPullPrincipalIds": { + "value": [ + "[reference('appIdentity').outputs.principalId.value]" + ] + }, + "buildPrincipalId": { + "value": "[variables('deployingUserPrincipalId')]" + }, + "buildPrincipalType": { + "value": "[variables('deployingUserType')]" + } + }, + "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": "10052698379792252001" + }, + "name": "Dedicated Azure Container Registry", + "description": "Provisions a dedicated Azure Container Registry (ACR) for a single deployment and configures identity-based authentication.\r\nAdmin user and anonymous pull are disabled. The provided application managed identity principals are granted the AcrPull role, and the deployer is granted a registry-scoped AcrPush role so it can push/pull images (remote builds via `az acr build` additionally rely on the deployer's higher-scope role for scheduleRun/action)." + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 50, + "metadata": { + "description": "Required. Name of the Azure Container Registry. Must be globally unique and 5-50 alphanumeric characters." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Azure region for the registry. Defaults to the resource group location." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to the registry." + } + }, + "sku": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Standard", + "Premium" + ], + "metadata": { + "description": "Optional. SKU for the registry. Premium is required for private networking. Defaults to Standard." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Public network access for the registry. Defaults to Enabled. Note: `az acr build` (ACR Tasks quick builds) and Container Apps image pulls require reachability; disabling public access requires VNet agent pools and private endpoints, so it is left Enabled by default for both WAF and non-WAF deployments." + } + }, + "networkRuleBypassOptions": { + "type": "string", + "defaultValue": "AzureServices", + "allowedValues": [ + "AzureServices", + "None" + ], + "metadata": { + "description": "Optional. Whether to allow trusted Azure services (e.g. ACR Tasks used by `az acr build`) to bypass network rules. Defaults to AzureServices." + } + }, + "acrPullPrincipalIds": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Principal IDs (managed identities) to grant the AcrPull role so they can pull images using identity-based authentication." + } + }, + "buildPrincipalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Principal ID (e.g. the deployer) to grant a registry-scoped AcrPush role so it can push/pull images. Leave empty to skip." + } + }, + "buildPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "metadata": { + "description": "Optional. Principal type for the build principal." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Resource ID of the subnet to host the registry private endpoint. When set (WAF mode), a private endpoint is created so runtime image pulls flow over the private network." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Resource ID of the privatelink.azurecr.io private DNS zone to link the private endpoint to." + } + } + }, + "variables": { + "acrPullRoleDefinitionId": "7f951dda-4ed3-4680-a7ca-43fe172d538d", + "acrPushRoleDefinitionId": "8311e382-0749-4cb8-b61a-304f252e45ec" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "adminUserEnabled": false, + "anonymousPullEnabled": false, + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "networkRuleBypassOptions": "[parameters('networkRuleBypassOptions')]", + "networkRuleSet": "[if(equals(parameters('publicNetworkAccess'), 'Disabled'), createObject('defaultAction', 'Deny'), null())]", + "policies": "[if(equals(parameters('publicNetworkAccess'), 'Disabled'), createObject('exportPolicy', createObject('status', 'disabled')), null())]" + } + }, + { + "condition": "[not(empty(parameters('privateEndpointSubnetResourceId')))]", + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-11-01", + "name": "[format('pep-{0}', parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "subnet": { + "id": "[parameters('privateEndpointSubnetResourceId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[format('pls-{0}', parameters('name'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "groupIds": [ + "registry" + ] + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + ] + }, + { + "condition": "[and(not(empty(parameters('privateEndpointSubnetResourceId'))), not(empty(parameters('privateDnsZoneResourceId'))))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('pep-{0}', parameters('name')), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "privatelink-azurecr-io", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', format('pep-{0}', parameters('name')))]" + ] + }, + { + "copy": { + "name": "acrPullRoleAssignments", + "count": "[length(parameters('acrPullPrincipalIds'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), parameters('acrPullPrincipalIds')[copyIndex()], variables('acrPullRoleDefinitionId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleDefinitionId'))]", + "principalId": "[parameters('acrPullPrincipalIds')[copyIndex()]]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + ] + }, + { + "condition": "[not(empty(parameters('buildPrincipalId')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), parameters('buildPrincipalId'), variables('acrPushRoleDefinitionId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPushRoleDefinitionId'))]", + "principalId": "[parameters('buildPrincipalId')]", + "principalType": "[parameters('buildPrincipalType')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + ] + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container registry." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the container registry." + }, + "value": "[parameters('name')]" + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "The login server (endpoint) of the container registry, e.g. myregistry.azurecr.io." + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "appIdentity", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)]", + "virtualNetwork" + ] + }, "logAnalyticsWorkspace": { "condition": "[and(or(parameters('enableMonitoring'), parameters('enablePrivateNetworking')), not(variables('useExistingLogAnalytics')))]", "type": "Microsoft.Resources/deployments", @@ -35199,13 +35492,7 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cosmosDB)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').appConfig)]", + "avmPrivateDnsZones", "virtualNetwork" ] }, @@ -40649,11 +40936,19 @@ ] } }, + "registries": { + "value": [ + { + "server": "[reference('containerRegistry').outputs.loginServer.value]", + "identity": "[reference('appIdentity').outputs.resourceId.value]" + } + ] + }, "containers": { "value": [ { "name": "backend-api", - "image": "[format('{0}/backend-api:{1}', parameters('containerRegistryEndpoint'), parameters('imageTag'))]", + "image": "[parameters('placeholderContainerImage')]", "env": "[concat(createArray(createObject('name', 'APP_CONFIGURATION_URL', 'value', reference('appConfiguration').outputs.endpoint.value), createObject('name', 'AZURE_CLIENT_ID', 'value', reference('appIdentity').outputs.clientId.value), createObject('name', 'PROCESSOR_CONTROL_URL', 'value', format('https://{0}.internal.{1}', variables('processorContainerAppName'), reference('containerAppsEnvironment').outputs.defaultDomain.value))), if(parameters('enableMonitoring'), createArray(createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('applicationInsights').outputs.connectionString.value)), createArray()))]", "resources": { "cpu": 1, @@ -42267,7 +42562,8 @@ "appConfiguration", "appIdentity", "applicationInsights", - "containerAppsEnvironment" + "containerAppsEnvironment", + "containerRegistry" ] }, "containerAppFrontend": { @@ -42296,11 +42592,19 @@ ] } }, + "registries": { + "value": [ + { + "server": "[reference('containerRegistry').outputs.loginServer.value]", + "identity": "[reference('appIdentity').outputs.resourceId.value]" + } + ] + }, "containers": { "value": [ { "name": "frontend", - "image": "[format('{0}/frontend:{1}', parameters('containerRegistryEndpoint'), parameters('imageTag'))]", + "image": "[parameters('placeholderContainerImage')]", "env": [ { "name": "API_URL", @@ -43915,7 +44219,8 @@ "dependsOn": [ "appIdentity", "containerAppBackend", - "containerAppsEnvironment" + "containerAppsEnvironment", + "containerRegistry" ] }, "containerAppProcessor": { @@ -43944,11 +44249,19 @@ ] } }, + "registries": { + "value": [ + { + "server": "[reference('containerRegistry').outputs.loginServer.value]", + "identity": "[reference('appIdentity').outputs.resourceId.value]" + } + ] + }, "containers": { "value": [ { "name": "processor", - "image": "[format('{0}/processor:{1}', parameters('containerRegistryEndpoint'), parameters('imageTag'))]", + "image": "[parameters('placeholderContainerImage')]", "env": "[concat(createArray(createObject('name', 'APP_CONFIGURATION_URL', 'value', reference('appConfiguration').outputs.endpoint.value), createObject('name', 'AZURE_CLIENT_ID', 'value', reference('appIdentity').outputs.clientId.value), createObject('name', 'AZURE_STORAGE_ACCOUNT_NAME', 'value', reference('storageAccount').outputs.name.value), createObject('name', 'STORAGE_ACCOUNT_NAME', 'value', reference('storageAccount').outputs.name.value), createObject('name', 'CONTROL_API_ENABLED', 'value', '1'), createObject('name', 'CONTROL_API_PORT', 'value', '8080')), if(parameters('enableMonitoring'), createArray(createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('applicationInsights').outputs.connectionString.value)), createArray()))]", "resources": { "cpu": 2, @@ -45546,6 +45859,7 @@ "appIdentity", "applicationInsights", "containerAppsEnvironment", + "containerRegistry", "storageAccount" ] } @@ -45612,6 +45926,34 @@ }, "value": "[resourceGroup().name]" }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "metadata": { + "description": "The name of the dedicated Azure Container Registry." + }, + "value": "[reference('containerRegistry').outputs.name.value]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "metadata": { + "description": "The login server (endpoint) of the dedicated Azure Container Registry." + }, + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "CONTAINER_PROCESSOR_APP_NAME": { + "type": "string", + "metadata": { + "description": "The name of the processor container app." + }, + "value": "[reference('containerAppProcessor').outputs.name.value]" + }, + "AZURE_ENV_IMAGE_TAG": { + "type": "string", + "metadata": { + "description": "The image tag used for deployment-specific container images." + }, + "value": "[parameters('imageTag')]" + }, "deployerObjectId": { "type": "string", "value": "[variables('deployingUserPrincipalId')]" diff --git a/infra/modules/containerRegistry.bicep b/infra/modules/containerRegistry.bicep index 6593f982..97a142d6 100644 --- a/infra/modules/containerRegistry.bicep +++ b/infra/modules/containerRegistry.bicep @@ -50,6 +50,12 @@ param buildPrincipalId string = '' ]) param buildPrincipalType string = 'User' +@description('Optional. Resource ID of the subnet to host the registry private endpoint. When set (WAF mode), a private endpoint is created so runtime image pulls flow over the private network.') +param privateEndpointSubnetResourceId string = '' + +@description('Optional. Resource ID of the privatelink.azurecr.io private DNS zone to link the private endpoint to.') +param privateDnsZoneResourceId string = '' + // AcrPull role definition ID (allows pulling images). var acrPullRoleDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPush role definition ID (least-privilege push/pull for the deployer). @@ -72,6 +78,51 @@ resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { anonymousPullEnabled: false publicNetworkAccess: publicNetworkAccess networkRuleBypassOptions: networkRuleBypassOptions + // WAF-aligned networking: when public access is Disabled, default-deny all + // network traffic (runtime pulls flow over the private endpoint) and disable + // image export. The DisableExport_PublicNetworkAccessMustBeDisabled constraint + // requires public access to be Disabled while exports are disabled, so the + // post-deploy build script toggles both together when it opens the registry. + networkRuleSet: publicNetworkAccess == 'Disabled' ? { defaultAction: 'Deny' } : null + policies: publicNetworkAccess == 'Disabled' ? { exportPolicy: { status: 'disabled' } } : null + } +} + +// WAF: private endpoint for runtime image pulls when public access is disabled. +resource registryPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (!empty(privateEndpointSubnetResourceId)) { + name: 'pep-${name}' + location: location + tags: tags + properties: { + subnet: { + id: privateEndpointSubnetResourceId + } + privateLinkServiceConnections: [ + { + name: 'pls-${name}' + properties: { + privateLinkServiceId: registry.id + groupIds: [ + 'registry' + ] + } + } + ] + } +} + +resource registryPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = if (!empty(privateEndpointSubnetResourceId) && !empty(privateDnsZoneResourceId)) { + parent: registryPrivateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'privatelink-azurecr-io' + properties: { + privateDnsZoneId: privateDnsZoneResourceId + } + } + ] } } diff --git a/scripts/deploy_container_images.ps1 b/scripts/deploy_container_images.ps1 index 81db0b82..e5e44603 100644 --- a/scripts/deploy_container_images.ps1 +++ b/scripts/deploy_container_images.ps1 @@ -118,14 +118,49 @@ function Update-App { if ($LASTEXITCODE -ne 0) { throw "az containerapp update failed for $AppName" } } -# Build & push all images to the dedicated ACR. -Build-Image -ImageName 'backend-api' -ContextDir (Join-Path $RootDir 'src/backend-api') -Build-Image -ImageName 'processor' -ContextDir (Join-Path $RootDir 'src/processor') -Build-Image -ImageName 'frontend' -ContextDir (Join-Path $RootDir 'src/frontend') - -# Point the Container Apps at the freshly built images. -Update-App -AppName $BackendApp -ImageName 'backend-api' -Update-App -AppName $ProcessorApp -ImageName 'processor' -Update-App -AppName $FrontendApp -ImageName 'frontend' +# --------------------------------------------------------------------------- +# WAF (private networking) support. +# +# In WAF mode the registry has public network access DISABLED at rest (with a +# default-deny network rule set and image export disabled); runtime pulls flow +# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the +# registry over its PUBLIC endpoint, so we temporarily relax those settings for +# the build/push and restore them afterwards - including on failure, via finally +# - so the registry is never left publicly reachable. WAF is detected from the +# resource group's 'Type' tag (set to 'WAF' when private networking is enabled). +# --------------------------------------------------------------------------- +$DeploymentType = az group show --name $ResourceGroup --query 'tags.Type' -o tsv 2>$null +if ($DeploymentType -eq 'WAF') { + Write-Host "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" + az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } + az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } + az acr update --name $AcrName --resource-group $ResourceGroup --default-action Allow --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } + Write-Host " Waiting ~45s for the network rule change to propagate..." + Start-Sleep -Seconds 45 +} + +try { + # Build & push all images to the dedicated ACR. + Build-Image -ImageName 'backend-api' -ContextDir (Join-Path $RootDir 'src/backend-api') + Build-Image -ImageName 'processor' -ContextDir (Join-Path $RootDir 'src/processor') + Build-Image -ImageName 'frontend' -ContextDir (Join-Path $RootDir 'src/frontend') + + # Point the Container Apps at the freshly built images. + Update-App -AppName $BackendApp -ImageName 'backend-api' + Update-App -AppName $ProcessorApp -ImageName 'processor' + Update-App -AppName $FrontendApp -ImageName 'frontend' +} +finally { + if ($DeploymentType -eq 'WAF') { + Write-Host "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" + az acr update --name $AcrName --resource-group $ResourceGroup --default-action Deny --output none --only-show-errors + az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled false --output none --only-show-errors + az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports false --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to fully restore ACR configuration; verify manually." } + } +} Write-Host "==> [deploy_container_images] Completed successfully." diff --git a/scripts/deploy_container_images.sh b/scripts/deploy_container_images.sh index 10a1f21b..e7c97145 100644 --- a/scripts/deploy_container_images.sh +++ b/scripts/deploy_container_images.sh @@ -88,6 +88,44 @@ echo " Registry : $REGISTRY_ENDPOINT ($ACR_NAME)" echo " Resource group : $RESOURCE_GROUP" echo " Image tag : $IMAGE_TAG" +# --------------------------------------------------------------------------- +# WAF (private networking) support. +# +# In WAF mode the registry has public network access DISABLED at rest (with a +# default-deny network rule set and image export disabled); runtime pulls flow +# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the +# registry over its PUBLIC endpoint, so we must temporarily relax those settings +# for the build/push and restore them afterwards - including on failure, via a +# trap - so the registry is never left publicly reachable. +# +# WAF is detected from the resource group's `Type` tag (set to 'WAF' by the +# infrastructure when private networking is enabled). +# --------------------------------------------------------------------------- +DEPLOYMENT_TYPE="$(az group show --name "$RESOURCE_GROUP" --query 'tags.Type' -o tsv 2>/dev/null || true)" + +relock_acr() { + if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then + echo "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Deny --output none --only-show-errors \ + || echo "WARNING: failed to restore ACR default-action; verify manually." >&2 + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled false --output none --only-show-errors \ + || echo "WARNING: failed to disable ACR public network access; verify manually." >&2 + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports false --output none --only-show-errors \ + || echo "WARNING: failed to disable ACR exports; verify manually." >&2 + fi +} + +if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then + echo "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" + # Ensure the locked-down state is restored on any exit (success or failure). + trap relock_acr EXIT + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports true --output none --only-show-errors + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled true --output none --only-show-errors + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Allow --output none --only-show-errors + echo " Waiting ~45s for the network rule change to propagate..." + sleep 45 +fi + # --------------------------------------------------------------------------- # Remote build helper - uses ACR Tasks (az acr build) so no local Docker daemon # is required on the machine running the deployment. From 64a6ad0f84a467325bb218d0501ef4dac684ed12 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Wed, 8 Jul 2026 14:31:20 +0530 Subject: [PATCH 02/16] fix: update deployment guide to include script for building and pushing application images --- docs/DeploymentGuide.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 3e3b4537..b22d9966 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -294,7 +294,21 @@ azd up **⚠️ Deployment Issues:** If you encounter errors or timeouts, try a different region as there may be capacity constraints. For detailed error solutions, see our [Troubleshooting Guide](./TroubleShootingSteps.md). -### 4.3 Get Application URL +### 4.3 Run the script to build and push the application images + +Build and push the frontend, backend, and processor images to the dedicated ACR, then update the Container Apps to use them. This step is **not run automatically** by `azd up` β€” run it from the repository root after deployment: + +```powershell +# PowerShell +./scripts/deploy_container_images.ps1 +``` + +```bash +# Bash +bash ./scripts/deploy_container_images.sh +``` + +### 4.4 Get Application URL After successful deployment: 1. Open [Azure Portal](https://portal.azure.com/) @@ -315,7 +329,7 @@ After successful deployment: ### 5.2 Verify Deployment -1. Access your application using the URL from Step 4.3 +1. Access your application using the URL from Step 4.4 2. Confirm the application loads successfully 3. Verify you can sign in with your authenticated account From 949c253cca1e8a0f968017ee329bf81b969d2d82 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Wed, 8 Jul 2026 14:50:49 +0530 Subject: [PATCH 03/16] fix: address Copilot review comments on WAF ACR toggle (PR #318) - ps1: move WAF ACR relaxation inside try{} so finally{} always restores the locked-down state after any partial change - ps1: check each restore command's exit code and warn per-step - containerRegistry.bicep: update publicNetworkAccess description to reflect WAF-disabled-at-rest behavior with script-driven temporary enablement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/modules/containerRegistry.bicep | 2 +- scripts/deploy_container_images.ps1 | 34 +++++++++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/infra/modules/containerRegistry.bicep b/infra/modules/containerRegistry.bicep index 97a142d6..24b276e5 100644 --- a/infra/modules/containerRegistry.bicep +++ b/infra/modules/containerRegistry.bicep @@ -20,7 +20,7 @@ param tags object = {} ]) param sku string = 'Standard' -@description('Optional. Public network access for the registry. Defaults to Enabled. Note: `az acr build` (ACR Tasks quick builds) and Container Apps image pulls require reachability; disabling public access requires VNet agent pools and private endpoints, so it is left Enabled by default for both WAF and non-WAF deployments.') +@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') @allowed([ 'Enabled' 'Disabled' diff --git a/scripts/deploy_container_images.ps1 b/scripts/deploy_container_images.ps1 index e5e44603..ef858832 100644 --- a/scripts/deploy_container_images.ps1 +++ b/scripts/deploy_container_images.ps1 @@ -130,19 +130,25 @@ function Update-App { # resource group's 'Type' tag (set to 'WAF' when private networking is enabled). # --------------------------------------------------------------------------- $DeploymentType = az group show --name $ResourceGroup --query 'tags.Type' -o tsv 2>$null -if ($DeploymentType -eq 'WAF') { - Write-Host "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" - az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports true --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } - az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled true --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } - az acr update --name $AcrName --resource-group $ResourceGroup --default-action Allow --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } - Write-Host " Waiting ~45s for the network rule change to propagate..." - Start-Sleep -Seconds 45 -} try { + # In WAF mode, temporarily relax the ACR restrictions so the remote build + # (az acr build / ACR Tasks) can reach the registry over its public endpoint. + # This is done inside the try so the finally block always restores the + # locked-down state - even if one of the relaxation steps partially succeeds + # and a later one fails. + if ($DeploymentType -eq 'WAF') { + Write-Host "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" + az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } + az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } + az acr update --name $AcrName --resource-group $ResourceGroup --default-action Allow --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } + Write-Host " Waiting ~45s for the network rule change to propagate..." + Start-Sleep -Seconds 45 + } + # Build & push all images to the dedicated ACR. Build-Image -ImageName 'backend-api' -ContextDir (Join-Path $RootDir 'src/backend-api') Build-Image -ImageName 'processor' -ContextDir (Join-Path $RootDir 'src/processor') @@ -156,10 +162,14 @@ try { finally { if ($DeploymentType -eq 'WAF') { Write-Host "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" + $restoreFailed = $false az acr update --name $AcrName --resource-group $ResourceGroup --default-action Deny --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to restore ACR default-action to Deny." } az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled false --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR public network access." } az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports false --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to fully restore ACR configuration; verify manually." } + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR exports." } + if ($restoreFailed) { Write-Warning "ACR was not fully restored to its locked-down state; verify manually that public network access is disabled." } } } From 7d5223fd66f78abcd645199ad61981fdd99c53bb Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Wed, 8 Jul 2026 15:49:55 +0530 Subject: [PATCH 04/16] fix: update default image tag and placeholder container image for consistency --- infra/main.bicep | 8 ++++---- infra/main.json | 14 +++++++------- scripts/deploy_container_images.ps1 | 2 +- scripts/deploy_container_images.sh | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index df834653..63145e90 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -45,12 +45,12 @@ param azureAiServiceLocation string #disable-next-line no-unused-params param containerRegistryEndpoint string = '' -@description('Optional. The image tag to use for container images. Defaults to "latest_v2".') -param imageTag string = 'latest_v2' +@description('Optional. The image tag to use for container images. Defaults to "latest".') +param imageTag string = 'latest' @description('''Optional. Placeholder container image used to initially provision the container apps. -The dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps quickstart image.''') -param placeholderContainerImage string = 'mcr.microsoft.com/k8se/quickstart:latest' +The dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image.''') +param placeholderContainerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' @minLength(1) @allowed(['Standard', 'GlobalStandard']) diff --git a/infra/main.json b/infra/main.json index 838641d7..aa12efb4 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.43.8.12551", - "templateHash": "17608527028333640769" + "templateHash": "8577992003868472105" } }, "parameters": { @@ -70,16 +70,16 @@ }, "imageTag": { "type": "string", - "defaultValue": "latest_v2", + "defaultValue": "latest", "metadata": { - "description": "Optional. The image tag to use for container images. Defaults to \"latest_v2\"." + "description": "Optional. The image tag to use for container images. Defaults to \"latest\"." } }, "placeholderContainerImage": { "type": "string", - "defaultValue": "mcr.microsoft.com/k8se/quickstart:latest", + "defaultValue": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", "metadata": { - "description": "Optional. Placeholder container image used to initially provision the container apps.\nThe dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps quickstart image." + "description": "Optional. Placeholder container image used to initially provision the container apps.\nThe dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image." } }, "deploymentType": { @@ -895,7 +895,7 @@ "_generator": { "name": "bicep", "version": "0.43.8.12551", - "templateHash": "10052698379792252001" + "templateHash": "17231291582633402413" }, "name": "Dedicated Azure Container Registry", "description": "Provisions a dedicated Azure Container Registry (ACR) for a single deployment and configures identity-based authentication.\r\nAdmin user and anonymous pull are disabled. The provided application managed identity principals are granted the AcrPull role, and the deployer is granted a registry-scoped AcrPush role so it can push/pull images (remote builds via `az acr build` additionally rely on the deployer's higher-scope role for scheduleRun/action)." @@ -942,7 +942,7 @@ "Disabled" ], "metadata": { - "description": "Optional. Public network access for the registry. Defaults to Enabled. Note: `az acr build` (ACR Tasks quick builds) and Container Apps image pulls require reachability; disabling public access requires VNet agent pools and private endpoints, so it is left Enabled by default for both WAF and non-WAF deployments." + "description": "Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled." } }, "networkRuleBypassOptions": { diff --git a/scripts/deploy_container_images.ps1 b/scripts/deploy_container_images.ps1 index ef858832..64145853 100644 --- a/scripts/deploy_container_images.ps1 +++ b/scripts/deploy_container_images.ps1 @@ -25,7 +25,7 @@ Write-Host "==> [deploy_container_images] Building and pushing images to the ded $AcrName = $env:AZURE_CONTAINER_REGISTRY_NAME $RegistryEndpoint = $env:AZURE_CONTAINER_REGISTRY_ENDPOINT $ResourceGroup = $env:AZURE_RESOURCE_GROUP -$ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { 'latest_v2' } +$ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { 'latest' } $BackendApp = $env:CONTAINER_API_APP_NAME $FrontendApp = $env:CONTAINER_WEB_APP_NAME $ProcessorApp = $env:CONTAINER_PROCESSOR_APP_NAME diff --git a/scripts/deploy_container_images.sh b/scripts/deploy_container_images.sh index e7c97145..8de0cfc3 100644 --- a/scripts/deploy_container_images.sh +++ b/scripts/deploy_container_images.sh @@ -23,7 +23,7 @@ echo "==> [deploy_container_images] Building and pushing images to the dedicated ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" -IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-latest_v2}" +IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-latest}" BACKEND_APP="${CONTAINER_API_APP_NAME:-}" FRONTEND_APP="${CONTAINER_WEB_APP_NAME:-}" PROCESSOR_APP="${CONTAINER_PROCESSOR_APP_NAME:-}" From 448e1f1771793cfb3103a7347bf05dea3fd03a7a Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Wed, 8 Jul 2026 17:00:04 +0530 Subject: [PATCH 05/16] fix: ensure default image tag is applied after environment variable fallback --- scripts/deploy_container_images.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/deploy_container_images.sh b/scripts/deploy_container_images.sh index 8de0cfc3..aa509d31 100644 --- a/scripts/deploy_container_images.sh +++ b/scripts/deploy_container_images.sh @@ -23,7 +23,7 @@ echo "==> [deploy_container_images] Building and pushing images to the dedicated ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" -IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-latest}" +IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-}" BACKEND_APP="${CONTAINER_API_APP_NAME:-}" FRONTEND_APP="${CONTAINER_WEB_APP_NAME:-}" PROCESSOR_APP="${CONTAINER_PROCESSOR_APP_NAME:-}" @@ -53,6 +53,10 @@ fi # Derive the login server from the registry name if it was not provided. REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-${ACR_NAME}.azurecr.io}" +# Apply the default image tag only after the azd fallback, so an explicitly +# configured tag (env var or `azd env get-values`) is honored. +IMAGE_TAG="${IMAGE_TAG:-latest}" + missing=() [[ -z "$ACR_NAME" ]] && missing+=("AZURE_CONTAINER_REGISTRY_NAME") [[ -z "$RESOURCE_GROUP" ]] && missing+=("AZURE_RESOURCE_GROUP") From 174e71a7e3d4abee6de676d70c982df2b0e803f6 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Mon, 13 Jul 2026 18:05:05 +0530 Subject: [PATCH 06/16] fix: include subscription ID in environment variable checks for container deployment scripts --- scripts/deploy_container_images.ps1 | 16 +++++++++++++++- scripts/deploy_container_images.sh | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/deploy_container_images.ps1 b/scripts/deploy_container_images.ps1 index 64145853..25e772a3 100644 --- a/scripts/deploy_container_images.ps1 +++ b/scripts/deploy_container_images.ps1 @@ -25,6 +25,7 @@ Write-Host "==> [deploy_container_images] Building and pushing images to the ded $AcrName = $env:AZURE_CONTAINER_REGISTRY_NAME $RegistryEndpoint = $env:AZURE_CONTAINER_REGISTRY_ENDPOINT $ResourceGroup = $env:AZURE_RESOURCE_GROUP +$SubscriptionId = $env:AZURE_SUBSCRIPTION_ID $ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { 'latest' } $BackendApp = $env:CONTAINER_API_APP_NAME $FrontendApp = $env:CONTAINER_WEB_APP_NAME @@ -34,7 +35,7 @@ $ProcessorApp = $env:CONTAINER_PROCESSOR_APP_NAME # covers not just the registry/resource group but also the container app names # and registry endpoint, so a partially-populated environment does not silently # skip image updates and leave apps on the placeholder image. -if ([string]::IsNullOrEmpty($AcrName) -or [string]::IsNullOrEmpty($ResourceGroup) -or [string]::IsNullOrEmpty($RegistryEndpoint) -or [string]::IsNullOrEmpty($BackendApp) -or [string]::IsNullOrEmpty($FrontendApp) -or [string]::IsNullOrEmpty($ProcessorApp)) { +if ([string]::IsNullOrEmpty($AcrName) -or [string]::IsNullOrEmpty($ResourceGroup) -or [string]::IsNullOrEmpty($RegistryEndpoint) -or [string]::IsNullOrEmpty($BackendApp) -or [string]::IsNullOrEmpty($FrontendApp) -or [string]::IsNullOrEmpty($ProcessorApp) -or [string]::IsNullOrEmpty($SubscriptionId)) { if (Get-Command azd -ErrorAction SilentlyContinue) { Write-Host "==> Loading missing values from 'azd env get-values'" foreach ($line in (azd env get-values)) { @@ -44,6 +45,7 @@ if ([string]::IsNullOrEmpty($AcrName) -or [string]::IsNullOrEmpty($ResourceGroup 'AZURE_CONTAINER_REGISTRY_NAME' { if (-not $AcrName) { $AcrName = $v } } 'AZURE_CONTAINER_REGISTRY_ENDPOINT' { if (-not $RegistryEndpoint) { $RegistryEndpoint = $v } } 'AZURE_RESOURCE_GROUP' { if (-not $ResourceGroup) { $ResourceGroup = $v } } + 'AZURE_SUBSCRIPTION_ID' { if (-not $SubscriptionId) { $SubscriptionId = $v } } 'AZURE_ENV_IMAGE_TAG' { if (-not $env:AZURE_ENV_IMAGE_TAG) { $ImageTag = $v } } 'CONTAINER_API_APP_NAME' { if (-not $BackendApp) { $BackendApp = $v } } 'CONTAINER_WEB_APP_NAME' { if (-not $FrontendApp) { $FrontendApp = $v } } @@ -84,6 +86,18 @@ if ($LASTEXITCODE -ne 0) { exit 1 } +# Pin the Azure CLI to the azd environment's subscription. `az acr build` and +# `az containerapp update` use the CLI's active subscription, which may differ +# from the azd environment when multiple subscriptions are available - without +# this the build/update could target the wrong subscription (or fail). +if (-not [string]::IsNullOrEmpty($SubscriptionId)) { + az account set --subscription $SubscriptionId 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to set Azure CLI subscription to '$SubscriptionId'. Verify the subscription ID and that your account has access to it." + exit 1 + } +} + # Resolve the repository root (this script lives in /scripts). $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $RootDir = Split-Path -Parent $ScriptDir diff --git a/scripts/deploy_container_images.sh b/scripts/deploy_container_images.sh index aa509d31..4f277432 100644 --- a/scripts/deploy_container_images.sh +++ b/scripts/deploy_container_images.sh @@ -23,6 +23,7 @@ echo "==> [deploy_container_images] Building and pushing images to the dedicated ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" +SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-}" IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-}" BACKEND_APP="${CONTAINER_API_APP_NAME:-}" FRONTEND_APP="${CONTAINER_WEB_APP_NAME:-}" @@ -32,7 +33,7 @@ PROCESSOR_APP="${CONTAINER_PROCESSOR_APP_NAME:-}" # This covers not just the registry/resource group but also the container app # names and registry endpoint, so a partially-populated environment does not # silently skip image updates and leave apps on the placeholder image. -if [[ -z "$ACR_NAME" || -z "$RESOURCE_GROUP" || -z "$REGISTRY_ENDPOINT" || -z "$BACKEND_APP" || -z "$FRONTEND_APP" || -z "$PROCESSOR_APP" ]]; then +if [[ -z "$ACR_NAME" || -z "$RESOURCE_GROUP" || -z "$REGISTRY_ENDPOINT" || -z "$BACKEND_APP" || -z "$FRONTEND_APP" || -z "$PROCESSOR_APP" || -z "$SUBSCRIPTION_ID" ]]; then if command -v azd >/dev/null 2>&1; then echo "==> Loading missing values from 'azd env get-values'" while IFS='=' read -r key value; do @@ -41,6 +42,7 @@ if [[ -z "$ACR_NAME" || -z "$RESOURCE_GROUP" || -z "$REGISTRY_ENDPOINT" || -z "$ AZURE_CONTAINER_REGISTRY_NAME) ACR_NAME="${ACR_NAME:-$value}" ;; AZURE_CONTAINER_REGISTRY_ENDPOINT) REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-$value}" ;; AZURE_RESOURCE_GROUP) RESOURCE_GROUP="${RESOURCE_GROUP:-$value}" ;; + AZURE_SUBSCRIPTION_ID) SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-$value}" ;; AZURE_ENV_IMAGE_TAG) IMAGE_TAG="${IMAGE_TAG:-$value}" ;; CONTAINER_API_APP_NAME) BACKEND_APP="${BACKEND_APP:-$value}" ;; CONTAINER_WEB_APP_NAME) FRONTEND_APP="${FRONTEND_APP:-$value}" ;; @@ -84,6 +86,18 @@ if ! az account show >/dev/null 2>&1; then exit 1 fi +# Pin the Azure CLI to the azd environment's subscription. `az acr build` and +# `az containerapp update` use the CLI's active subscription, which may differ +# from the azd environment when the user has multiple subscriptions - without +# this the build/update could target the wrong subscription (or fail). +if [[ -n "$SUBSCRIPTION_ID" ]]; then + if ! az account set --subscription "$SUBSCRIPTION_ID" 2>/dev/null; then + echo "ERROR: Failed to set Azure CLI subscription to '$SUBSCRIPTION_ID'." >&2 + echo " Verify the subscription ID and that your account has access to it." >&2 + exit 1 + fi +fi + # Resolve the repository root (this script lives in /scripts). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" From 3c96cb6fb6b5852c7e0c39282a72aeac5fab30c7 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Limited)" Date: Thu, 16 Jul 2026 12:12:59 +0530 Subject: [PATCH 07/16] Update Owners ID for Vinay and Prajwal --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94a0c2eb..78c821dd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repo. -* @Avijit-Microsoft @Roopan-Microsoft @Prajwal1-Microsoft @VinaySh-Microsoft @aniaroramsft @Dongbumlee @sethsteenken @toherman-msft @nchandhi @dgp10801 +* @Avijit-Microsoft @Roopan-Microsoft @Prajwal-Microsoft @Vinay-Microsoft @aniaroramsft @Dongbumlee @sethsteenken @toherman-msft @nchandhi @dgp10801 From 03aa515bd14e0eecd9b56144fcb20b7263f3daa5 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 08:51:37 +0530 Subject: [PATCH 08/16] fix: update placeholder image script references and add new build/push scripts for container images --- infra/main.bicep | 2 +- infra/main.json | 4 +- infra/modules/containerRegistry.bicep | 2 +- scripts/acr_build_push.ps1 | 190 ++++++++++++++++++++++++++ scripts/acr_build_push.sh | 188 +++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 scripts/acr_build_push.ps1 create mode 100644 scripts/acr_build_push.sh diff --git a/infra/main.bicep b/infra/main.bicep index 63145e90..4bb4ec6e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -49,7 +49,7 @@ param containerRegistryEndpoint string = '' param imageTag string = 'latest' @description('''Optional. Placeholder container image used to initially provision the container apps. -The dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image.''') +The dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/acr_build_push.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image.''') param placeholderContainerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' @minLength(1) diff --git a/infra/main.json b/infra/main.json index aa12efb4..d39ac842 100644 --- a/infra/main.json +++ b/infra/main.json @@ -79,7 +79,7 @@ "type": "string", "defaultValue": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", "metadata": { - "description": "Optional. Placeholder container image used to initially provision the container apps.\nThe dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/deploy_container_images.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image." + "description": "Optional. Placeholder container image used to initially provision the container apps.\nThe dedicated Azure Container Registry is empty right after infrastructure provisioning, so a public image is used as the default allowed image until the post-deployment script (scripts/acr_build_push.*) builds and pushes the deployment-specific images and updates the apps. Defaults to the Azure Container Apps hello-world image." } }, "deploymentType": { @@ -942,7 +942,7 @@ "Disabled" ], "metadata": { - "description": "Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled." + "description": "Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/acr_build_push.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled." } }, "networkRuleBypassOptions": { diff --git a/infra/modules/containerRegistry.bicep b/infra/modules/containerRegistry.bicep index 24b276e5..7ac84435 100644 --- a/infra/modules/containerRegistry.bicep +++ b/infra/modules/containerRegistry.bicep @@ -20,7 +20,7 @@ param tags object = {} ]) param sku string = 'Standard' -@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') +@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/acr_build_push.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') @allowed([ 'Enabled' 'Disabled' diff --git a/scripts/acr_build_push.ps1 b/scripts/acr_build_push.ps1 new file mode 100644 index 00000000..8bae78ca --- /dev/null +++ b/scripts/acr_build_push.ps1 @@ -0,0 +1,190 @@ +<# +.SYNOPSIS + Separate post-deployment script (Windows / PowerShell) that runs FIRST, + before any existing post-deployment scripts. + +.DESCRIPTION + Builds the deployment-specific container images using Azure Container + Registry *remote* builds (az acr build - no local Docker required) and + pushes them to the dedicated, per-deployment ACR. It then updates the + Container Apps to use the freshly pushed images. + + This does NOT depend on a shared/public registry or anonymous pull. Image + pulls at runtime use identity-based authentication (the app's managed + identity has the AcrPull role on the dedicated ACR). +#> + +$ErrorActionPreference = 'Stop' + +Write-Host "==> [acr_build_push] Building and pushing images to the dedicated ACR (remote build)" + +# --------------------------------------------------------------------------- +# Resolve required values. azd exports deployment outputs as environment +# variables inside hooks; fall back to `azd env get-values` if needed. +# --------------------------------------------------------------------------- +$AcrName = $env:AZURE_CONTAINER_REGISTRY_NAME +$RegistryEndpoint = $env:AZURE_CONTAINER_REGISTRY_ENDPOINT +$ResourceGroup = $env:AZURE_RESOURCE_GROUP +$SubscriptionId = $env:AZURE_SUBSCRIPTION_ID +$ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { 'latest' } +$BackendApp = $env:CONTAINER_API_APP_NAME +$FrontendApp = $env:CONTAINER_WEB_APP_NAME +$ProcessorApp = $env:CONTAINER_PROCESSOR_APP_NAME + +# Load values from `azd env get-values` when any required value is missing. This +# covers not just the registry/resource group but also the container app names +# and registry endpoint, so a partially-populated environment does not silently +# skip image updates and leave apps on the placeholder image. +if ([string]::IsNullOrEmpty($AcrName) -or [string]::IsNullOrEmpty($ResourceGroup) -or [string]::IsNullOrEmpty($RegistryEndpoint) -or [string]::IsNullOrEmpty($BackendApp) -or [string]::IsNullOrEmpty($FrontendApp) -or [string]::IsNullOrEmpty($ProcessorApp) -or [string]::IsNullOrEmpty($SubscriptionId)) { + if (Get-Command azd -ErrorAction SilentlyContinue) { + Write-Host "==> Loading missing values from 'azd env get-values'" + foreach ($line in (azd env get-values)) { + if ($line -match '^(?[A-Za-z0-9_]+)="?(?.*?)"?$') { + $k = $Matches['k']; $v = $Matches['v'] + switch ($k) { + 'AZURE_CONTAINER_REGISTRY_NAME' { if (-not $AcrName) { $AcrName = $v } } + 'AZURE_CONTAINER_REGISTRY_ENDPOINT' { if (-not $RegistryEndpoint) { $RegistryEndpoint = $v } } + 'AZURE_RESOURCE_GROUP' { if (-not $ResourceGroup) { $ResourceGroup = $v } } + 'AZURE_SUBSCRIPTION_ID' { if (-not $SubscriptionId) { $SubscriptionId = $v } } + 'AZURE_ENV_IMAGE_TAG' { if (-not $env:AZURE_ENV_IMAGE_TAG) { $ImageTag = $v } } + 'CONTAINER_API_APP_NAME' { if (-not $BackendApp) { $BackendApp = $v } } + 'CONTAINER_WEB_APP_NAME' { if (-not $FrontendApp) { $FrontendApp = $v } } + 'CONTAINER_PROCESSOR_APP_NAME' { if (-not $ProcessorApp) { $ProcessorApp = $v } } + } + } + } + } +} + +# Derive the login server from the registry name if it was not provided. +if ([string]::IsNullOrEmpty($RegistryEndpoint)) { + $RegistryEndpoint = "$AcrName.azurecr.io" +} + +$missing = @() +if ([string]::IsNullOrEmpty($AcrName)) { $missing += 'AZURE_CONTAINER_REGISTRY_NAME' } +if ([string]::IsNullOrEmpty($ResourceGroup)) { $missing += 'AZURE_RESOURCE_GROUP' } +if ($missing.Count -gt 0) { + Write-Error "Missing required deployment values: $($missing -join ', '). Ensure infrastructure has been provisioned (azd provision) first." + exit 1 +} + +# Ensure the Azure CLI is installed before any `az` invocation, so a missing +# CLI produces a clear, actionable error instead of an opaque failure in the +# azd hook logs. +if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + Write-Error "Azure CLI ('az') is not installed or not on PATH. Install it from https://learn.microsoft.com/cli/azure/install-azure-cli and re-run." + exit 1 +} + +# Ensure the Azure CLI has a valid, non-expired login. `az acr build` and +# `az containerapp update` authenticate via the az CLI (separate from azd), so a +# stale/expired token here would otherwise fail part-way through the build. +az account show --output none 2>$null +if ($LASTEXITCODE -ne 0) { + Write-Error "Azure CLI is not authenticated or its token has expired. Run 'az login' (add '--tenant ' if needed) and re-run this script." + exit 1 +} + +# Pin the Azure CLI to the azd environment's subscription. `az acr build` and +# `az containerapp update` use the CLI's active subscription, which may differ +# from the azd environment when multiple subscriptions are available - without +# this the build/update could target the wrong subscription (or fail). +if (-not [string]::IsNullOrEmpty($SubscriptionId)) { + az account set --subscription $SubscriptionId 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to set Azure CLI subscription to '$SubscriptionId'. Verify the subscription ID and that your account has access to it." + exit 1 + } +} + +# Resolve the repository root (this script lives in /scripts). +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RootDir = Split-Path -Parent $ScriptDir + +Write-Host " Registry : $RegistryEndpoint ($AcrName)" +Write-Host " Resource group : $ResourceGroup" +Write-Host " Image tag : $ImageTag" + +function Build-Image { + param([string]$ImageName, [string]$ContextDir) + Write-Host "==> Remote build (az acr build): ${ImageName}:$ImageTag" + az acr build ` + --registry $AcrName ` + --image "${ImageName}:$ImageTag" ` + --file (Join-Path $ContextDir 'Dockerfile') ` + $ContextDir + if ($LASTEXITCODE -ne 0) { throw "az acr build failed for $ImageName" } +} + +function Update-App { + param([string]$AppName, [string]$ImageName) + if ([string]::IsNullOrEmpty($AppName)) { + Write-Error "Container app name for '$ImageName' is not set; cannot update its image. Ensure the deployment outputs / azd environment include the container app names so the app is not left on the placeholder image." + exit 1 + } + Write-Host "==> Updating container app '$AppName' -> $RegistryEndpoint/${ImageName}:$ImageTag" + az containerapp update ` + --name $AppName ` + --resource-group $ResourceGroup ` + --image "$RegistryEndpoint/${ImageName}:$ImageTag" ` + --output none + if ($LASTEXITCODE -ne 0) { throw "az containerapp update failed for $AppName" } +} + +# --------------------------------------------------------------------------- +# WAF (private networking) support. +# +# In WAF mode the registry has public network access DISABLED at rest (with a +# default-deny network rule set and image export disabled); runtime pulls flow +# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the +# registry over its PUBLIC endpoint, so we temporarily relax those settings for +# the build/push and restore them afterwards - including on failure, via finally +# - so the registry is never left publicly reachable. WAF is detected from the +# resource group's 'Type' tag (set to 'WAF' when private networking is enabled). +# --------------------------------------------------------------------------- +$DeploymentType = az group show --name $ResourceGroup --query 'tags.Type' -o tsv 2>$null + +try { + # In WAF mode, temporarily relax the ACR restrictions so the remote build + # (az acr build / ACR Tasks) can reach the registry over its public endpoint. + # This is done inside the try so the finally block always restores the + # locked-down state - even if one of the relaxation steps partially succeeds + # and a later one fails. + if ($DeploymentType -eq 'WAF') { + Write-Host "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" + az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } + az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled true --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } + az acr update --name $AcrName --resource-group $ResourceGroup --default-action Allow --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } + Write-Host " Waiting ~45s for the network rule change to propagate..." + Start-Sleep -Seconds 45 + } + + # Build & push all images to the dedicated ACR. + Build-Image -ImageName 'backend-api' -ContextDir (Join-Path $RootDir 'src/backend-api') + Build-Image -ImageName 'processor' -ContextDir (Join-Path $RootDir 'src/processor') + Build-Image -ImageName 'frontend' -ContextDir (Join-Path $RootDir 'src/frontend') + + # Point the Container Apps at the freshly built images. + Update-App -AppName $BackendApp -ImageName 'backend-api' + Update-App -AppName $ProcessorApp -ImageName 'processor' + Update-App -AppName $FrontendApp -ImageName 'frontend' +} +finally { + if ($DeploymentType -eq 'WAF') { + Write-Host "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" + $restoreFailed = $false + az acr update --name $AcrName --resource-group $ResourceGroup --default-action Deny --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to restore ACR default-action to Deny." } + az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled false --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR public network access." } + az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports false --output none --only-show-errors + if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR exports." } + if ($restoreFailed) { Write-Warning "ACR was not fully restored to its locked-down state; verify manually that public network access is disabled." } + } +} + +Write-Host "==> [acr_build_push] Completed successfully." diff --git a/scripts/acr_build_push.sh b/scripts/acr_build_push.sh new file mode 100644 index 00000000..6872fb87 --- /dev/null +++ b/scripts/acr_build_push.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# +# acr_build_push.sh +# +# Separate post-deployment script that runs FIRST (before any existing +# post-deployment scripts). It builds the deployment-specific container images +# using Azure Container Registry *remote* builds (az acr build - no local Docker +# required) and pushes them to the dedicated, per-deployment ACR. It then updates +# the Container Apps to use the freshly pushed images. +# +# This intentionally does NOT depend on a shared/public registry or anonymous +# pull. Image pulls at runtime use identity-based authentication (the app's +# managed identity has the AcrPull role on the dedicated ACR). +# +set -euo pipefail + +echo "==> [acr_build_push] Building and pushing images to the dedicated ACR (remote build)" + +# --------------------------------------------------------------------------- +# Resolve required values. azd exports deployment outputs as environment +# variables inside hooks; fall back to `azd env get-values` if needed. +# --------------------------------------------------------------------------- +ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" +REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" +RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" +SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-}" +IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-}" +BACKEND_APP="${CONTAINER_API_APP_NAME:-}" +FRONTEND_APP="${CONTAINER_WEB_APP_NAME:-}" +PROCESSOR_APP="${CONTAINER_PROCESSOR_APP_NAME:-}" + +# Load values from `azd env get-values` when any required value is missing. +# This covers not just the registry/resource group but also the container app +# names and registry endpoint, so a partially-populated environment does not +# silently skip image updates and leave apps on the placeholder image. +if [[ -z "$ACR_NAME" || -z "$RESOURCE_GROUP" || -z "$REGISTRY_ENDPOINT" || -z "$BACKEND_APP" || -z "$FRONTEND_APP" || -z "$PROCESSOR_APP" || -z "$SUBSCRIPTION_ID" ]]; then + if command -v azd >/dev/null 2>&1; then + echo "==> Loading missing values from 'azd env get-values'" + while IFS='=' read -r key value; do + value="${value%\"}"; value="${value#\"}" + case "$key" in + AZURE_CONTAINER_REGISTRY_NAME) ACR_NAME="${ACR_NAME:-$value}" ;; + AZURE_CONTAINER_REGISTRY_ENDPOINT) REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-$value}" ;; + AZURE_RESOURCE_GROUP) RESOURCE_GROUP="${RESOURCE_GROUP:-$value}" ;; + AZURE_SUBSCRIPTION_ID) SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-$value}" ;; + AZURE_ENV_IMAGE_TAG) IMAGE_TAG="${IMAGE_TAG:-$value}" ;; + CONTAINER_API_APP_NAME) BACKEND_APP="${BACKEND_APP:-$value}" ;; + CONTAINER_WEB_APP_NAME) FRONTEND_APP="${FRONTEND_APP:-$value}" ;; + CONTAINER_PROCESSOR_APP_NAME) PROCESSOR_APP="${PROCESSOR_APP:-$value}" ;; + esac + done < <(azd env get-values 2>/dev/null || true) + fi +fi + +# Derive the login server from the registry name if it was not provided. +REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-${ACR_NAME}.azurecr.io}" + +# Apply the default image tag only after the azd fallback, so an explicitly +# configured tag (env var or `azd env get-values`) is honored. +IMAGE_TAG="${IMAGE_TAG:-latest}" + +missing=() +[[ -z "$ACR_NAME" ]] && missing+=("AZURE_CONTAINER_REGISTRY_NAME") +[[ -z "$RESOURCE_GROUP" ]] && missing+=("AZURE_RESOURCE_GROUP") +if [[ ${#missing[@]} -gt 0 ]]; then + echo "ERROR: Missing required deployment values: ${missing[*]}" >&2 + echo " Ensure infrastructure has been provisioned (azd provision) first." >&2 + exit 1 +fi + +# Ensure the Azure CLI is installed before any `az` invocation. Under `set -e` +# a missing `az` would otherwise fail with an opaque "command not found" that is +# hard to diagnose in CI/azd hook logs. +if ! command -v az >/dev/null 2>&1; then + echo "ERROR: Azure CLI ('az') is not installed or not on PATH." >&2 + echo " Install it from https://learn.microsoft.com/cli/azure/install-azure-cli and re-run." >&2 + exit 1 +fi + +# Ensure the Azure CLI has a valid, non-expired login. `az acr build` and +# `az containerapp update` authenticate via the az CLI (separate from azd), so a +# stale/expired token here would otherwise fail part-way through the build. +if ! az account show >/dev/null 2>&1; then + echo "ERROR: Azure CLI is not authenticated or its token has expired." >&2 + echo " Run 'az login' (add '--tenant ' if needed) and re-run this script." >&2 + exit 1 +fi + +# Pin the Azure CLI to the azd environment's subscription. `az acr build` and +# `az containerapp update` use the CLI's active subscription, which may differ +# from the azd environment when the user has multiple subscriptions - without +# this the build/update could target the wrong subscription (or fail). +if [[ -n "$SUBSCRIPTION_ID" ]]; then + if ! az account set --subscription "$SUBSCRIPTION_ID" 2>/dev/null; then + echo "ERROR: Failed to set Azure CLI subscription to '$SUBSCRIPTION_ID'." >&2 + echo " Verify the subscription ID and that your account has access to it." >&2 + exit 1 + fi +fi + +# Resolve the repository root (this script lives in /scripts). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo " Registry : $REGISTRY_ENDPOINT ($ACR_NAME)" +echo " Resource group : $RESOURCE_GROUP" +echo " Image tag : $IMAGE_TAG" + +# --------------------------------------------------------------------------- +# WAF (private networking) support. +# +# In WAF mode the registry has public network access DISABLED at rest (with a +# default-deny network rule set and image export disabled); runtime pulls flow +# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the +# registry over its PUBLIC endpoint, so we must temporarily relax those settings +# for the build/push and restore them afterwards - including on failure, via a +# trap - so the registry is never left publicly reachable. +# +# WAF is detected from the resource group's `Type` tag (set to 'WAF' by the +# infrastructure when private networking is enabled). +# --------------------------------------------------------------------------- +DEPLOYMENT_TYPE="$(az group show --name "$RESOURCE_GROUP" --query 'tags.Type' -o tsv 2>/dev/null || true)" + +relock_acr() { + if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then + echo "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Deny --output none --only-show-errors \ + || echo "WARNING: failed to restore ACR default-action; verify manually." >&2 + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled false --output none --only-show-errors \ + || echo "WARNING: failed to disable ACR public network access; verify manually." >&2 + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports false --output none --only-show-errors \ + || echo "WARNING: failed to disable ACR exports; verify manually." >&2 + fi +} + +if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then + echo "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" + # Ensure the locked-down state is restored on any exit (success or failure). + trap relock_acr EXIT + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports true --output none --only-show-errors + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled true --output none --only-show-errors + az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Allow --output none --only-show-errors + echo " Waiting ~45s for the network rule change to propagate..." + sleep 45 +fi + +# --------------------------------------------------------------------------- +# Remote build helper - uses ACR Tasks (az acr build) so no local Docker daemon +# is required on the machine running the deployment. +# --------------------------------------------------------------------------- +build_image() { + local image_name="$1" + local context_dir="$2" + echo "==> Remote build (az acr build): ${image_name}:${IMAGE_TAG}" + az acr build \ + --registry "$ACR_NAME" \ + --image "${image_name}:${IMAGE_TAG}" \ + --file "${context_dir}/Dockerfile" \ + "$context_dir" +} + +update_app() { + local app_name="$1" + local image_name="$2" + if [[ -z "$app_name" ]]; then + echo "ERROR: Container app name for '${image_name}' is not set; cannot update its image." >&2 + echo " Ensure the deployment outputs / azd environment include the container app names so the app is not left on the placeholder image." >&2 + exit 1 + fi + echo "==> Updating container app '${app_name}' -> ${REGISTRY_ENDPOINT}/${image_name}:${IMAGE_TAG}" + az containerapp update \ + --name "$app_name" \ + --resource-group "$RESOURCE_GROUP" \ + --image "${REGISTRY_ENDPOINT}/${image_name}:${IMAGE_TAG}" \ + --output none +} + +# Build & push all images to the dedicated ACR. +build_image "backend-api" "${ROOT_DIR}/src/backend-api" +build_image "processor" "${ROOT_DIR}/src/processor" +build_image "frontend" "${ROOT_DIR}/src/frontend" + +# Point the Container Apps at the freshly built images. +update_app "$BACKEND_APP" "backend-api" +update_app "$PROCESSOR_APP" "processor" +update_app "$FRONTEND_APP" "frontend" + +echo "==> [acr_build_push] Completed successfully." From 168813d529f420b3d59a641d227afb8b698d2de6 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 08:53:22 +0530 Subject: [PATCH 09/16] fix: remove deprecated deploy_container_images scripts for cleaner deployment process --- scripts/deploy_container_images.ps1 | 190 ---------------------------- scripts/deploy_container_images.sh | 188 --------------------------- 2 files changed, 378 deletions(-) delete mode 100644 scripts/deploy_container_images.ps1 delete mode 100644 scripts/deploy_container_images.sh diff --git a/scripts/deploy_container_images.ps1 b/scripts/deploy_container_images.ps1 deleted file mode 100644 index 25e772a3..00000000 --- a/scripts/deploy_container_images.ps1 +++ /dev/null @@ -1,190 +0,0 @@ -<# -.SYNOPSIS - Separate post-deployment script (Windows / PowerShell) that runs FIRST, - before any existing post-deployment scripts. - -.DESCRIPTION - Builds the deployment-specific container images using Azure Container - Registry *remote* builds (az acr build - no local Docker required) and - pushes them to the dedicated, per-deployment ACR. It then updates the - Container Apps to use the freshly pushed images. - - This does NOT depend on a shared/public registry or anonymous pull. Image - pulls at runtime use identity-based authentication (the app's managed - identity has the AcrPull role on the dedicated ACR). -#> - -$ErrorActionPreference = 'Stop' - -Write-Host "==> [deploy_container_images] Building and pushing images to the dedicated ACR (remote build)" - -# --------------------------------------------------------------------------- -# Resolve required values. azd exports deployment outputs as environment -# variables inside hooks; fall back to `azd env get-values` if needed. -# --------------------------------------------------------------------------- -$AcrName = $env:AZURE_CONTAINER_REGISTRY_NAME -$RegistryEndpoint = $env:AZURE_CONTAINER_REGISTRY_ENDPOINT -$ResourceGroup = $env:AZURE_RESOURCE_GROUP -$SubscriptionId = $env:AZURE_SUBSCRIPTION_ID -$ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { 'latest' } -$BackendApp = $env:CONTAINER_API_APP_NAME -$FrontendApp = $env:CONTAINER_WEB_APP_NAME -$ProcessorApp = $env:CONTAINER_PROCESSOR_APP_NAME - -# Load values from `azd env get-values` when any required value is missing. This -# covers not just the registry/resource group but also the container app names -# and registry endpoint, so a partially-populated environment does not silently -# skip image updates and leave apps on the placeholder image. -if ([string]::IsNullOrEmpty($AcrName) -or [string]::IsNullOrEmpty($ResourceGroup) -or [string]::IsNullOrEmpty($RegistryEndpoint) -or [string]::IsNullOrEmpty($BackendApp) -or [string]::IsNullOrEmpty($FrontendApp) -or [string]::IsNullOrEmpty($ProcessorApp) -or [string]::IsNullOrEmpty($SubscriptionId)) { - if (Get-Command azd -ErrorAction SilentlyContinue) { - Write-Host "==> Loading missing values from 'azd env get-values'" - foreach ($line in (azd env get-values)) { - if ($line -match '^(?[A-Za-z0-9_]+)="?(?.*?)"?$') { - $k = $Matches['k']; $v = $Matches['v'] - switch ($k) { - 'AZURE_CONTAINER_REGISTRY_NAME' { if (-not $AcrName) { $AcrName = $v } } - 'AZURE_CONTAINER_REGISTRY_ENDPOINT' { if (-not $RegistryEndpoint) { $RegistryEndpoint = $v } } - 'AZURE_RESOURCE_GROUP' { if (-not $ResourceGroup) { $ResourceGroup = $v } } - 'AZURE_SUBSCRIPTION_ID' { if (-not $SubscriptionId) { $SubscriptionId = $v } } - 'AZURE_ENV_IMAGE_TAG' { if (-not $env:AZURE_ENV_IMAGE_TAG) { $ImageTag = $v } } - 'CONTAINER_API_APP_NAME' { if (-not $BackendApp) { $BackendApp = $v } } - 'CONTAINER_WEB_APP_NAME' { if (-not $FrontendApp) { $FrontendApp = $v } } - 'CONTAINER_PROCESSOR_APP_NAME' { if (-not $ProcessorApp) { $ProcessorApp = $v } } - } - } - } - } -} - -# Derive the login server from the registry name if it was not provided. -if ([string]::IsNullOrEmpty($RegistryEndpoint)) { - $RegistryEndpoint = "$AcrName.azurecr.io" -} - -$missing = @() -if ([string]::IsNullOrEmpty($AcrName)) { $missing += 'AZURE_CONTAINER_REGISTRY_NAME' } -if ([string]::IsNullOrEmpty($ResourceGroup)) { $missing += 'AZURE_RESOURCE_GROUP' } -if ($missing.Count -gt 0) { - Write-Error "Missing required deployment values: $($missing -join ', '). Ensure infrastructure has been provisioned (azd provision) first." - exit 1 -} - -# Ensure the Azure CLI is installed before any `az` invocation, so a missing -# CLI produces a clear, actionable error instead of an opaque failure in the -# azd hook logs. -if (-not (Get-Command az -ErrorAction SilentlyContinue)) { - Write-Error "Azure CLI ('az') is not installed or not on PATH. Install it from https://learn.microsoft.com/cli/azure/install-azure-cli and re-run." - exit 1 -} - -# Ensure the Azure CLI has a valid, non-expired login. `az acr build` and -# `az containerapp update` authenticate via the az CLI (separate from azd), so a -# stale/expired token here would otherwise fail part-way through the build. -az account show --output none 2>$null -if ($LASTEXITCODE -ne 0) { - Write-Error "Azure CLI is not authenticated or its token has expired. Run 'az login' (add '--tenant ' if needed) and re-run this script." - exit 1 -} - -# Pin the Azure CLI to the azd environment's subscription. `az acr build` and -# `az containerapp update` use the CLI's active subscription, which may differ -# from the azd environment when multiple subscriptions are available - without -# this the build/update could target the wrong subscription (or fail). -if (-not [string]::IsNullOrEmpty($SubscriptionId)) { - az account set --subscription $SubscriptionId 2>$null - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to set Azure CLI subscription to '$SubscriptionId'. Verify the subscription ID and that your account has access to it." - exit 1 - } -} - -# Resolve the repository root (this script lives in /scripts). -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$RootDir = Split-Path -Parent $ScriptDir - -Write-Host " Registry : $RegistryEndpoint ($AcrName)" -Write-Host " Resource group : $ResourceGroup" -Write-Host " Image tag : $ImageTag" - -function Build-Image { - param([string]$ImageName, [string]$ContextDir) - Write-Host "==> Remote build (az acr build): ${ImageName}:$ImageTag" - az acr build ` - --registry $AcrName ` - --image "${ImageName}:$ImageTag" ` - --file (Join-Path $ContextDir 'Dockerfile') ` - $ContextDir - if ($LASTEXITCODE -ne 0) { throw "az acr build failed for $ImageName" } -} - -function Update-App { - param([string]$AppName, [string]$ImageName) - if ([string]::IsNullOrEmpty($AppName)) { - Write-Error "Container app name for '$ImageName' is not set; cannot update its image. Ensure the deployment outputs / azd environment include the container app names so the app is not left on the placeholder image." - exit 1 - } - Write-Host "==> Updating container app '$AppName' -> $RegistryEndpoint/${ImageName}:$ImageTag" - az containerapp update ` - --name $AppName ` - --resource-group $ResourceGroup ` - --image "$RegistryEndpoint/${ImageName}:$ImageTag" ` - --output none - if ($LASTEXITCODE -ne 0) { throw "az containerapp update failed for $AppName" } -} - -# --------------------------------------------------------------------------- -# WAF (private networking) support. -# -# In WAF mode the registry has public network access DISABLED at rest (with a -# default-deny network rule set and image export disabled); runtime pulls flow -# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the -# registry over its PUBLIC endpoint, so we temporarily relax those settings for -# the build/push and restore them afterwards - including on failure, via finally -# - so the registry is never left publicly reachable. WAF is detected from the -# resource group's 'Type' tag (set to 'WAF' when private networking is enabled). -# --------------------------------------------------------------------------- -$DeploymentType = az group show --name $ResourceGroup --query 'tags.Type' -o tsv 2>$null - -try { - # In WAF mode, temporarily relax the ACR restrictions so the remote build - # (az acr build / ACR Tasks) can reach the registry over its public endpoint. - # This is done inside the try so the finally block always restores the - # locked-down state - even if one of the relaxation steps partially succeeds - # and a later one fails. - if ($DeploymentType -eq 'WAF') { - Write-Host "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" - az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports true --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } - az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled true --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } - az acr update --name $AcrName --resource-group $ResourceGroup --default-action Allow --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } - Write-Host " Waiting ~45s for the network rule change to propagate..." - Start-Sleep -Seconds 45 - } - - # Build & push all images to the dedicated ACR. - Build-Image -ImageName 'backend-api' -ContextDir (Join-Path $RootDir 'src/backend-api') - Build-Image -ImageName 'processor' -ContextDir (Join-Path $RootDir 'src/processor') - Build-Image -ImageName 'frontend' -ContextDir (Join-Path $RootDir 'src/frontend') - - # Point the Container Apps at the freshly built images. - Update-App -AppName $BackendApp -ImageName 'backend-api' - Update-App -AppName $ProcessorApp -ImageName 'processor' - Update-App -AppName $FrontendApp -ImageName 'frontend' -} -finally { - if ($DeploymentType -eq 'WAF') { - Write-Host "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" - $restoreFailed = $false - az acr update --name $AcrName --resource-group $ResourceGroup --default-action Deny --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to restore ACR default-action to Deny." } - az acr update --name $AcrName --resource-group $ResourceGroup --public-network-enabled false --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR public network access." } - az acr update --name $AcrName --resource-group $ResourceGroup --allow-exports false --output none --only-show-errors - if ($LASTEXITCODE -ne 0) { $restoreFailed = $true; Write-Warning "Failed to disable ACR exports." } - if ($restoreFailed) { Write-Warning "ACR was not fully restored to its locked-down state; verify manually that public network access is disabled." } - } -} - -Write-Host "==> [deploy_container_images] Completed successfully." diff --git a/scripts/deploy_container_images.sh b/scripts/deploy_container_images.sh deleted file mode 100644 index 4f277432..00000000 --- a/scripts/deploy_container_images.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash -# -# deploy_container_images.sh -# -# Separate post-deployment script that runs FIRST (before any existing -# post-deployment scripts). It builds the deployment-specific container images -# using Azure Container Registry *remote* builds (az acr build - no local Docker -# required) and pushes them to the dedicated, per-deployment ACR. It then updates -# the Container Apps to use the freshly pushed images. -# -# This intentionally does NOT depend on a shared/public registry or anonymous -# pull. Image pulls at runtime use identity-based authentication (the app's -# managed identity has the AcrPull role on the dedicated ACR). -# -set -euo pipefail - -echo "==> [deploy_container_images] Building and pushing images to the dedicated ACR (remote build)" - -# --------------------------------------------------------------------------- -# Resolve required values. azd exports deployment outputs as environment -# variables inside hooks; fall back to `azd env get-values` if needed. -# --------------------------------------------------------------------------- -ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" -REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" -RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" -SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-}" -IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-}" -BACKEND_APP="${CONTAINER_API_APP_NAME:-}" -FRONTEND_APP="${CONTAINER_WEB_APP_NAME:-}" -PROCESSOR_APP="${CONTAINER_PROCESSOR_APP_NAME:-}" - -# Load values from `azd env get-values` when any required value is missing. -# This covers not just the registry/resource group but also the container app -# names and registry endpoint, so a partially-populated environment does not -# silently skip image updates and leave apps on the placeholder image. -if [[ -z "$ACR_NAME" || -z "$RESOURCE_GROUP" || -z "$REGISTRY_ENDPOINT" || -z "$BACKEND_APP" || -z "$FRONTEND_APP" || -z "$PROCESSOR_APP" || -z "$SUBSCRIPTION_ID" ]]; then - if command -v azd >/dev/null 2>&1; then - echo "==> Loading missing values from 'azd env get-values'" - while IFS='=' read -r key value; do - value="${value%\"}"; value="${value#\"}" - case "$key" in - AZURE_CONTAINER_REGISTRY_NAME) ACR_NAME="${ACR_NAME:-$value}" ;; - AZURE_CONTAINER_REGISTRY_ENDPOINT) REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-$value}" ;; - AZURE_RESOURCE_GROUP) RESOURCE_GROUP="${RESOURCE_GROUP:-$value}" ;; - AZURE_SUBSCRIPTION_ID) SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-$value}" ;; - AZURE_ENV_IMAGE_TAG) IMAGE_TAG="${IMAGE_TAG:-$value}" ;; - CONTAINER_API_APP_NAME) BACKEND_APP="${BACKEND_APP:-$value}" ;; - CONTAINER_WEB_APP_NAME) FRONTEND_APP="${FRONTEND_APP:-$value}" ;; - CONTAINER_PROCESSOR_APP_NAME) PROCESSOR_APP="${PROCESSOR_APP:-$value}" ;; - esac - done < <(azd env get-values 2>/dev/null || true) - fi -fi - -# Derive the login server from the registry name if it was not provided. -REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-${ACR_NAME}.azurecr.io}" - -# Apply the default image tag only after the azd fallback, so an explicitly -# configured tag (env var or `azd env get-values`) is honored. -IMAGE_TAG="${IMAGE_TAG:-latest}" - -missing=() -[[ -z "$ACR_NAME" ]] && missing+=("AZURE_CONTAINER_REGISTRY_NAME") -[[ -z "$RESOURCE_GROUP" ]] && missing+=("AZURE_RESOURCE_GROUP") -if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: Missing required deployment values: ${missing[*]}" >&2 - echo " Ensure infrastructure has been provisioned (azd provision) first." >&2 - exit 1 -fi - -# Ensure the Azure CLI is installed before any `az` invocation. Under `set -e` -# a missing `az` would otherwise fail with an opaque "command not found" that is -# hard to diagnose in CI/azd hook logs. -if ! command -v az >/dev/null 2>&1; then - echo "ERROR: Azure CLI ('az') is not installed or not on PATH." >&2 - echo " Install it from https://learn.microsoft.com/cli/azure/install-azure-cli and re-run." >&2 - exit 1 -fi - -# Ensure the Azure CLI has a valid, non-expired login. `az acr build` and -# `az containerapp update` authenticate via the az CLI (separate from azd), so a -# stale/expired token here would otherwise fail part-way through the build. -if ! az account show >/dev/null 2>&1; then - echo "ERROR: Azure CLI is not authenticated or its token has expired." >&2 - echo " Run 'az login' (add '--tenant ' if needed) and re-run this script." >&2 - exit 1 -fi - -# Pin the Azure CLI to the azd environment's subscription. `az acr build` and -# `az containerapp update` use the CLI's active subscription, which may differ -# from the azd environment when the user has multiple subscriptions - without -# this the build/update could target the wrong subscription (or fail). -if [[ -n "$SUBSCRIPTION_ID" ]]; then - if ! az account set --subscription "$SUBSCRIPTION_ID" 2>/dev/null; then - echo "ERROR: Failed to set Azure CLI subscription to '$SUBSCRIPTION_ID'." >&2 - echo " Verify the subscription ID and that your account has access to it." >&2 - exit 1 - fi -fi - -# Resolve the repository root (this script lives in /scripts). -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" - -echo " Registry : $REGISTRY_ENDPOINT ($ACR_NAME)" -echo " Resource group : $RESOURCE_GROUP" -echo " Image tag : $IMAGE_TAG" - -# --------------------------------------------------------------------------- -# WAF (private networking) support. -# -# In WAF mode the registry has public network access DISABLED at rest (with a -# default-deny network rule set and image export disabled); runtime pulls flow -# over a private endpoint. Remote build (az acr build / ACR Tasks) reaches the -# registry over its PUBLIC endpoint, so we must temporarily relax those settings -# for the build/push and restore them afterwards - including on failure, via a -# trap - so the registry is never left publicly reachable. -# -# WAF is detected from the resource group's `Type` tag (set to 'WAF' by the -# infrastructure when private networking is enabled). -# --------------------------------------------------------------------------- -DEPLOYMENT_TYPE="$(az group show --name "$RESOURCE_GROUP" --query 'tags.Type' -o tsv 2>/dev/null || true)" - -relock_acr() { - if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then - echo "==> Restoring WAF ACR configuration (default-action Deny, public access disabled, exports off)" - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Deny --output none --only-show-errors \ - || echo "WARNING: failed to restore ACR default-action; verify manually." >&2 - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled false --output none --only-show-errors \ - || echo "WARNING: failed to disable ACR public network access; verify manually." >&2 - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports false --output none --only-show-errors \ - || echo "WARNING: failed to disable ACR exports; verify manually." >&2 - fi -} - -if [[ "$DEPLOYMENT_TYPE" == "WAF" ]]; then - echo "==> WAF deployment detected - temporarily relaxing ACR restrictions for the image push" - # Ensure the locked-down state is restored on any exit (success or failure). - trap relock_acr EXIT - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --allow-exports true --output none --only-show-errors - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --public-network-enabled true --output none --only-show-errors - az acr update --name "$ACR_NAME" --resource-group "$RESOURCE_GROUP" --default-action Allow --output none --only-show-errors - echo " Waiting ~45s for the network rule change to propagate..." - sleep 45 -fi - -# --------------------------------------------------------------------------- -# Remote build helper - uses ACR Tasks (az acr build) so no local Docker daemon -# is required on the machine running the deployment. -# --------------------------------------------------------------------------- -build_image() { - local image_name="$1" - local context_dir="$2" - echo "==> Remote build (az acr build): ${image_name}:${IMAGE_TAG}" - az acr build \ - --registry "$ACR_NAME" \ - --image "${image_name}:${IMAGE_TAG}" \ - --file "${context_dir}/Dockerfile" \ - "$context_dir" -} - -update_app() { - local app_name="$1" - local image_name="$2" - if [[ -z "$app_name" ]]; then - echo "ERROR: Container app name for '${image_name}' is not set; cannot update its image." >&2 - echo " Ensure the deployment outputs / azd environment include the container app names so the app is not left on the placeholder image." >&2 - exit 1 - fi - echo "==> Updating container app '${app_name}' -> ${REGISTRY_ENDPOINT}/${image_name}:${IMAGE_TAG}" - az containerapp update \ - --name "$app_name" \ - --resource-group "$RESOURCE_GROUP" \ - --image "${REGISTRY_ENDPOINT}/${image_name}:${IMAGE_TAG}" \ - --output none -} - -# Build & push all images to the dedicated ACR. -build_image "backend-api" "${ROOT_DIR}/src/backend-api" -build_image "processor" "${ROOT_DIR}/src/processor" -build_image "frontend" "${ROOT_DIR}/src/frontend" - -# Point the Container Apps at the freshly built images. -update_app "$BACKEND_APP" "backend-api" -update_app "$PROCESSOR_APP" "processor" -update_app "$FRONTEND_APP" "frontend" - -echo "==> [deploy_container_images] Completed successfully." From c32d2266060d155f03a124270848559ca556ae62 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 08:55:49 +0530 Subject: [PATCH 10/16] fix: update deployment scripts to use new acr_build_push scripts for consistency --- docs/DeploymentGuide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index b22d9966..08629b8e 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -300,12 +300,12 @@ Build and push the frontend, backend, and processor images to the dedicated ACR, ```powershell # PowerShell -./scripts/deploy_container_images.ps1 +./scripts/acr_build_push.ps1 ``` ```bash # Bash -bash ./scripts/deploy_container_images.sh +bash ./scripts/acr_build_push.sh ``` ### 4.4 Get Application URL From ff1cd6f40502ecced4e012009c338b4ddf663a96 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 08:59:13 +0530 Subject: [PATCH 11/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- infra/modules/containerRegistry.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/modules/containerRegistry.bicep b/infra/modules/containerRegistry.bicep index 7ac84435..24b276e5 100644 --- a/infra/modules/containerRegistry.bicep +++ b/infra/modules/containerRegistry.bicep @@ -20,7 +20,7 @@ param tags object = {} ]) param sku string = 'Standard' -@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/acr_build_push.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') +@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') @allowed([ 'Enabled' 'Disabled' From 196fc3e650c850c88b3c5f73a5b63dae186a06bd Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 09:01:46 +0530 Subject: [PATCH 12/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure.yaml b/azure.yaml index 26bc6e9c..ba37af44 100644 --- a/azure.yaml +++ b/azure.yaml @@ -13,7 +13,7 @@ hooks: shell: sh run: | echo "ℹ️ Container images are NOT built automatically. Run the build manually when ready:" - echo " bash ./scripts/deploy_container_images.sh" + echo " bash ./scripts/acr_build_push.sh" echo "-----" echo "🧭 Web App Details:" echo "βœ… Name: $CONTAINER_WEB_APP_NAME" From 0dc96ec3c39cb301b2ed68c583e3a32891358aac Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 09:01:59 +0530 Subject: [PATCH 13/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure.yaml b/azure.yaml index ba37af44..7ad979f6 100644 --- a/azure.yaml +++ b/azure.yaml @@ -30,7 +30,7 @@ hooks: shell: pwsh run: | Write-Host "ℹ️ Container images are NOT built automatically. Run the build manually when ready:" - Write-Host " ./scripts/deploy_container_images.ps1" + Write-Host " ./scripts/acr_build_push.ps1" Write-Host "-----" Write-Host "🧭 Web App Details:" Write-Host "βœ… Name: $env:CONTAINER_WEB_APP_NAME" From 5f0dd5ef9f8ee696a458d304dcc3dd675e0e0a6e Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 10:04:30 +0530 Subject: [PATCH 14/16] fix: update resource ID references for private networking in container registry module --- infra/main.bicep | 6 ++++-- infra/main.json | 14 ++++++-------- infra/modules/containerRegistry.bicep | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 4bb4ec6e..8698b1fa 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -245,8 +245,10 @@ module containerRegistry './modules/containerRegistry.bicep' = { networkRuleBypassOptions: 'AzureServices' // WAF: host the registry private endpoint in the backend subnet and link it // to the privatelink.azurecr.io DNS zone so image pulls resolve privately. - privateEndpointSubnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.backendSubnetResourceId : '' - privateDnsZoneResourceId: enablePrivateNetworking ? avmPrivateDnsZones[dnsZoneIndex.containerRegistry]!.outputs.resourceId : '' + // Use deterministic resource IDs here so non-private deployments do not + // pick up unconditional dependencies on the conditional network modules. + privateEndpointSubnetResourceId: enablePrivateNetworking ? resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks/subnets', 'vnet-${solutionSuffix}', 'backend') : '' + privateDnsZoneResourceId: enablePrivateNetworking ? resourceId(resourceGroup().name, 'Microsoft.Network/privateDnsZones', 'privatelink.azurecr.io') : '' // Application managed identity gets AcrPull for identity-based image pulls. acrPullPrincipalIds: [ appIdentity.outputs.principalId diff --git a/infra/main.json b/infra/main.json index d39ac842..a34d8817 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.43.8.12551", - "templateHash": "8577992003868472105" + "templateHash": "14967895930995130893" } }, "parameters": { @@ -874,8 +874,8 @@ "networkRuleBypassOptions": { "value": "AzureServices" }, - "privateEndpointSubnetResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.backendSubnetResourceId.value), createObject('value', ''))]", - "privateDnsZoneResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)).outputs.resourceId.value), createObject('value', ''))]", + "privateEndpointSubnetResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks/subnets', format('vnet-{0}', variables('solutionSuffix')), 'backend')), createObject('value', ''))]", + "privateDnsZoneResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', resourceId(resourceGroup().name, 'Microsoft.Network/privateDnsZones', 'privatelink.azurecr.io')), createObject('value', ''))]", "acrPullPrincipalIds": { "value": [ "[reference('appIdentity').outputs.principalId.value]" @@ -895,7 +895,7 @@ "_generator": { "name": "bicep", "version": "0.43.8.12551", - "templateHash": "17231291582633402413" + "templateHash": "6541411388428378234" }, "name": "Dedicated Azure Container Registry", "description": "Provisions a dedicated Azure Container Registry (ACR) for a single deployment and configures identity-based authentication.\r\nAdmin user and anonymous pull are disabled. The provided application managed identity principals are granted the AcrPull role, and the deployer is granted a registry-scoped AcrPush role so it can push/pull images (remote builds via `az acr build` additionally rely on the deployer's higher-scope role for scheduleRun/action)." @@ -1128,9 +1128,7 @@ } }, "dependsOn": [ - "appIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)]", - "virtualNetwork" + "appIdentity" ] }, "logAnalyticsWorkspace": { @@ -27769,8 +27767,8 @@ }, "dependsOn": [ "appIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "virtualNetwork" ] }, diff --git a/infra/modules/containerRegistry.bicep b/infra/modules/containerRegistry.bicep index 24b276e5..7ac84435 100644 --- a/infra/modules/containerRegistry.bicep +++ b/infra/modules/containerRegistry.bicep @@ -20,7 +20,7 @@ param tags object = {} ]) param sku string = 'Standard' -@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/deploy_container_images.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') +@description('Optional. Public network access for the registry. Defaults to Enabled for non-WAF deployments. In WAF (private-networking) deployments the caller sets this to Disabled: runtime image pulls flow over a private endpoint, and the post-deployment build script (scripts/acr_build_push.*) temporarily re-enables public access for the remote `az acr build` and then restores it to Disabled.') @allowed([ 'Enabled' 'Disabled' From a64dcffb4f05406005943cda917e10c9e1bb4984 Mon Sep 17 00:00:00 2001 From: "Priyanka Singhal (Persistent Systems Limited)" Date: Fri, 17 Jul 2026 12:34:03 +0530 Subject: [PATCH 15/16] remove local deployment changes --- .github/workflows/ci.yml | 2 - azure_custom.yaml | 108 --- docs/DeploymentGuide.md | 62 +- infra/main_custom.bicep | 1562 -------------------------------------- 4 files changed, 1 insertion(+), 1733 deletions(-) delete mode 100644 azure_custom.yaml delete mode 100644 infra/main_custom.bicep diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd2422df..3081a9f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ on: paths: - 'infra/**' - 'azure.yaml' - - 'azure_custom.yaml' - 'scripts/**' - '.github/workflows/ci.yml' pull_request: @@ -23,7 +22,6 @@ on: paths: - 'infra/**' - 'azure.yaml' - - 'azure_custom.yaml' - 'scripts/**' - '.github/workflows/ci.yml' schedule: diff --git a/azure_custom.yaml b/azure_custom.yaml deleted file mode 100644 index 3dc0a43b..00000000 --- a/azure_custom.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -# This file contains a developer‑focused Azure Developer CLI configuration. It -# extends the default template by defining three services (backend API, -# processor and frontend) and instructs azd to build container images from -# your local source code. All three services are packaged as container apps -# using the Dockerfiles located in their respective project directories. -# After deployment a post‑deploy hook prints the endpoints of the deployed -# container apps. - -name: container-migration-solution-accelerator -metadata: - template: container-migration-solution-accelerator@1.0 - -requiredVersions: - # Require a recent version of azd that supports the packaging - # functionality used here. Versions less than 1.17.1 had a bug in - # remoteBuild. - azd: '>=1.18.2 != 1.23.9' - -infra: - parameters: - backendImageName: ${SERVICE_BACKEND_IMAGE_NAME} - processorImageName: ${SERVICE_PROCESSOR_IMAGE_NAME} - frontendImageName: ${SERVICE_FRONTEND_IMAGE_NAME} - -services: - # Backend API service. This is a Python FastAPI application defined in - # src/backend-api. The azd packaging stage builds a Docker image using - # the Dockerfile in that directory. The image name 'backend-api' is - # combined with the automatically created Azure Container Registry login - # server to form the final image reference. - backend: - project: ./src/backend-api - language: py - host: containerapp - docker: - image: backend-api - remoteBuild: true - - # Processor service. This service reads messages from storage queues and - # orchestrates long‑running migrations. It is also packaged as a - # container and deployed to a container app environment. The Dockerfile - # in the src/processor directory defines how the image is built. - processor: - project: ./src/processor - language: py - host: containerapp - docker: - image: processor - remoteBuild: true - - # Frontend service. The frontend consists of a React single‑page - # application and a lightweight Python server that serves the compiled - # assets. azd packages the frontend by building a Docker image using - # the Dockerfile in the src/frontend directory and deploys it to a - # container app. - frontend: - project: ./src/frontend - language: py - host: containerapp - docker: - image: frontend - remoteBuild: true - -hooks: - # After deployment prints the names and endpoints of the deployed - # container apps. This reproduces the behaviour of the default - # configuration so that developers can easily discover their services. - postdeploy: - posix: - shell: sh - run: | - echo "-----" - echo "🧭 Frontend Container App Details:" - echo "βœ… Name: $CONTAINER_FRONTEND_APP_NAME" - echo "🌐 Endpoint: https://$CONTAINER_FRONTEND_APP_FQDN" - echo "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_FRONTEND_APP_NAME" - echo "-----" - echo "🧭 Backend API Container App Details:" - echo "βœ… Name: $CONTAINER_API_APP_NAME" - echo "🌐 Endpoint: https://$CONTAINER_API_APP_FQDN" - echo "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_API_APP_NAME" - echo "-----" - echo "🧭 Processor Container App Details:" - echo "βœ… Name: $SERVICE_PROCESSOR_NAME" - echo "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$SERVICE_PROCESSOR_NAME" - echo "-----" - interactive: true - windows: - shell: pwsh - run: | - Write-Host "-----" - Write-Host "🧭 Frontend Container App Details:" - Write-Host "βœ… Name: $env:CONTAINER_FRONTEND_APP_NAME" - Write-Host "🌐 Endpoint: https://$env:CONTAINER_FRONTEND_APP_FQDN" - Write-Host "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$env:AZURE_SUBSCRIPTION_ID/resourceGroups/$env:AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$env:CONTAINER_FRONTEND_APP_NAME" -ForegroundColor Cyan - Write-Host "-----" - Write-Host "🧭 Backend API Container App Details:" - Write-Host "βœ… Name: $env:CONTAINER_API_APP_NAME" - Write-Host "🌐 Endpoint: https://$env:CONTAINER_API_APP_FQDN" - Write-Host "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$env:AZURE_SUBSCRIPTION_ID/resourceGroups/$env:AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$env:CONTAINER_API_APP_NAME" -ForegroundColor Cyan - Write-Host "-----" - Write-Host "🧭 Processor Container App Details:" - Write-Host "βœ… Name: $env:SERVICE_PROCESSOR_NAME" - Write-Host "πŸ”— Portal URL: https://portal.azure.com/#resource/subscriptions/$env:AZURE_SUBSCRIPTION_ID/resourceGroups/$env:AZURE_RESOURCE_GROUP/providers/Microsoft.App/containerApps/$env:SERVICE_PROCESSOR_NAME" -ForegroundColor Cyan - Write-Host "-----" - interactive: true diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 3e3b4537..e30fb690 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -460,64 +460,4 @@ Now that your deployment is complete and tested, explore these resources to enha - πŸ› **Issues:** Check [Troubleshooting Guide](./TroubleShootingSteps.md) - πŸ’¬ **Support:** Review [Support Guidelines](../SUPPORT.md) -- πŸ”§ **Development:** See [Contributing Guide](../CONTRIBUTING.md) - ---- - -## Advanced: Deploy Local Changes - -If you've made local modifications to the code and want to deploy them to Azure, follow these steps to swap the configuration files so that `azd up` builds Docker images from your local source code instead of pulling pre-built images from the GitHub repository. - -**How it works:** -- The custom `azure.yaml` defines three services (backend, processor, frontend) with `remoteBuild: true`, which instructs `azd` to build Docker images from your local `src/` directories and push them to Azure Container Registry (ACR). -- The custom `main.bicep` accepts image name parameters (`backendImageName`, `processorImageName`, `frontendImageName`) that `azd` passes automatically after building the images. - -> **Note:** To set up and run the application locally for development, see the [Local Development Setup Guide](./LocalDevelopmentSetup.md). - -### Step 1: Rename Azure Configuration Files - -**In the root directory:** -1. Rename `azure.yaml` to `azure_custom2.yaml` -2. Rename `azure_custom.yaml` to `azure.yaml` - -### Step 2: Rename Infrastructure Files - -**In the `infra` directory:** -1. Rename `main.bicep` to `main_custom2.bicep` -2. Rename `main_custom.bicep` to `main.bicep` - -### Step 3: Deploy Changes - -> ⚠️ **Critical: Redeployment Warning** -> If you have previously run `azd up` in this folder (i.e., a `.azure` folder exists), you must create a fresh environment before deploying to avoid conflicts and deployment failures. - -**Create a fresh environment:** -```shell -# Create a new named environment (3-16 characters, alphanumeric only) -azd env new -``` - -> **Note:** When prompted "Set new environment as default environment?", select **Y**. This eliminates the need to run `azd env select` separately. - -**Run the deployment:** -```shell -azd up -``` - -> **Note:** During the packaging phase, you may see `"No artifacts were found"` for each service. This is expected β€” because `remoteBuild: true` is configured, Docker images are built remotely on Azure Container Registry, not on your local machine. Your local code is still being deployed. - -**⚠️ Deployment Issues:** If `azd up` fails on the first attempt (e.g., with a `ResourceNotFound` error), try running `azd up` again. Transient errors can occur due to resource propagation delays, and a retry typically resolves them. For other errors, try a different region or see the [Troubleshooting Guide](./TroubleShootingSteps.md). - -### Step 4: Revert Configuration Files - -After your custom deployment is complete, revert the renames to restore the original configuration: - -**In the root directory:** -1. Rename `azure.yaml` to `azure_custom.yaml` -2. Rename `azure_custom2.yaml` to `azure.yaml` - -**In the `infra` directory:** -1. Rename `main.bicep` to `main_custom.bicep` -2. Rename `main_custom2.bicep` to `main.bicep` - -> **Note:** This restores the original files so that standard deployments and git status remain clean. +- πŸ”§ **Development:** See [Contributing Guide](../CONTRIBUTING.md) \ No newline at end of file diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep deleted file mode 100644 index a32408d7..00000000 --- a/infra/main_custom.bicep +++ /dev/null @@ -1,1562 +0,0 @@ -targetScope = 'resourceGroup' - -@minLength(3) -@maxLength(16) -@description('Required. A unique application/solution name for all resources in this deployment. This should be 3-16 characters long.') -param solutionName string - -@maxLength(5) -@description('Optional. A unique text/token for the solution. This is used to ensure resource names are unique for global resources. Defaults to a 5-character substring of the unique string generated from the subscription ID, resource group name, and solution name.') -param solutionUniqueText string = substring(uniqueString(subscription().id, resourceGroup().name, solutionName), 0, 5) - -@minLength(3) -@metadata({ azd: { type: 'location' } }) -@description('Required. Azure region for container apps, storage, and other services. Choose a region close to your users.') -param location string -var solutionLocation = empty(location) ? resourceGroup().location : location - -@allowed([ - 'australiaeast' - 'eastus' - 'eastus2' - 'francecentral' - 'japaneast' - 'norwayeast' - 'southindia' - 'swedencentral' - 'uksouth' - 'westus' - 'westus3' -]) -@metadata({ - azd: { - type: 'location' - usageName: [ - 'OpenAI.GlobalStandard.gpt-5.1, 500' - ] - } -}) -@description('Required. Azure region for AI services (OpenAI/AI Foundry). Must be a region that supports GPT5.1 model deployment.') -param azureAiServiceLocation string - - - - - -@secure() -@description('The full image name (including tag) for the backend API container, generated by azd.') -param backendImageName string = '' - -@secure() -@description('The full image name (including tag) for the processor container, generated by azd.') -param processorImageName string = '' - -@secure() -@description('The full image name (including tag) for the frontend container, generated by azd.') -param frontendImageName string = '' - -@minLength(1) -@allowed(['Standard', 'GlobalStandard']) -@description('Optional. Model deployment type. Defaults to GlobalStandard.') -param deploymentType string = 'GlobalStandard' - -@minLength(1) -@description('Optional. Name of the AI model to deploy. Recommend using gpt-5.1. Defaults to gpt-5.1.') -param gptModelName string = 'gpt-5.1' - -@minLength(1) -@description('Optional. Version of AI model. Review available version numbers per model before setting. Defaults to 2025-11-13.') -param gptModelVersion string = '2025-11-13' - -@description('Optional. GPT model deployment token capacity. Lower this if initial provisioning fails due to capacity. Defaults to 500K tokens per minute to improve regional success rate.') -param gptDeploymentCapacity int = 500 - -@description('Optional. The tags to apply to all deployed Azure resources.') -param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} - -@description('Optional. Enable redundancy for applicable resources. Defaults to false.') -param enableRedundancy bool = false - -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -@description('Optional. Enable private networking for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') -param enablePrivateNetworking bool = false - -@description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') -param enableMonitoring bool = false - -@description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') -param enableScalability bool = false - -@description('Optional. CosmosDB Location') -param cosmosLocation string = 'eastus2' - -@description('Optional. Existing Log Analytics Workspace Resource ID') -param existingLogAnalyticsWorkspaceId string = '' - -@description('Tag, Created by user name') -param createdBy string = contains(deployer(), 'userPrincipalName') - ? split(deployer().userPrincipalName, '@')[0] - : deployer().objectId - -// Get the current deployer's information -var deployerInfo = deployer() -var deployingUserPrincipalId = deployerInfo.objectId - -@description('Optional. Resource ID of an existing Foundry project') -param existingFoundryProjectResourceId string = '' - -@description('Optional. Admin username for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') -@secure() -//param vmAdminUsername string = take(newGuid(), 20) -param vmAdminUsername string? - -@description('Optional. Admin password for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') -@secure() -//param vmAdminPassword string = newGuid() -param vmAdminPassword string? - -@description('Optional. Size of the Jumpbox Virtual Machine when created. Set to custom value if enablePrivateNetworking is true.') -param vmSize string? - -// Extracts subscription, resource group, and workspace name from the resource ID when using an existing Log Analytics workspace -var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) -var existingLawSubscription = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' -var existingLawResourceGroup = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' -var existingLawName = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' - -resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-08-01' existing = if (useExistingLogAnalytics) { - name: existingLawName - scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) -} - -var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics - ? existingLogAnalyticsWorkspaceId - : logAnalyticsWorkspace!.outputs.resourceId - -var solutionSuffix = toLower(trim(replace( - replace( - replace(replace(replace(replace('${solutionName}${solutionUniqueText}', '-', ''), '_', ''), '.', ''), '/', ''), - ' ', - '' - ), - '*', - '' -))) - -var allTags = union( - { - 'azd-env-name': solutionName - TemplateName: 'Container Migration' - }, - tags -) - -var existingTags = resourceGroup().tags ?? {} - -resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { - name: 'default' - properties: { - tags: union( - existingTags, - tags, - { - TemplateName: 'Container Migration' - Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' - CreatedBy: createdBy - } - ) - } -} - -// Replica regions list based on article in [Azure regions list](https://learn.microsoft.com/azure/reliability/regions-list) and [Enhance resilience by replicating your Log Analytics workspace across regions](https://learn.microsoft.com/azure/azure-monitor/logs/workspace-replication#supported-regions) for supported regions for Log Analytics Workspace. -var replicaRegionPairs = { - australiaeast: 'australiasoutheast' - centralus: 'westus' - eastasia: 'japaneast' - eastus: 'centralus' - eastus2: 'centralus' - japaneast: 'eastasia' - northeurope: 'westeurope' - southeastasia: 'eastasia' - uksouth: 'westeurope' - westeurope: 'northeurope' -} -var replicaLocation = replicaRegionPairs[resourceGroup().location] - -// ========== User Assigned Identity ========== // -// WAF best practices for identity and access management: https://learn.microsoft.com/en-us/azure/well-architected/security/identity-access -var userAssignedIdentityResourceName = 'id-${solutionSuffix}' -module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { - name: take('avm.res.managed-identity.user-assigned-identity.${userAssignedIdentityResourceName}', 64) - params: { - name: userAssignedIdentityResourceName - location: solutionLocation - tags: allTags - enableTelemetry: enableTelemetry - } -} - -// ========== Log Analytics Workspace ========== // -// WAF best practices for Log Analytics: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-log-analytics -// WAF PSRules for Log Analytics: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#azure-monitor-logs -var logAnalyticsWorkspaceResourceName = 'log-${solutionSuffix}' -module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = if ((enableMonitoring || enablePrivateNetworking) && !useExistingLogAnalytics) { - name: take('avm.res.operational-insights.workspace.${logAnalyticsWorkspaceResourceName}', 64) - params: { - name: logAnalyticsWorkspaceResourceName - location: solutionLocation - skuName: 'PerGB2018' - dataRetention: 30 - diagnosticSettings: [{ useThisWorkspace: true }] - tags: allTags - enableTelemetry: enableTelemetry - features: { enableLogAccessUsingOnlyResourcePermissions: true } - // WAF aligned configuration for Redundancy - dailyQuotaGb: enableRedundancy ? 10 : null //WAF recommendation: 10 GB per day is a good starting point for most workloads - replication: enableRedundancy - ? { - enabled: true - location: replicaLocation - } - : null - // WAF aligned configuration for Private Networking - publicNetworkAccessForIngestion: enablePrivateNetworking ? 'Disabled' : 'Enabled' - publicNetworkAccessForQuery: enablePrivateNetworking ? 'Disabled' : 'Enabled' - dataSources: enablePrivateNetworking - ? [ - { - tags: allTags - eventLogName: 'Application' - eventTypes: [ - { - eventType: 'Error' - } - { - eventType: 'Warning' - } - { - eventType: 'Information' - } - ] - kind: 'WindowsEvent' - name: 'applicationEvent' - } - { - counterName: '% Processor Time' - instanceName: '*' - intervalSeconds: 60 - kind: 'WindowsPerformanceCounter' - name: 'windowsPerfCounter1' - objectName: 'Processor' - } - { - kind: 'IISLogs' - name: 'sampleIISLog1' - state: 'OnPremiseEnabled' - } - ] - : null - } -} - -// ========== Application Insights ========== // -// WAF best practices for Application Insights: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/application-insights -// WAF PSRules for Application Insights: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#application-insights -var applicationInsightsResourceName = 'appi-${solutionSuffix}' -module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (enableMonitoring) { - name: take('avm.res.insights.component.${applicationInsightsResourceName}', 64) - #disable-next-line no-unnecessary-dependson - //dependsOn: [logAnalyticsWorkspace] - params: { - name: applicationInsightsResourceName - location: solutionLocation - tags: allTags - enableTelemetry: enableTelemetry - retentionInDays: 365 - kind: 'web' - disableIpMasking: false - flowType: 'Bluefield' - // WAF aligned configuration for Monitoring - workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - } -} - -// ========== Virtual Network ========== // -module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { - name: take('module.virtual-network.${solutionSuffix}', 64) - params: { - name: 'vnet-${solutionSuffix}' - addressPrefixes: ['10.0.0.0/20'] - location: location - tags: allTags - logAnalyticsWorkspaceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' - resourceSuffix: solutionSuffix - enableTelemetry: enableTelemetry - } -} - -// Azure Bastion Host -var bastionHostName = 'bas-${solutionSuffix}' // Bastion host name must be between 3 and 15 characters in length and use numbers and lower-case letters only. -module bastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.bastion-host.${bastionHostName}', 64) - params: { - name: bastionHostName - skuName: 'Standard' - location: location - virtualNetworkResourceId: virtualNetwork!.outputs.resourceId - diagnosticSettings: enableMonitoring - ? [ - { - name: 'bastionDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceResourceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - } - ] - : null - tags: allTags - enableTelemetry: enableTelemetry - publicIPAddressObject: { - name: 'pip-${bastionHostName}' - zones: [] - } - } -} -// Jumpbox Virtual Machine -var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) -module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.15.0' = if (enablePrivateNetworking) { - name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) - params: { - name: take(jumpboxVmName, 15) // Shorten VM name to 15 characters to avoid Azure limits - vmSize: vmSize ?? 'Standard_D2s_v5' - location: location - adminUsername: vmAdminUsername ?? 'JumpboxAdminUser' - adminPassword: vmAdminPassword ?? 'JumpboxAdminP@ssw0rd1234!' - tags: allTags - zone: 0 - // SFI: enable system-assigned managed identity on the jumpbox VM. Required so - // the Azure Monitor Agent can authenticate to the Log Analytics workspace and - // honor the SecurityAuditEvents data collection rule association. - managedIdentities: { systemAssigned: true } - imageReference: { - offer: 'WindowsServer' - publisher: 'MicrosoftWindowsServer' - sku: '2019-datacenter' - version: 'latest' - } - osType: 'Windows' - osDisk: { - name: 'osdisk-${jumpboxVmName}' - managedDisk: { - storageAccountType: 'Standard_LRS' - } - } - encryptionAtHost: false // Some Azure subscriptions do not support encryption at host - nicConfigurations: [ - { - name: 'nic-${jumpboxVmName}' - ipConfigurations: [ - { - name: 'ipconfig1' - subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId - } - ] - diagnosticSettings: enableMonitoring - ? [ - { - name: 'jumpboxDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceResourceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - metricCategories: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } - ] - : null - } - ] - enableTelemetry: enableTelemetry - // SFI: associate the SecurityAuditEvents data collection rule with the - // jumpbox VM via the Azure Monitor Agent extension. Routes Windows audit - // success / audit failure events to Log Analytics. Gated on the same - // (enablePrivateNetworking && enableMonitoring) expression as the DCR - // module so the dereference of windowsVmDataCollectionRules!.outputs - // stays safe even if the outer jumpbox VM gate ever changes. - extensionMonitoringAgentConfig: (enablePrivateNetworking && enableMonitoring) - ? { - enabled: true - tags: allTags - dataCollectionRuleAssociations: [ - { - name: 'send-${logAnalyticsWorkspaceResourceName}' - dataCollectionRuleResourceId: windowsVmDataCollectionRules!.outputs.resourceId - } - ] - } - : null - } -} - -// SFI: data collection rule that captures Windows Security audit success and -// audit failure events from the jumpbox VM and routes them to Log Analytics -// via the Microsoft-Event stream. The xPath filter uses the Windows -// audit Keywords bitmask (0x30000000000000 = AuditSuccess|AuditFailure) and -// excludes EventID 4624 (successful logon) because it is extremely -// high-volume. Also collects a small set of Windows performance counters via -// Microsoft-Perf for the jumpbox so the same DCR provides basic VM health -// signal. The SecurityEvent / Perf tables are auto-provisioned by Azure -// Monitor on first ingestion via the DCR; no legacy OMSGallery/Security -// solution is needed. -var dataCollectionRulesResourceName = 'dcr-${solutionSuffix}' -var dataCollectionRulesLocation = useExistingLogAnalytics - ? existingLogAnalyticsWorkspace!.location - : logAnalyticsWorkspace!.outputs.location -var dcrLogAnalyticsDestinationName = 'la-${logAnalyticsWorkspaceResourceName}-destination' -module windowsVmDataCollectionRules 'br/public:avm/res/insights/data-collection-rule:0.11.0' = if (enablePrivateNetworking && enableMonitoring) { - name: take('avm.res.insights.data-collection-rule.${dataCollectionRulesResourceName}', 64) - params: { - name: dataCollectionRulesResourceName - tags: allTags - enableTelemetry: enableTelemetry - location: dataCollectionRulesLocation - dataCollectionRuleProperties: { - kind: 'Windows' - dataSources: { - windowsEventLogs: [ - { - name: 'SecurityAuditEvents' - streams: [ - 'Microsoft-Event' - ] - xPathQueries: [ - 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' - ] - } - ] - performanceCounters: [ - { - name: 'perfCounterDataSource60' - streams: [ - 'Microsoft-Perf' - ] - samplingFrequencyInSeconds: 60 - counterSpecifiers: [ - '\\Processor Information(_Total)\\% Processor Time' - '\\Processor Information(_Total)\\% Privileged Time' - '\\Processor Information(_Total)\\% User Time' - '\\Processor Information(_Total)\\Processor Frequency' - '\\System\\Processes' - '\\Process(_Total)\\Thread Count' - '\\Process(_Total)\\Handle Count' - '\\System\\System Up Time' - '\\System\\Context Switches/sec' - '\\System\\Processor Queue Length' - '\\Memory\\% Committed Bytes In Use' - '\\Memory\\Available Bytes' - '\\Memory\\Committed Bytes' - '\\Memory\\Cache Bytes' - '\\Memory\\Pool Paged Bytes' - '\\Memory\\Pool Nonpaged Bytes' - '\\Memory\\Pages/sec' - '\\Memory\\Page Faults/sec' - '\\Process(_Total)\\Working Set' - '\\Process(_Total)\\Working Set - Private' - '\\LogicalDisk(_Total)\\% Disk Time' - '\\LogicalDisk(_Total)\\% Disk Read Time' - '\\LogicalDisk(_Total)\\% Disk Write Time' - '\\LogicalDisk(_Total)\\% Idle Time' - '\\LogicalDisk(_Total)\\Disk Bytes/sec' - '\\LogicalDisk(_Total)\\Disk Read Bytes/sec' - '\\LogicalDisk(_Total)\\Disk Write Bytes/sec' - '\\LogicalDisk(_Total)\\Disk Transfers/sec' - '\\LogicalDisk(_Total)\\Disk Reads/sec' - '\\LogicalDisk(_Total)\\Disk Writes/sec' - '\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer' - '\\LogicalDisk(_Total)\\Avg. Disk sec/Read' - '\\LogicalDisk(_Total)\\Avg. Disk sec/Write' - '\\LogicalDisk(_Total)\\Avg. Disk Queue Length' - '\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length' - '\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length' - '\\LogicalDisk(_Total)\\% Free Space' - '\\LogicalDisk(_Total)\\Free Megabytes' - '\\Network Interface(*)\\Bytes Total/sec' - '\\Network Interface(*)\\Bytes Sent/sec' - '\\Network Interface(*)\\Bytes Received/sec' - '\\Network Interface(*)\\Packets/sec' - '\\Network Interface(*)\\Packets Sent/sec' - '\\Network Interface(*)\\Packets Received/sec' - '\\Network Interface(*)\\Packets Outbound Errors' - '\\Network Interface(*)\\Packets Received Errors' - ] - } - ] - } - destinations: { - logAnalytics: [ - { - workspaceResourceId: logAnalyticsWorkspaceResourceId - name: dcrLogAnalyticsDestinationName - } - ] - } - dataFlows: [ - { - streams: [ - 'Microsoft-Event' - ] - destinations: [ - dcrLogAnalyticsDestinationName - ] - } - { - streams: [ - 'Microsoft-Perf' - ] - destinations: [ - dcrLogAnalyticsDestinationName - ] - } - ] - } - } -} - -var processBlobContainerName = 'processes' -var processQueueName = 'processes-queue' - -// ========== Private DNS Zones ========== // -var privateDnsZones = [ - 'privatelink.cognitiveservices.azure.com' - 'privatelink.openai.azure.com' - 'privatelink.services.ai.azure.com' - 'privatelink.documents.azure.com' - 'privatelink.blob.${environment().suffixes.storage}' - 'privatelink.queue.${environment().suffixes.storage}' - 'privatelink.azconfig.io' -] - -// DNS Zone Index Constants -var dnsZoneIndex = { - cognitiveServices: 0 - openAI: 1 - aiServices: 2 - cosmosDB: 3 - storageBlob: 4 - storageQueue: 5 - appConfig: 6 -} - -// List of DNS zone indices that correspond to AI-related services. -var aiRelatedDnsZoneIndices = [ - dnsZoneIndex.cognitiveServices - dnsZoneIndex.openAI - dnsZoneIndex.aiServices -] - -// =================================================== -// DEPLOY PRIVATE DNS ZONES -// - Deploys all zones if no existing Foundry project is used -// - Excludes AI-related zones when using with an existing Foundry project -// =================================================== -@batchSize(5) -module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.7.1' = [ - for (zone, i) in privateDnsZones: if (enablePrivateNetworking && (empty(existingFoundryProjectResourceId) || !contains( - aiRelatedDnsZoneIndices, - i - ))) { - name: 'dns-zone-${i}' - params: { - name: zone - tags: allTags - enableTelemetry: enableTelemetry - virtualNetworkLinks: [ - { - name: take('vnetlink-${virtualNetwork!.outputs.name}-${split(zone, '.')[1]}', 80) - virtualNetworkResourceId: virtualNetwork!.outputs.resourceId - } - ] - } - } -] - -// ========== AVM WAF ========== // -// ========== Storage account module ========== // -var storageAccountName = 'st${solutionSuffix}' // Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only. -module storageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = { - name: take('avm.res.storage.storage-account.${storageAccountName}', 64) - params: { - name: storageAccountName - location: solutionLocation - managedIdentities: { systemAssigned: true } - minimumTlsVersion: 'TLS1_2' - // SFI: enable infrastructure (double) encryption at rest - requireInfrastructureEncryption: true - enableTelemetry: enableTelemetry - tags: allTags - accessTier: 'Hot' - supportsHttpsTrafficOnly: true - roleAssignments: [ - { - roleDefinitionIdOrName: 'Storage Blob Data Contributor' - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - { - roleDefinitionIdOrName: 'Storage Queue Data Contributor' - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - ] - // WAF aligned networking - networkAcls: { - bypass: 'AzureServices' - defaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' - } - allowBlobPublicAccess: enablePrivateNetworking ? true : false - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - // Private endpoints for blob and queue - privateEndpoints: enablePrivateNetworking - ? [ - { - name: 'pep-storage-${storageAccountName}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'storage-dns-zone-group-blob' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.storageBlob]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId - service: 'blob' - } - { - name: 'pep-queue-${solutionSuffix}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'storage-dns-zone-group-queue' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.storageQueue]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId - service: 'queue' - } - ] - : [] - blobServices: { - corsRules: [] - deleteRetentionPolicyEnabled: false - containers: [ - { - name: 'data' - publicAccess: 'None' - denyEncryptionScopeOverride: false - defaultEncryptionScope: '$account-encryption-key' - } - ] - } - queueServices: { - deleteRetentionPolicyEnabled: true - deleteRetentionPolicyDays: 7 - queues: [ - for queue in ([processQueueName, '${processQueueName}-dead-letter'] ?? []): { - name: queue - } - ] - } - } -} - -//========== AVM WAF ========== // -//========== Cosmos DB module ========== // -var cosmosDbResourceName = 'cosmos-${solutionSuffix}' -var cosmosDbZoneRedundantHaRegionPairs = { - australiaeast: 'uksouth' //'southeastasia' - centralus: 'eastus2' - eastasia: 'southeastasia' - eastus: 'centralus' - eastus2: 'centralus' - japaneast: 'australiaeast' - northeurope: 'westeurope' - southeastasia: 'eastasia' - uksouth: 'westeurope' - westeurope: 'northeurope' -} -var cosmosDbHaLocation = cosmosDbZoneRedundantHaRegionPairs[resourceGroup().location] - -var cosmosDatabaseName = 'migration_db' -var processCosmosContainerName = 'processes' -var agentTelemetryCosmosContainerName = 'agent_telemetry' -module cosmosDb 'br/public:avm/res/document-db/database-account:0.15.0' = { - name: take('avm.res.document-db.database-account.${cosmosDbResourceName}', 64) - params: { - name: cosmosDbResourceName - location: cosmosLocation - tags: allTags - enableTelemetry: enableTelemetry - // SFI: enable system-assigned managed identity for Cosmos DB account - managedIdentities: { systemAssigned: true } - sqlDatabases: [ - { - name: cosmosDatabaseName - containers: [ - { - name: processCosmosContainerName - paths: [ - '/_partitionKey' - ] - } - { - name: agentTelemetryCosmosContainerName - paths: [ - '/_partitionKey' - ] - } - { - name: 'files' - paths: [ - '/_partitionKey' - ] - } - { - name: 'process_statuses' - paths: [ - '/_partitionKey' - ] - } - ] - } - ] - - diagnosticSettings: enableMonitoring - ? [ - { - workspaceResourceId: logAnalyticsWorkspaceResourceId - } - ] - : null - - networkRestrictions: { - networkAclBypass: 'None' - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - } - - privateEndpoints: enablePrivateNetworking - ? [ - { - name: 'pep-${cosmosDbResourceName}' - customNetworkInterfaceName: 'nic-${cosmosDbResourceName}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cosmosDB]!.outputs.resourceId } - ] - } - service: 'Sql' - subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId - } - ] - : [] - - zoneRedundant: enableRedundancy ? true : false - capabilitiesToAdd: enableRedundancy - ? null - : [ - 'EnableServerless' - ] - automaticFailover: enableRedundancy ? true : false - failoverLocations: enableRedundancy - ? [ - { - failoverPriority: 0 - isZoneRedundant: true - locationName: solutionLocation - } - { - failoverPriority: 1 - isZoneRedundant: true - locationName: cosmosDbHaLocation - } - ] - : [ - { - locationName: solutionLocation - failoverPriority: 0 - isZoneRedundant: enableRedundancy - } - ] - // Use built-in Cosmos DB roles for RBAC access - roleAssignments: [ - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'DocumentDB Account Contributor' - } - ] - // Create custom data plane role definition and assignment - dataPlaneRoleDefinitions: [ - { - roleName: 'CosmosDB Data Contributor Custom' - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - ] - assignments: [ - { principalId: appIdentity.outputs.principalId } - // ADD THIS for local debugging support: - { principalId: deployingUserPrincipalId } - ] - } - ] - } - dependsOn: [storageAccount] -} - - -// ========== Container Registry for developer builds ========== // -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') -module containerRegistry 'br/public:avm/res/container-registry/registry:0.9.1' = { - name: 'registryDeployment' - params: { - name: 'cr${solutionSuffix}' - acrAdminUserEnabled: false - acrSku: 'Basic' - azureADAuthenticationAsArmPolicyStatus: 'enabled' - exportPolicyStatus: 'enabled' - location: solutionLocation - softDeletePolicyDays: 7 - softDeletePolicyStatus: 'disabled' - tags: allTags - networkRuleBypassOptions: 'AzureServices' - // SFI: enable system-assigned managed identity for the container registry - managedIdentities: { systemAssigned: true } - roleAssignments: [ - { - roleDefinitionIdOrName: acrPullRole - principalType: 'ServicePrincipal' - principalId: appIdentity.outputs.principalId - } - ] - } -} - -var aiModelDeploymentName = gptModelName - -var useExistingAiFoundryAiProject = !empty(existingFoundryProjectResourceId) -var aiFoundryAiServicesResourceGroupName = useExistingAiFoundryAiProject - ? split(existingFoundryProjectResourceId, '/')[4] - : 'rg-${solutionSuffix}' -var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject - ? split(existingFoundryProjectResourceId, '/')[2] - : subscription().id -var aiFoundryAiServicesResourceName = useExistingAiFoundryAiProject - ? split(existingFoundryProjectResourceId, '/')[8] - : 'aif-${solutionSuffix}' -var aiFoundryAiProjectResourceName = 'aifp-${solutionSuffix}' -var aiFoundryAiProjectDescription = 'AI Foundry project for ${solutionName}' - -resource existingAiFoundryAiServices 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (useExistingAiFoundryAiProject) { - name: aiFoundryAiServicesResourceName - scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) -} - -module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.bicep' = if (useExistingAiFoundryAiProject) { - name: take('module.ai-services-model-deployments.${existingAiFoundryAiServices.name}', 64) - scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) - params: { - name: existingAiFoundryAiServices.name - deployments: [ - { - name: aiModelDeploymentName - model: { - format: 'OpenAI' - name: gptModelName - version: gptModelVersion - } - sku: { - name: deploymentType - capacity: gptDeploymentCapacity - } - } - ] - roleAssignments: [ - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' - } - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer - } - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Foundry User - } - ] - } -} - -// ========== AI Foundry AI Services ========== // -module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.13.2' = if (!useExistingAiFoundryAiProject) { - name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) - params: { - name: aiFoundryAiServicesResourceName - location: empty(azureAiServiceLocation) ? location : azureAiServiceLocation - tags: allTags - sku: 'S0' - kind: 'AIServices' - disableLocalAuth: true - allowProjectManagement: true - customSubDomainName: aiFoundryAiServicesResourceName - deployments: [ - { - name: aiModelDeploymentName - model: { - format: 'OpenAI' - name: gptModelName - version: gptModelVersion - } - sku: { - name: deploymentType - capacity: gptDeploymentCapacity - } - } - ] - networkAcls: { - defaultAction: 'Allow' - virtualNetworkRules: [] - ipRules: [] - } - managedIdentities: { - systemAssigned: true - userAssignedResourceIds: [appIdentity.outputs.resourceId] - } - roleAssignments: [ - // Service Principal permissions - { - roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - { - roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - { - roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Foundry User - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - // Deployer permissions for local debugging - { - roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' - principalId: deployingUserPrincipalId - principalType: 'User' - } - { - roleDefinitionIdOrName: 'Cognitive Services User' - principalId: deployingUserPrincipalId - principalType: 'User' - } - ] - // WAF aligned configuration for Monitoring - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - // Private endpoints are deployed separately via the aiFoundryPrivateEndpoint module below - privateEndpoints: [] - enableTelemetry: enableTelemetry - } -} - -// ========== AI Foundry Private Endpoint ========== // -module aiFoundryPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.8.1' = if (enablePrivateNetworking && !useExistingAiFoundryAiProject) { - name: take('pep-${aiFoundryAiServicesResourceName}-deployment', 64) - dependsOn: [ - aiFoundryAiServices - virtualNetwork - avmPrivateDnsZones - ] - params: { - name: 'pep-${aiFoundryAiServicesResourceName}' - customNetworkInterfaceName: 'nic-${aiFoundryAiServicesResourceName}' - location: solutionLocation - tags: allTags - enableTelemetry: enableTelemetry - privateLinkServiceConnections: [ - { - name: 'pep-${aiFoundryAiServicesResourceName}-connection' - properties: { - privateLinkServiceId: aiFoundryAiServices!.outputs.resourceId - groupIds: ['account'] - } - } - ] - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'ai-services-dns-zone-cognitiveservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId - } - { - name: 'ai-services-dns-zone-openai' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.openAI]!.outputs.resourceId - } - { - name: 'ai-services-dns-zone-aiservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.aiServices]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId - } -} - -module aiFoundryProject 'modules/ai-project.bicep' = if (!useExistingAiFoundryAiProject) { - name: take('module.ai-project.${aiFoundryAiProjectResourceName}', 64) - dependsOn: enablePrivateNetworking ? [aiFoundryPrivateEndpoint] : [] - params: { - name: aiFoundryAiProjectResourceName - location: azureAiServiceLocation - tags: tags - desc: aiFoundryAiProjectDescription - //Implicit dependencies below - aiServicesName: aiFoundryAiServices!.outputs.name - } -} - -// User Role Assignment for Azure OpenAI - New Resources -module userOpenAiRoleAssignment './modules/role.bicep' = if (!useExistingAiFoundryAiProject) { - name: take('user-openai-${uniqueString(deployingUserPrincipalId, aiFoundryAiServicesResourceName)}', 64) - params: { - name: 'user-openai-${uniqueString(deployingUserPrincipalId, aiFoundryAiServicesResourceName)}' - principalId: deployingUserPrincipalId - aiServiceName: aiFoundryAiServicesResourceName - principalType: 'User' - } -} - -// User Role Assignment for Azure OpenAI - Existing Resources -module userOpenAiRoleAssignmentExisting './modules/role.bicep' = if (useExistingAiFoundryAiProject) { - name: take('user-openai-existing-${uniqueString(deployingUserPrincipalId, existingAiFoundryAiServices.name)}', 64) - params: { - name: 'user-openai-existing-${uniqueString(deployingUserPrincipalId, existingAiFoundryAiServices.name)}' - principalId: deployingUserPrincipalId - aiServiceName: existingAiFoundryAiServices.name - principalType: 'User' - } - scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) -} - -var aiServicesName = useExistingAiFoundryAiProject ? existingAiFoundryAiServices.name : aiFoundryAiServicesResourceName -module appConfiguration 'br/public:avm/res/app-configuration/configuration-store:0.9.1' = { - name: take('avm.res.app-config.store.${solutionSuffix}', 64) - params: { - location: solutionLocation - name: 'appcs-${solutionSuffix}' - disableLocalAuth: false // needed to allow setting app config key values from this module - tags: allTags - // Always set key values during deployment since Container Apps will be in private network - keyValues: [ - { - name: 'APP_LOGGING_ENABLE' - value: 'true' - } - { - name: 'APP_LOGGING_LEVEL' - value: 'INFO' - } - { - name: 'AZURE_PACKAGE_LOGGING_LEVEL' - value: 'INFO' - } - { - name: 'AZURE_LOGGING_PACKAGES' - value: '' - } - { - name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME' - value: '' - } - { - name: 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING' - value: '' - } - { - name: 'AZURE_OPENAI_API_VERSION' - value: '2025-03-01-preview' - } - { - name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' - value: aiModelDeploymentName - } - { - name: 'AZURE_OPENAI_ENDPOINT' - value: 'https://${aiServicesName}.cognitiveservices.azure.com/' - } - { - name: 'AZURE_OPENAI_ENDPOINT_BASE' - value: 'https://${aiServicesName}.cognitiveservices.azure.com/' - } - { - name: 'AZURE_TRACING_ENABLED' - value: 'True' - } - { - name: 'STORAGE_ACCOUNT_BLOB_URL' - value: 'https://${storageAccountName}.blob.${environment().suffixes.storage}' - } - { - name: 'STORAGE_ACCOUNT_NAME' - value: storageAccount.outputs.name - } - { - name: 'STORAGE_ACCOUNT_PROCESS_CONTAINER' - value: processBlobContainerName - } - { - name: 'STORAGE_ACCOUNT_PROCESS_QUEUE' - value: processQueueName - } - { - name: 'STORAGE_ACCOUNT_QUEUE_URL' - value: 'https://${storageAccountName}.queue.${environment().suffixes.storage}' - } - { - name: 'COSMOS_DB_CONTAINER_NAME' - value: agentTelemetryCosmosContainerName - } - { - name: 'COSMOS_DB_DATABASE_NAME' - value: cosmosDatabaseName - } - { - name: 'COSMOS_DB_ACCOUNT_URL' - value: cosmosDb.outputs.endpoint - } - { - name: 'COSMOS_DB_PROCESS_CONTAINER' - value: processCosmosContainerName - } - { - name: 'COSMOS_DB_PROCESS_LOG_CONTAINER' - value: agentTelemetryCosmosContainerName - } - { - name: 'GLOBAL_LLM_SERVICE' - value: 'AzureOpenAI' - } - { - name: 'STORAGE_QUEUE_ACCOUNT' - value: storageAccount.outputs.name - } - ] - roleAssignments: [ - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'App Configuration Data Reader' - } - ] - enableTelemetry: enableTelemetry - managedIdentities: { systemAssigned: true } - sku: 'Standard' - publicNetworkAccess: 'Enabled' - } -} - -module avmAppConfigUpdated 'br/public:avm/res/app-configuration/configuration-store:0.6.3' = if (enablePrivateNetworking) { - name: take('avm.res.app-configuration.configuration-store-update.${solutionSuffix}', 64) - params: { - name: 'appcs-${solutionSuffix}' - location: solutionLocation - managedIdentities: { systemAssigned: true } - sku: 'Standard' - enableTelemetry: enableTelemetry - tags: allTags - disableLocalAuth: true - // Keep public access enabled for Container Apps access (Container Apps not in private network due to capacity constraints) - publicNetworkAccess: 'Enabled' - privateEndpoints: enablePrivateNetworking - ? [ - { - name: 'pep-appconfig-${solutionSuffix}' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'appconfig-dns-zone-group' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.appConfig]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId - } - ] - : [] - } - dependsOn: [ - appConfiguration - ] -} - -var logAnalyticsPrimarySharedKey = useExistingLogAnalytics - ? existingLogAnalyticsWorkspace!.listKeys().primarySharedKey - : logAnalyticsWorkspace!.outputs!.primarySharedKey -var logAnalyticsWorkspaceId = useExistingLogAnalytics - ? existingLogAnalyticsWorkspace!.properties.customerId - : logAnalyticsWorkspace!.outputs.logAnalyticsWorkspaceId -// ========== Container App Environment ========== // -module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.2' = { - name: take('avm.res.app.managed-environment.${solutionSuffix}', 64) - params: { - name: 'cae-${solutionSuffix}' - location: location - tags: { - ...resourceGroup().tags - ...existingTags - ...allTags - ...tags - } - managedIdentities: { systemAssigned: true } - appLogsConfiguration: enableMonitoring - ? { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalyticsWorkspaceId - sharedKey: logAnalyticsPrimarySharedKey - } - } - : null - workloadProfiles: [ - { - name: 'Consumption' - workloadProfileType: 'Consumption' - } - ] - enableTelemetry: enableTelemetry - publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment - // SFI: enable mTLS / end-to-end encryption between revisions within the - // Container Apps environment (Container Apps equivalent of App Service's - // endToEndEncryptionEnabled). Applies to Microsoft.App/managedEnvironments - // peerTrafficConfiguration.encryption.enabled. - peerTrafficEncryption: true - - // <========== WAF related parameters - - platformReservedCidr: '172.17.17.0/24' - platformReservedDnsIP: '172.17.17.17' - zoneRedundant: (enablePrivateNetworking) ? true : false // Enable zone redundancy if private networking is enabled - infrastructureSubnetResourceId: (enablePrivateNetworking) - ? virtualNetwork!.outputs.containersSubnetResourceId // Use the container app subnet - : null // Use the container app subnet - } -} - -var backendContainerPort = 80 -var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32) -var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) -module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { - name: take('avm.res.app.container-app.${backendContainerAppName}', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [applicationInsights] - params: { - name: backendContainerAppName - location: solutionLocation - environmentResourceId: containerAppsEnvironment.outputs.resourceId - tags: union(allTags, { 'azd-service-name': 'backend' }) - managedIdentities: { - userAssignedResourceIds: [ - appIdentity.outputs.resourceId - ] - } - registries: [ - { - server: containerRegistry.outputs.loginServer - identity: appIdentity.outputs.resourceId - } - ] - containers: [ - { - name: 'backend-api' - image: !empty(backendImageName) ? backendImageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - env: concat( - [ - { - name: 'APP_CONFIGURATION_URL' - value: appConfiguration.outputs.endpoint - } - { - name: 'AZURE_CLIENT_ID' - value: appIdentity.outputs.clientId - } - { - name: 'PROCESSOR_CONTROL_URL' - // Internal ingress FQDN format: https://.internal. - value: 'https://${processorContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' - } - ], - enableMonitoring - ? [ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: applicationInsights!.outputs.connectionString - } - ] - : [] - ) - resources: { - cpu: 1 - memory: '2.0Gi' - } - } - ] - ingressTargetPort: backendContainerPort - ingressExternal: true - scaleSettings: { - maxReplicas: enableScalability ? 3 : 1 - minReplicas: 1 - rules: enableScalability - ? [ - { - name: 'http-scaler' - http: { - metadata: { - concurrentRequests: 100 - } - } - } - ] - : [] - } - corsPolicy: { - allowedOrigins: [ - '*' - ] - allowedMethods: [ - 'GET' - 'POST' - 'PUT' - 'DELETE' - 'OPTIONS' - ] - allowedHeaders: [ - 'Authorization' - 'Content-Type' - '*' - ] - } - enableTelemetry: enableTelemetry - } -} - -var frontEndContainerAppName = take('ca-frontend-${solutionSuffix}', 32) -module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { - name: take('avm.res.app.container-app.${frontEndContainerAppName}', 64) - params: { - name: frontEndContainerAppName - location: solutionLocation - environmentResourceId: containerAppsEnvironment.outputs.resourceId - tags: union(allTags, { 'azd-service-name': 'frontend' }) - managedIdentities: { - userAssignedResourceIds: [ - appIdentity.outputs.resourceId - ] - } - registries: [ - { - server: containerRegistry.outputs.loginServer - identity: appIdentity.outputs.resourceId - } - ] - containers: [ - { - name: 'frontend' - image: !empty(frontendImageName) ? frontendImageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - env: [ - { - name: 'API_URL' - value: 'https://${containerAppBackend.outputs.fqdn}' - } - { - name: 'APP_ENV' - value: 'prod' - } - { - name: 'ALLOWED_ORIGINS' - value: 'https://${frontEndContainerAppName}.${containerAppsEnvironment.outputs.defaultDomain}' - } - ] - resources: { - cpu: '1' - memory: '2.0Gi' - } - } - ] - ingressTargetPort: 3000 - ingressExternal: true - scaleSettings: { - maxReplicas: enableScalability ? 3 : 1 - minReplicas: 1 - rules: enableScalability - ? [ - { - name: 'http-scaler' - http: { - metadata: { - concurrentRequests: 100 - } - } - } - ] - : [] - } - enableTelemetry: enableTelemetry - } -} - -module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { - name: take('avm.res.app.container-app.${processorContainerAppName}', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [applicationInsights] - params: { - name: processorContainerAppName - location: solutionLocation - environmentResourceId: containerAppsEnvironment.outputs.resourceId - tags: union(allTags, { 'azd-service-name': 'processor' }) - managedIdentities: { - userAssignedResourceIds: [ - appIdentity.outputs.resourceId - ] - } - registries: [ - { - server: containerRegistry.outputs.loginServer - identity: appIdentity.outputs.resourceId - } - ] - containers: [ - { - name: 'processor' - image: !empty(processorImageName) ? processorImageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - env: concat( - [ - { - name: 'APP_CONFIGURATION_URL' - value: appConfiguration.outputs.endpoint - } - { - name: 'AZURE_CLIENT_ID' - value: appIdentity.outputs.clientId - } - { - name: 'AZURE_STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed or if pulled from app config service - value: storageAccount.outputs.name - } - { - name: 'STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed - value: storageAccount.outputs.name - } - { - name: 'CONTROL_API_ENABLED' - value: '1' - } - { - name: 'CONTROL_API_PORT' - value: '8080' - } - ], - enableMonitoring - ? [ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: applicationInsights!.outputs.connectionString - } - ] - : [] - ) - resources: { - // TODO - assess increasing resource limits - cpu: 2 - memory: '4.0Gi' - } - } - ] - // Internal ingress required for container-to-container communication - ingressTargetPort: 8080 - ingressExternal: false - ingressAllowInsecure: true // Allow HTTP without SSL redirect for internal calls - scaleSettings: { - maxReplicas: enableScalability ? 3 : 1 - minReplicas: 1 - //rules: [] - TODO - what scaling rules to use here? - } - enableTelemetry: enableTelemetry - } -} - -@description('The name of the resource group.') -output resourceGroupName string = resourceGroup().name - -@description('The name of the frontend container app.') -output CONTAINER_FRONTEND_APP_NAME string = containerAppFrontend.outputs.name - -@description('The FQDN of the frontend container app.') -output CONTAINER_FRONTEND_APP_FQDN string = containerAppFrontend.outputs.fqdn - -// Keep these for backward compatibility if needed -@description('The name of the web app container app.') -output CONTAINER_WEB_APP_NAME string = containerAppFrontend.outputs.name - -@description('The FQDN of the web app container app.') -output CONTAINER_WEB_APP_FQDN string = containerAppFrontend.outputs.fqdn - -@description('The name of the API container app.') -output CONTAINER_API_APP_NAME string = containerAppBackend.outputs.name - -@description('The FQDN of the API container app.') -output CONTAINER_API_APP_FQDN string = containerAppBackend.outputs.fqdn - -@description('The Azure subscription ID.') -output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId - -@description('The Azure resource group name.') -output AZURE_RESOURCE_GROUP string = resourceGroup().name - -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer - -@description('Backend service container app name') -output SERVICE_BACKEND_NAME string = containerAppBackend.outputs.name - -@description('Backend service container app URI') -output SERVICE_BACKEND_URI string = 'https://${containerAppBackend.outputs.fqdn}' - -@description('Processor service container app name') -output SERVICE_PROCESSOR_NAME string = containerAppProcessor.outputs.name - -@description('Frontend service container app name') -output SERVICE_FRONTEND_NAME string = containerAppFrontend.outputs.name - -@description('Frontend service container app URI') -output SERVICE_FRONTEND_URI string = 'https://${containerAppFrontend.outputs.fqdn}' From 8a591b4fbf328e99a8293f8de305ea2c94843d59 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 17:55:27 +0530 Subject: [PATCH 16/16] fix: update container registry module to reference VNet and DNS zone outputs directly --- infra/main.bicep | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 8698b1fa..af67aef5 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -245,10 +245,10 @@ module containerRegistry './modules/containerRegistry.bicep' = { networkRuleBypassOptions: 'AzureServices' // WAF: host the registry private endpoint in the backend subnet and link it // to the privatelink.azurecr.io DNS zone so image pulls resolve privately. - // Use deterministic resource IDs here so non-private deployments do not - // pick up unconditional dependencies on the conditional network modules. - privateEndpointSubnetResourceId: enablePrivateNetworking ? resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks/subnets', 'vnet-${solutionSuffix}', 'backend') : '' - privateDnsZoneResourceId: enablePrivateNetworking ? resourceId(resourceGroup().name, 'Microsoft.Network/privateDnsZones', 'privatelink.azurecr.io') : '' + // Reference the VNet and DNS zone outputs directly to avoid case-sensitivity + // issues with manually constructed resource IDs. + privateEndpointSubnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.backendSubnetResourceId : '' + privateDnsZoneResourceId: enablePrivateNetworking ? avmPrivateDnsZones[dnsZoneIndex.containerRegistry]!.outputs.resourceId : '' // Application managed identity gets AcrPull for identity-based image pulls. acrPullPrincipalIds: [ appIdentity.outputs.principalId