From 7712d4a4fb1142791791ecdf9178de588b7bd198 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Tue, 7 Jul 2026 15:29:07 +0530 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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 03aa515bd14e0eecd9b56144fcb20b7263f3daa5 Mon Sep 17 00:00:00 2001 From: Akhileswara-Microsoft Date: Fri, 17 Jul 2026 08:51:37 +0530 Subject: [PATCH 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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'