From 72a840f3c9b5c5317bf9fb6dd51b96eec59a9479 Mon Sep 17 00:00:00 2001 From: Omar McIver Date: Fri, 23 Jan 2026 15:32:48 -0600 Subject: [PATCH] Add JFrog support for enterprise lockdown environments --- ACA/README-JFROG.md | 179 ++++++++++ ACA/aca_main_jfrog.bicep | 358 ++++++++++++++++++++ ACA/deploy-to-aca-jfrog.ps1 | 453 ++++++++++++++++++++++++++ ACA/update-aca-app-jfrog.ps1 | 351 ++++++++++++++++++++ MongoMigrationWebApp/Dockerfile.jfrog | 77 +++++ 5 files changed, 1418 insertions(+) create mode 100644 ACA/README-JFROG.md create mode 100644 ACA/aca_main_jfrog.bicep create mode 100644 ACA/deploy-to-aca-jfrog.ps1 create mode 100644 ACA/update-aca-app-jfrog.ps1 create mode 100644 MongoMigrationWebApp/Dockerfile.jfrog diff --git a/ACA/README-JFROG.md b/ACA/README-JFROG.md new file mode 100644 index 0000000..bb82fac --- /dev/null +++ b/ACA/README-JFROG.md @@ -0,0 +1,179 @@ +# Azure Container Apps Deployment with JFrog Artifactory + +This guide explains how to deploy the MongoDB Migration Web-Based Utility to Azure Container Apps using JFrog Artifactory as the container registry instead of Azure Container Registry (ACR). + +## Overview + +The JFrog deployment differs from the standard ACR deployment in the following ways: + +| Aspect | ACR Deployment | JFrog Deployment | +|--------|----------------|------------------| +| Base Images | Pulled from `mcr.microsoft.com` | Pulled from JFrog (mirrored/proxied) | +| Build Location | Azure ACR Tasks (cloud) | Local Docker (your machine) | +| Image Storage | Azure Container Registry | JFrog Artifactory | +| Authentication | Managed Identity | Username/Password or API Key | + +## Prerequisites + +### 1. JFrog Artifactory Setup + +Ensure you have JFrog Artifactory configured with: + +1. **Docker Repository** - Either: + - A **local repository** (e.g., `docker-local`) for pushing your built images + - A **remote repository** (e.g., `docker-remote`) that proxies `mcr.microsoft.com` for base images + - A **virtual repository** (e.g., `docker-virtual`) that combines local and remote + +2. **.NET 9.0 Base Images Available** via one of: + - Remote repository proxying Microsoft Container Registry (MCR) + - Manual upload of `dotnet/sdk:9.0` and `dotnet/aspnet:9.0` + - Virtual repository that resolves from remote proxy + +3. **Authentication Credentials**: + - JFrog username + - JFrog password or API key (preferred for automation) + +### 2. Local Requirements + +- **Docker Desktop** or Docker CLI installed and running +- **Azure CLI** installed and logged in (`az login`) +- Network access to your JFrog Artifactory instance + +## Files + +| File | Description | +|------|-------------| +| `Dockerfile.jfrog` | Dockerfile with parameterized JFrog base images | +| `aca_main_jfrog.bicep` | Bicep template for ACA with JFrog registry config | +| `deploy-to-aca-jfrog.ps1` | Full deployment script (infrastructure + app) | +| `update-aca-app-jfrog.ps1` | Update script (app only, preserves config) | + +## JFrog Repository Structure + +Recommended JFrog structure: + +``` +yourcompany.jfrog.io/ +├── docker-remote/ # Remote repo proxying mcr.microsoft.com +│ └── dotnet/ +│ ├── sdk:9.0 +│ └── aspnet:9.0 +├── docker-local/ # Local repo for your images +│ └── mongomigration:latest +└── docker-virtual/ # Virtual repo (optional, combines above) + ├── dotnet/... + └── mongomigration:... +``` + +## Deployment + +### Full Deployment (First Time) + +```powershell +.\deploy-to-aca-jfrog.ps1 ` + -ResourceGroupName "my-rg" ` + -ContainerAppName "mongo-migration" ` + -JFrogRegistryServer "yourcompany.jfrog.io" ` + -JFrogUsername "your-username" ` + -JFrogRepository "docker-local/mongomigration" ` + -JFrogBaseImageRegistry "yourcompany.jfrog.io/docker-virtual" ` + -Location "eastus" ` + -OwnerTag "your-email@company.com" +``` + +#### Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `ResourceGroupName` | Yes | Azure resource group name | +| `ContainerAppName` | Yes | Name for the Container App | +| `JFrogRegistryServer` | Yes | JFrog server URL (e.g., `yourcompany.jfrog.io`) | +| `JFrogUsername` | Yes | JFrog username for authentication | +| `JFrogRepository` | Yes | Path in JFrog for the image (e.g., `docker-local/mongomigration`) | +| `JFrogBaseImageRegistry` | No | Registry for base images (defaults to `JFrogRegistryServer`) | +| `Location` | Yes | Azure region (e.g., `eastus`) | +| `OwnerTag` | Yes | Owner tag for Azure resources | +| `ImageTag` | No | Image tag (default: `latest`) | +| `VCores` | No | CPU cores 1-32 (default: 8) | +| `MemoryGB` | No | Memory in GB 2-64 (default: 32) | +| `SkipDockerBuild` | No | Skip build if image already exists | +| `UseEntraIdForAzureStorage` | No | Use Managed Identity for storage | +| `InfrastructureSubnetResourceId` | No | VNet subnet for network integration | + +### Update Existing Deployment + +```powershell +.\update-aca-app-jfrog.ps1 ` + -ResourceGroupName "my-rg" ` + -ContainerAppName "mongo-migration" ` + -JFrogRegistryServer "yourcompany.jfrog.io" ` + -JFrogUsername "your-username" ` + -JFrogRepository "docker-local/mongomigration" ` + -ImageTag "v1.2.3" +``` + +## Authentication Flow + +1. **During Deployment**: Script prompts for JFrog password/API key +2. **Stored in Azure**: Password is stored as a Container App secret (`jfrog-password`) +3. **At Runtime**: ACA uses stored credentials to pull images from JFrog + +## Troubleshooting + +### Docker Build Fails + +**Error**: `failed to solve: yourcompany.jfrog.io/dotnet/sdk:9.0: failed to resolve source metadata` + +**Cause**: Base images not available in JFrog + +**Fix**: +1. Ensure your JFrog remote repository is configured to proxy `mcr.microsoft.com` +2. Or upload base images manually to your JFrog local repository +3. Verify connectivity: `docker pull yourcompany.jfrog.io/docker-virtual/dotnet/sdk:9.0` + +### Authentication Fails + +**Error**: `unauthorized: authentication required` + +**Fix**: +1. Verify username and password/API key +2. Check JFrog permissions for the repository +3. Test manually: `docker login yourcompany.jfrog.io -u your-username` + +### Container App Fails to Start + +**Error**: Image pull fails in Azure + +**Fix**: +1. Verify the full image path is correct in Azure Portal +2. Check JFrog credentials are stored correctly as secrets +3. Ensure JFrog allows access from Azure IP ranges (if using IP restrictions) + +## Security Considerations + +1. **API Keys**: Use JFrog API keys instead of passwords for automation +2. **Minimal Permissions**: Create a dedicated JFrog user with only pull/push permissions for the specific repositories +3. **Credential Rotation**: Plan for periodic rotation of JFrog API keys +4. **Network Security**: Consider JFrog IP whitelisting if your organization requires it + +## Comparison: ACR vs JFrog + +| Feature | ACR | JFrog | +|---------|-----|-------| +| **Build Location** | Cloud (ACR Tasks) | Local (Docker) | +| **Build Speed** | Slower (network upload) | Faster (local) | +| **Cost** | ACR pricing | JFrog licensing | +| **Integration** | Native Azure | Cross-platform | +| **Authentication** | Managed Identity | Username/Password | +| **Artifact Types** | Containers only | Universal (npm, maven, etc.) | + +## Migration from ACR + +If you have an existing ACR deployment: + +1. Build and push image to JFrog using the JFrog scripts +2. Update the Container App to use JFrog credentials +3. Deploy new revision pointing to JFrog image +4. (Optional) Delete old ACR if no longer needed + +The infrastructure (Storage, Managed Identity, Container Apps Environment) remains the same—only the registry configuration changes. diff --git a/ACA/aca_main_jfrog.bicep b/ACA/aca_main_jfrog.bicep new file mode 100644 index 0000000..de61073 --- /dev/null +++ b/ACA/aca_main_jfrog.bicep @@ -0,0 +1,358 @@ +@description('Location for all resources') +param location string = resourceGroup().location + +@description('Owner tag required by Azure Policy') +param ownerTag string + +@description('Name of the Container App') +param containerAppName string + +@description('JFrog Registry server URL (e.g., yourcompany.jfrog.io)') +param jfrogRegistryServer string + +@description('JFrog Registry username') +param jfrogUsername string + +@secure() +@description('JFrog Registry password or API key') +param jfrogPassword string + +@description('Full image path in JFrog (e.g., yourcompany.jfrog.io/docker-local/mongomigration)') +param jfrogImageRepository string + +@description('Docker image tag to deploy') +param imageTag string = 'latest' + +@description('Storage account name for persistent migration files') +param storageAccountName string = take('${replace(containerAppName, '-', '')}stor', 24) + +@secure() +@description('StateStore connection string for the container') +param stateStoreConnectionString string = '' + +@description('StateStore App ID for the container') +param stateStoreAppID string = '' + +@description('Number of vCores for the container') +@minValue(1) +@maxValue(32) +param vCores int = 8 + +@description('Memory in GB for the container') +@minValue(2) +@maxValue(64) +param memoryGB int = 32 + +@description('ASP.NET Core environment setting') +param aspNetCoreEnvironment string = 'Development' + +@description('Optional: Resource ID of the subnet for VNet integration (e.g., /subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.Network/virtualNetworks/{vnet-name}/subnets/{subnet-name})') +param infrastructureSubnetResourceId string = '' + +@description('Use Entra ID (Managed Identity) for Azure Blob Storage instead of mounting Azure Files. When true, UseBlobServiceClient env var is set and no volume is mounted.') +param useEntraIdForStorage bool = false + +// Variables for dynamic workload profile selection +var workloadProfileType = vCores <= 4 ? 'D4' : vCores <= 8 ? 'D8' : vCores <= 16 ? 'D16' : 'D32' +var workloadProfileName = 'Dedicated' + +// Managed Identity for Container App (used for Azure Storage access, not registry) +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${containerAppName}-identity' + location: location + tags: { + owner: ownerTag + } +} + +// Storage Account for persistent migration files +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageAccountName + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + minimumTlsVersion: 'TLS1_2' + // Disable shared key access when using Entra ID - required by some org policies + allowSharedKeyAccess: !useEntraIdForStorage + } + tags: { + owner: ownerTag + } +} + +// File Share for migration data (100GB) - only needed when NOT using Entra ID +resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-01-01' = if (!useEntraIdForStorage) { + name: '${storageAccount.name}/default/migration-data' + properties: { + shareQuota: 100 + enabledProtocols: 'SMB' + } +} + +// Container Apps Environment with Dedicated Plan +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { + name: '${containerAppName}-env-${workloadProfileType}' + location: location + tags: { + owner: ownerTag + } + properties: union( + { + workloadProfiles: [ + { + name: 'Consumption' + workloadProfileType: 'Consumption' + } + { + name: workloadProfileName + workloadProfileType: workloadProfileType + minimumCount: 1 + maximumCount: 1 + } + ] + }, + infrastructureSubnetResourceId != '' ? { + vnetConfiguration: { + infrastructureSubnetId: infrastructureSubnetResourceId + internal: false + } + } : {} + ) +} + +// Storage configuration for Container Apps Environment (only when not using Entra ID) +resource storageConfiguration 'Microsoft.App/managedEnvironments/storages@2023-05-01' = if (!useEntraIdForStorage) { + parent: containerAppEnvironment + name: 'migration-storage' + properties: { + azureFile: { + accountName: storageAccount.name + accountKey: storageAccount.listKeys().keys[0].value + shareName: 'migration-data' + accessMode: 'ReadWrite' + } + } +} + +// Role assignment for Managed Identity to access Blob Storage (only when using Entra ID) +resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (useEntraIdForStorage) { + name: guid(storageAccount.id, managedIdentity.id, 'storageBlobDataContributor') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor + principalId: managedIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +// Container App with JFrog Registry +resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { + name: containerAppName + location: location + tags: { + owner: ownerTag + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + managedEnvironmentId: containerAppEnvironment.id + workloadProfileName: workloadProfileName + configuration: { + secrets: concat( + [ + { + name: 'jfrog-password' + value: jfrogPassword + } + ], + stateStoreConnectionString != '' ? [ + { + name: 'statestore-connection' + value: stateStoreConnectionString + } + ] : [] + ) + // JFrog Registry configuration with username/password authentication + registries: [ + { + server: jfrogRegistryServer + username: jfrogUsername + passwordSecretRef: 'jfrog-password' + } + ] + ingress: { + external: true + targetPort: 8080 + allowInsecure: true + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + } + } + template: { + containers: [ + { + name: containerAppName + image: stateStoreAppID == '' ? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' : '${jfrogImageRepository}:${imageTag}' + resources: { + cpu: vCores + memory: '${memoryGB}Gi' + } + volumeMounts: useEntraIdForStorage ? [] : [ + { + volumeName: 'migration-data-volume' + mountPath: '/app/migration-data' + } + ] + env: concat([ + { + name: 'ASPNETCORE_ENVIRONMENT' + value: aspNetCoreEnvironment + } + { + name: 'ASPNETCORE_HTTP_PORTS' + value: '8080' + } + { + name: 'StateStoreAppID' + value: stateStoreAppID + } + { + name: 'ResourceDrive' + value: '/app/migration-data' + } + ], stateStoreConnectionString != '' ? [ + { + name: 'StateStoreConnectionStringOrPath' + secretRef: 'statestore-connection' + } + ] : [], useEntraIdForStorage ? [ + { + name: 'UseBlobServiceClient' + value: 'true' + } + { + name: 'BlobServiceClientURI' + value: 'https://${storageAccount.name}.blob.${environment().suffixes.storage}' + } + { + name: 'BlobContainerName' + value: 'migration-data' + } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + ] : []) + probes: [ + { + type: 'Startup' + httpGet: { + path: '/api/HealthCheck/ping' + port: 8080 + scheme: 'HTTP' + } + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 30 + successThreshold: 1 + timeoutSeconds: 3 + } + { + type: 'Liveness' + httpGet: { + path: '/api/HealthCheck/ping' + port: 8080 + scheme: 'HTTP' + } + initialDelaySeconds: 0 + periodSeconds: 30 + failureThreshold: 3 + successThreshold: 1 + timeoutSeconds: 5 + } + { + type: 'Readiness' + httpGet: { + path: '/api/HealthCheck/ping' + port: 8080 + scheme: 'HTTP' + } + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + successThreshold: 1 + timeoutSeconds: 3 + } + ] + } + ] + volumes: useEntraIdForStorage ? [] : [ + { + name: 'migration-data-volume' + storageType: 'AzureFile' + storageName: 'migration-storage' + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } + dependsOn: useEntraIdForStorage ? [ + storageBlobDataContributorRole + ] : [ + storageConfiguration + ] +} + +// Outputs +@description('Container Apps Environment ID') +output containerAppEnvironmentId string = containerAppEnvironment.id + +@description('Container App FQDN') +output containerAppFQDN string = stateStoreAppID != '' ? containerApp.properties.configuration.ingress.fqdn : 'not-ready' + +@description('Container App URL') +output containerAppUrl string = stateStoreAppID == '' ? 'not-ready' : 'https://${containerApp.properties.configuration.ingress.fqdn}' + +@description('Managed Identity Resource ID') +output managedIdentityId string = managedIdentity.id + +@description('Managed Identity Client ID') +output managedIdentityClientId string = managedIdentity.properties.clientId + +@description('Storage Account Name for migration data') +output storageAccountName string = storageAccount.name + +@description('File Share Name for migration data (only when not using Entra ID)') +output fileShareName string = 'migration-data' + +@description('Resource Drive Mount Path in container') +output resourceDrivePath string = '/app/migration-data' + +@description('Storage mode: MountedAzureFiles or EntraIdBlobStorage') +output storageMode string = useEntraIdForStorage ? 'EntraIdBlobStorage' : 'MountedAzureFiles' + +@description('Blob Service URI (only when using Entra ID)') +output blobServiceUri string = useEntraIdForStorage ? 'https://${storageAccount.name}.blob.${environment().suffixes.storage}' : '' + +@description('JFrog Registry Server') +output jfrogRegistry string = jfrogRegistryServer + +@description('Container Image (full path with tag)') +output containerImage string = stateStoreAppID == '' ? 'placeholder' : '${jfrogImageRepository}:${imageTag}' diff --git a/ACA/deploy-to-aca-jfrog.ps1 b/ACA/deploy-to-aca-jfrog.ps1 new file mode 100644 index 0000000..95abc3d --- /dev/null +++ b/ACA/deploy-to-aca-jfrog.ps1 @@ -0,0 +1,453 @@ +# Azure Container Apps Deployment Script - JFrog Registry Edition +# Deploys the MongoDB Migration Web-Based Utility to Azure Container Apps +# using JFrog Artifactory as the container registry +# +# PREREQUISITES: +# - Docker Desktop or Docker CLI installed and running +# - Azure CLI installed and logged in +# - JFrog Artifactory account with Docker repository access +# - .NET 9.0 base images available in JFrog (mirrored/proxied from MCR) +# +# JFROG SETUP: +# 1. Create a Docker repository in JFrog (e.g., docker-local for pushing, docker-virtual for pulling) +# 2. Ensure dotnet/sdk:9.0 and dotnet/aspnet:9.0 are available (via remote proxy to MCR or manual upload) +# 3. Generate an API key or use username/password for authentication + +param( + [Parameter(Mandatory=$true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory=$true)] + [string]$ContainerAppName, + + [Parameter(Mandatory=$true)] + [string]$JFrogRegistryServer, + + [Parameter(Mandatory=$true)] + [string]$JFrogUsername, + + [Parameter(Mandatory=$true)] + [string]$JFrogRepository, + + [Parameter(Mandatory=$false)] + [string]$JFrogBaseImageRegistry = "", + + [Parameter(Mandatory=$false)] + [string]$StateStoreAppID = "", + + [Parameter(Mandatory=$true)] + [string]$Location, + + [Parameter(Mandatory=$false)] + [string]$StorageAccountName = "", + + [Parameter(Mandatory=$false)] + [string]$ImageTag = "latest", + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 32)] + [int]$VCores = 8, + + [Parameter(Mandatory=$false)] + [ValidateRange(2, 64)] + [int]$MemoryGB = 32, + + [Parameter(Mandatory=$false)] + [string]$InfrastructureSubnetResourceId = "", + + [Parameter(Mandatory=$false)] + [switch]$UseEntraIdForAzureStorage, + + [Parameter(Mandatory=$true)] + [string]$OwnerTag, + + [Parameter(Mandatory=$false)] + [switch]$SkipDockerBuild +) + +$ErrorActionPreference = "Stop" + +# Use the same registry for base images if not specified +if ([string]::IsNullOrEmpty($JFrogBaseImageRegistry)) { + $JFrogBaseImageRegistry = $JFrogRegistryServer + Write-Host "Using JFrog server for base images: $JFrogBaseImageRegistry" -ForegroundColor Cyan +} + +# Generate StateStoreAppID if not provided +if ([string]::IsNullOrEmpty($StateStoreAppID)) { + $StateStoreAppID = $ContainerAppName + Write-Host "Using ContainerAppName as StateStoreAppID: $StateStoreAppID" -ForegroundColor Cyan +} + +# Generate storage account name if not provided +if ([string]::IsNullOrEmpty($StorageAccountName)) { + $StorageAccountName = ($ContainerAppName -replace '-', '').ToLower() + 'stor' + if ($StorageAccountName.Length -gt 24) { + $StorageAccountName = $StorageAccountName.Substring(0, 24) + } + Write-Host "Using generated storage account name: $StorageAccountName" -ForegroundColor Cyan +} + +# Build full image path +$FullImagePath = "$JFrogRegistryServer/$JFrogRepository" +$FullImageWithTag = "${FullImagePath}:${ImageTag}" + +Write-Host "`n=== Azure Container Apps Deployment (JFrog Registry) ===" -ForegroundColor Cyan +Write-Host "Resource Group: $ResourceGroupName" -ForegroundColor White +Write-Host "Container App: $ContainerAppName" -ForegroundColor White +Write-Host "JFrog Registry: $JFrogRegistryServer" -ForegroundColor White +Write-Host "JFrog Repository: $JFrogRepository" -ForegroundColor White +Write-Host "Base Image Registry: $JFrogBaseImageRegistry" -ForegroundColor White +Write-Host "Image: $FullImageWithTag" -ForegroundColor White +Write-Host "Location: $Location" -ForegroundColor White +if ($UseEntraIdForAzureStorage) { + Write-Host "Using Entra ID (Managed Identity) for Azure Storage instead of mounted disk" -ForegroundColor Cyan +} +Write-Host "" + +# Prompt for JFrog password/API key +Write-Host "Step 0: JFrog Authentication" -ForegroundColor Yellow +$secureJFrogPassword = Read-Host -Prompt "Enter JFrog password or API key" -AsSecureString +$jfrogPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto( + [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureJFrogPassword) +) + +if (-not $SkipDockerBuild) { + Write-Host "`nStep 1: Building and pushing Docker image to JFrog..." -ForegroundColor Yellow + + # Check if Docker is running + Write-Host "Checking Docker availability..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Docker is not running. Please start Docker Desktop or Docker service." -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker is available." -ForegroundColor Green + + # Login to JFrog + Write-Host "Logging in to JFrog registry..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + $jfrogPassword | docker login $JFrogRegistryServer -u $JFrogUsername --password-stdin + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to login to JFrog registry" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Successfully logged in to JFrog." -ForegroundColor Green + + # Build the Docker image using JFrog base images + Write-Host "Building Docker image with JFrog base images..." -ForegroundColor Gray + Write-Host " Base image registry: $JFrogBaseImageRegistry" -ForegroundColor Gray + Write-Host " Target image: $FullImageWithTag" -ForegroundColor Gray + + $ErrorActionPreference = 'Continue' + Push-Location .. + try { + docker build ` + -f MongoMigrationWebApp/Dockerfile.jfrog ` + --build-arg JFROG_REGISTRY=$JFrogBaseImageRegistry ` + -t $FullImageWithTag ` + . + + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Docker build failed" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + } + finally { + Pop-Location + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker image built successfully." -ForegroundColor Green + + # Push the image to JFrog + Write-Host "Pushing image to JFrog..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + docker push $FullImageWithTag + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to push image to JFrog" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker image pushed successfully." -ForegroundColor Green + + # Logout from JFrog (cleanup) + docker logout $JFrogRegistryServer 2>&1 | Out-Null +} else { + Write-Host "`nStep 1: Skipping Docker build (SkipDockerBuild flag set)" -ForegroundColor Yellow + Write-Host "Assuming image already exists at: $FullImageWithTag" -ForegroundColor Gray +} + +Write-Host "`nStep 2: Deploying infrastructure (Storage Account, Managed Identity, Container Apps Environment)..." -ForegroundColor Yellow +Write-Host "Note: This may take 3-5 minutes..." -ForegroundColor Gray + +$bicepParams = @( + "deployment", "group", "create", + "--resource-group", $ResourceGroupName, + "--template-file", "aca_main_jfrog.bicep", + "--parameters", + "containerAppName=$ContainerAppName", + "jfrogRegistryServer=$JFrogRegistryServer", + "jfrogUsername=$JFrogUsername", + "jfrogPassword=`"$jfrogPassword`"", + "jfrogImageRepository=$FullImagePath", + "location=$Location", + "storageAccountName=$StorageAccountName", + "vCores=$VCores", + "memoryGB=$MemoryGB", + "ownerTag=$OwnerTag", + "useEntraIdForStorage=$($UseEntraIdForAzureStorage.ToString().ToLower())" +) + +# Add VNet configuration if provided +if (-not [string]::IsNullOrEmpty($InfrastructureSubnetResourceId)) { + Write-Host "VNet integration enabled with subnet: $InfrastructureSubnetResourceId" -ForegroundColor Cyan + $bicepParams += "infrastructureSubnetResourceId=$InfrastructureSubnetResourceId" +} + +Write-Host "Running: az deployment group create..." -ForegroundColor Gray +az @bicepParams + +if ($LASTEXITCODE -ne 0) { + Write-Host "`nError: Infrastructure deployment failed" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 +} + +Write-Host "Infrastructure deployment completed successfully" -ForegroundColor Green + +Write-Host "`nStep 3: Prompting for StateStore connection string..." -ForegroundColor Yellow +$secureConnString = Read-Host -Prompt "The StateStore keeps track of migration job details in a DocumentDB. You may use the same database as the Target DocumentDB or a separate one. Enter the connection string for the StateStore." -AsSecureString +$connString = [Runtime.InteropServices.Marshal]::PtrToStringAuto( + [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureConnString) +) + +Write-Host "`nStep 4: Deploying Container App with application image..." -ForegroundColor Yellow + +$finalBicepParams = @( + "deployment", "group", "create", + "--resource-group", $ResourceGroupName, + "--template-file", "aca_main_jfrog.bicep", + "--parameters", + "containerAppName=$ContainerAppName", + "jfrogRegistryServer=$JFrogRegistryServer", + "jfrogUsername=$JFrogUsername", + "jfrogPassword=`"$jfrogPassword`"", + "jfrogImageRepository=$FullImagePath", + "location=$Location", + "storageAccountName=$StorageAccountName", + "vCores=$VCores", + "memoryGB=$MemoryGB", + "stateStoreAppID=$StateStoreAppID", + "stateStoreConnectionString=`"$connString`"", + "aspNetCoreEnvironment=Development", + "imageTag=$ImageTag", + "ownerTag=$OwnerTag", + "useEntraIdForStorage=$($UseEntraIdForAzureStorage.ToString().ToLower())" +) + +# Add VNet configuration if provided +if (-not [string]::IsNullOrEmpty($InfrastructureSubnetResourceId)) { + $finalBicepParams += "infrastructureSubnetResourceId=$InfrastructureSubnetResourceId" +} + +az @finalBicepParams + +if ($LASTEXITCODE -ne 0) { + Write-Host "`nError: Container App deployment failed" -ForegroundColor Red + Remove-Variable connString, secureConnString, jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 +} + +Remove-Variable connString, secureConnString, jfrogPassword, secureJFrogPassword -ErrorAction Ignore + +Write-Host "`n=== Deployment Complete ===" -ForegroundColor Cyan + +# Deactivate old revisions to free up resources +Write-Host "`nCleaning up old revisions..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' + +$latestRevision = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.latestRevisionName" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + +if ($latestRevision) { + Write-Host "Latest revision: $latestRevision" -ForegroundColor Cyan + + # Get all active revisions + $allRevisions = az containerapp revision list ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "[?properties.active==``true``].name" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + + if ($allRevisions) { + $revisionList = $allRevisions -split "`n" | Where-Object { $_ -and $_ -ne $latestRevision } + + foreach ($oldRevision in $revisionList) { + if ($oldRevision.Trim()) { + Write-Host " Deactivating old revision: $oldRevision" -ForegroundColor Gray + az containerapp revision deactivate ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --revision $oldRevision ` + 2>&1 | Out-Null + } + } + Write-Host "Old revisions deactivated successfully" -ForegroundColor Green + } +} + +$ErrorActionPreference = 'Stop' + +# Step 5: Verify the new image becomes active +Write-Host "`nStep 5: Verifying new image deployment..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' + +# Get the expected replica count from scaling configuration +$scaleConfig = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.template.scale" ` + --output json ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } | ConvertFrom-Json + +$expectedReplicaCount = 1 +if ($scaleConfig.minReplicas) { + $expectedReplicaCount = $scaleConfig.minReplicas +} + +Write-Host "Expected replica count: $expectedReplicaCount (minReplicas: $($scaleConfig.minReplicas), maxReplicas: $($scaleConfig.maxReplicas))" -ForegroundColor Cyan + +# Get the deployed image name +$imageName = $FullImageWithTag + +# Wait for the new container to become ready +Write-Host "`nWaiting for container to become active and healthy..." -ForegroundColor Yellow +$maxAttempts = 60 # 10 minutes (60 * 10 seconds) +$attemptCount = 0 +$isReady = $false + +while ($attemptCount -lt $maxAttempts -and -not $isReady) { + $attemptCount++ + Write-Host "Checking deployment status (attempt $attemptCount/$maxAttempts)..." -ForegroundColor Gray + + # Get the active revision + $activeRevision = az containerapp revision list ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "[?properties.active==``true``].name" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + + if ($activeRevision -and $LASTEXITCODE -eq 0) { + # Get comprehensive revision details + $revisionOutput = az containerapp revision show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --revision $activeRevision ` + --output json ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' -and $_ -notmatch 'ERROR' } + + if ($LASTEXITCODE -eq 0 -and $revisionOutput) { + try { + $revisionInfo = $revisionOutput | ConvertFrom-Json + + $runningState = $revisionInfo.properties.runningState + $provisioningState = $revisionInfo.properties.provisioningState + $healthState = $revisionInfo.properties.healthState + $activeReplicaCount = $revisionInfo.properties.replicas + + # Check if the new image is actually running + $currentImage = $revisionInfo.properties.template.containers[0].image + + Write-Host " Running State: $runningState | Provisioning: $provisioningState | Health: $healthState | Replicas: $activeReplicaCount" -ForegroundColor Gray + Write-Host " Current Image: $currentImage" -ForegroundColor Gray + + # Verify all conditions are met + $imageMatches = $currentImage -eq $imageName + $statesOk = ($runningState -eq "RunningAtMaxScale" -or $runningState -eq "Running") -and ($provisioningState -eq "Provisioned") -and ($healthState -eq "Healthy") + $correctReplicaCount = $activeReplicaCount -eq $expectedReplicaCount + + if ($imageMatches -and $statesOk -and $correctReplicaCount) { + $isReady = $true + Write-Host "`nContainer is fully active and healthy!" -ForegroundColor Green + Write-Host " Running state: $runningState" -ForegroundColor Green + Write-Host " Provisioning state: $provisioningState" -ForegroundColor Green + Write-Host " Health state: $healthState" -ForegroundColor Green + Write-Host " Active replicas: $activeReplicaCount (expected: $expectedReplicaCount)" -ForegroundColor Green + Write-Host " Image verified: $currentImage" -ForegroundColor Green + break + } else { + if (-not $imageMatches) { + Write-Host " Waiting for image to be deployed..." -ForegroundColor Yellow + } + if (-not $statesOk) { + Write-Host " Waiting for container to reach healthy state..." -ForegroundColor Yellow + } + if (-not $correctReplicaCount) { + if ($activeReplicaCount -gt $expectedReplicaCount) { + Write-Host " Waiting for replicas to stabilize ($activeReplicaCount -> $expectedReplicaCount)..." -ForegroundColor Yellow + } else { + Write-Host " Waiting for replicas to start ($activeReplicaCount -> $expectedReplicaCount)..." -ForegroundColor Yellow + } + } + Write-Host " Checking again in 10 seconds..." -ForegroundColor Gray + Start-Sleep -Seconds 10 + } + } + catch { + Write-Host " Error parsing revision info. Retrying in 10 seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } + } else { + Write-Host " Revision info not available yet. Waiting..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } + } else { + Write-Host " Waiting for active revision..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } +} + +if (-not $isReady) { + Write-Host "`nWarning: Container did not become fully active within expected time." -ForegroundColor Yellow + Write-Host "The deployment may still be in progress. Please check the Azure Portal for more details." -ForegroundColor Yellow +} + +$ErrorActionPreference = 'Stop' +Write-Host "" + +# Retrieve and display the application URL +Write-Host "Retrieving application URL..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' +$appUrl = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.configuration.ingress.fqdn" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } +$ErrorActionPreference = 'Stop' + +if ($appUrl) { + Write-Host "" + Write-Host "==========================================" -ForegroundColor Green + Write-Host " Application deployed successfully!" -ForegroundColor Green + Write-Host "==========================================" -ForegroundColor Green + Write-Host " Launch URL: https://$appUrl" -ForegroundColor Cyan + Write-Host " Registry: JFrog ($JFrogRegistryServer)" -ForegroundColor Cyan + Write-Host "==========================================" -ForegroundColor Green + Write-Host "" +} else { + Write-Host "Unable to retrieve application URL. Please check the Azure Portal." -ForegroundColor Yellow +} diff --git a/ACA/update-aca-app-jfrog.ps1 b/ACA/update-aca-app-jfrog.ps1 new file mode 100644 index 0000000..0ad7f5b --- /dev/null +++ b/ACA/update-aca-app-jfrog.ps1 @@ -0,0 +1,351 @@ +# Azure Container Apps - Application Update Script (JFrog Registry Edition) +# Updates only the application image without resetting environment variables and secrets +# Uses JFrog Artifactory as the container registry + +param( + [Parameter(Mandatory=$true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory=$true)] + [string]$ContainerAppName, + + [Parameter(Mandatory=$true)] + [string]$JFrogRegistryServer, + + [Parameter(Mandatory=$true)] + [string]$JFrogUsername, + + [Parameter(Mandatory=$true)] + [string]$JFrogRepository, + + [Parameter(Mandatory=$false)] + [string]$JFrogBaseImageRegistry = "", + + [Parameter(Mandatory=$false)] + [string]$ImageTag = "latest", + + [Parameter(Mandatory=$false)] + [switch]$SkipDockerBuild +) + +$ErrorActionPreference = "Stop" + +# Use the same registry for base images if not specified +if ([string]::IsNullOrEmpty($JFrogBaseImageRegistry)) { + $JFrogBaseImageRegistry = $JFrogRegistryServer +} + +# Build full image path +$FullImagePath = "$JFrogRegistryServer/$JFrogRepository" +$FullImageWithTag = "${FullImagePath}:${ImageTag}" + +Write-Host "`n=== Azure Container App - Image Update (JFrog Registry) ===" -ForegroundColor Cyan +Write-Host "Resource Group: $ResourceGroupName" -ForegroundColor White +Write-Host "Container App: $ContainerAppName" -ForegroundColor White +Write-Host "JFrog Registry: $JFrogRegistryServer" -ForegroundColor White +Write-Host "JFrog Repository: $JFrogRepository" -ForegroundColor White +Write-Host "Image Tag: $ImageTag" -ForegroundColor White +Write-Host "Full Image: $FullImageWithTag" -ForegroundColor White +Write-Host "" + +# Prompt for JFrog password/API key +Write-Host "JFrog Authentication" -ForegroundColor Yellow +$secureJFrogPassword = Read-Host -Prompt "Enter JFrog password or API key" -AsSecureString +$jfrogPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto( + [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureJFrogPassword) +) + +if (-not $SkipDockerBuild) { + Write-Host "`nStep 1: Building and pushing new image to JFrog..." -ForegroundColor Yellow + + # Check if Docker is running + Write-Host "Checking Docker availability..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Docker is not running. Please start Docker Desktop or Docker service." -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker is available." -ForegroundColor Green + + # Login to JFrog + Write-Host "Logging in to JFrog registry..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + $jfrogPassword | docker login $JFrogRegistryServer -u $JFrogUsername --password-stdin + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to login to JFrog registry" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Successfully logged in to JFrog." -ForegroundColor Green + + # Build the Docker image using JFrog base images + Write-Host "Building Docker image with JFrog base images..." -ForegroundColor Gray + Write-Host " Base image registry: $JFrogBaseImageRegistry" -ForegroundColor Gray + Write-Host " Target image: $FullImageWithTag" -ForegroundColor Gray + + $ErrorActionPreference = 'Continue' + Push-Location .. + try { + docker build ` + -f MongoMigrationWebApp/Dockerfile.jfrog ` + --build-arg JFROG_REGISTRY=$JFrogBaseImageRegistry ` + -t $FullImageWithTag ` + . + + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Docker build failed" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + } + finally { + Pop-Location + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker image built successfully." -ForegroundColor Green + + # Push the image to JFrog + Write-Host "Pushing image to JFrog..." -ForegroundColor Gray + $ErrorActionPreference = 'Continue' + docker push $FullImageWithTag + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to push image to JFrog" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 + } + $ErrorActionPreference = 'Stop' + Write-Host "Docker image pushed successfully." -ForegroundColor Green + + # Logout from JFrog (cleanup) + docker logout $JFrogRegistryServer 2>&1 | Out-Null +} else { + Write-Host "`nStep 1: Skipping Docker build (SkipDockerBuild flag set)" -ForegroundColor Yellow + Write-Host "Assuming image already exists at: $FullImageWithTag" -ForegroundColor Gray +} + +# Step 2: Update Container App with new image +Write-Host "`nStep 2: Updating Container App with new image..." -ForegroundColor Yellow +Write-Host "Note: Warnings about cryptography or UserWarnings are normal and can be ignored." -ForegroundColor Gray + +# First, update the registry secret with the new password +Write-Host "Updating JFrog registry secret..." -ForegroundColor Gray +$ErrorActionPreference = 'Continue' +az containerapp secret set ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --secrets "jfrog-password=$jfrogPassword" ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' } + +if ($LASTEXITCODE -ne 0) { + Write-Host "Warning: Could not update JFrog secret. Continuing with existing secret..." -ForegroundColor Yellow +} + +# Update the container app with new image +az containerapp update ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --min-replicas 1 ` + --max-replicas 1 ` + --set template.scale.rules=[] ` + --image $FullImageWithTag ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' } + +if ($LASTEXITCODE -ne 0) { + Write-Host "`nError: Failed to update Container App" -ForegroundColor Red + Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + exit 1 +} +$ErrorActionPreference = 'Stop' + +Remove-Variable jfrogPassword, secureJFrogPassword -ErrorAction Ignore + +Write-Host "`n=== Update Complete ===" -ForegroundColor Cyan +Write-Host "The Container App '$ContainerAppName' has been updated with image: $FullImageWithTag" -ForegroundColor Green +Write-Host "Environment variables and secrets remain unchanged." -ForegroundColor Green +Write-Host "" + +# Deactivate old revisions to free up resources +Write-Host "Cleaning up old revisions..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' + +$latestRevision = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.latestRevisionName" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + +if ($latestRevision) { + Write-Host "Latest revision: $latestRevision" -ForegroundColor Cyan + + # Get all active revisions + $allRevisions = az containerapp revision list ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "[?properties.active==``true``].name" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + + if ($allRevisions) { + $revisionList = $allRevisions -split "`n" | Where-Object { $_ -and $_ -ne $latestRevision } + + foreach ($oldRevision in $revisionList) { + if ($oldRevision.Trim()) { + Write-Host " Deactivating old revision: $oldRevision" -ForegroundColor Gray + az containerapp revision deactivate ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --revision $oldRevision ` + 2>&1 | Out-Null + } + } + Write-Host "Old revisions deactivated successfully" -ForegroundColor Green + } +} + +$ErrorActionPreference = 'Stop' + +# Step 3: Verify the new image becomes active +Write-Host "Step 3: Verifying new image deployment..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' + +# Get the expected replica count from scaling configuration +$scaleConfig = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.template.scale" ` + --output json ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } | ConvertFrom-Json + +$expectedReplicaCount = 1 +if ($scaleConfig.minReplicas) { + $expectedReplicaCount = $scaleConfig.minReplicas +} + +Write-Host "Expected replica count: $expectedReplicaCount (minReplicas: $($scaleConfig.minReplicas), maxReplicas: $($scaleConfig.maxReplicas))" -ForegroundColor Cyan + +# Wait for the new container to become ready +Write-Host "`nWaiting for new image to become active and healthy..." -ForegroundColor Yellow +$maxAttempts = 60 # 10 minutes (60 * 10 seconds) +$attemptCount = 0 +$isReady = $false +$imageName = $FullImageWithTag + +while ($attemptCount -lt $maxAttempts -and -not $isReady) { + $attemptCount++ + Write-Host "Checking deployment status (attempt $attemptCount/$maxAttempts)..." -ForegroundColor Gray + + # Get the active revision + $activeRevision = az containerapp revision list ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "[?properties.active==``true``].name" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } + + if ($activeRevision -and $LASTEXITCODE -eq 0) { + # Get comprehensive revision details + $revisionOutput = az containerapp revision show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --revision $activeRevision ` + --output json ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' -and $_ -notmatch 'ERROR' } + + if ($LASTEXITCODE -eq 0 -and $revisionOutput) { + try { + $revisionInfo = $revisionOutput | ConvertFrom-Json + + $runningState = $revisionInfo.properties.runningState + $provisioningState = $revisionInfo.properties.provisioningState + $healthState = $revisionInfo.properties.healthState + $activeReplicaCount = $revisionInfo.properties.replicas + + # Check if the new image is actually running + $currentImage = $revisionInfo.properties.template.containers[0].image + + Write-Host " Running State: $runningState | Provisioning: $provisioningState | Health: $healthState | Replicas: $activeReplicaCount" -ForegroundColor Gray + Write-Host " Current Image: $currentImage" -ForegroundColor Gray + + # Verify all conditions are met + $imageMatches = $currentImage -eq $imageName + $statesOk = ($runningState -eq "RunningAtMaxScale") -and ($provisioningState -eq "Provisioned") -and ($healthState -eq "Healthy") + $correctReplicaCount = $activeReplicaCount -eq $expectedReplicaCount + + if ($imageMatches -and $statesOk -and $correctReplicaCount) { + $isReady = $true + Write-Host "`nNew image is fully active and healthy!" -ForegroundColor Green + Write-Host " Running state: $runningState" -ForegroundColor Green + Write-Host " Provisioning state: $provisioningState" -ForegroundColor Green + Write-Host " Health state: $healthState" -ForegroundColor Green + Write-Host " Active replicas: $activeReplicaCount (expected: $expectedReplicaCount)" -ForegroundColor Green + Write-Host " Image verified: $currentImage" -ForegroundColor Green + break + } else { + if (-not $imageMatches) { + Write-Host " Waiting for new image to be deployed..." -ForegroundColor Yellow + } + if (-not $statesOk) { + Write-Host " Waiting for container to reach healthy state..." -ForegroundColor Yellow + } + if (-not $correctReplicaCount) { + if ($activeReplicaCount -gt $expectedReplicaCount) { + Write-Host " Waiting for old replica to terminate ($activeReplicaCount -> $expectedReplicaCount)..." -ForegroundColor Yellow + } else { + Write-Host " Waiting for replicas to start ($activeReplicaCount -> $expectedReplicaCount)..." -ForegroundColor Yellow + } + } + Write-Host " Checking again in 10 seconds..." -ForegroundColor Gray + Start-Sleep -Seconds 10 + } + } + catch { + Write-Host " Error parsing revision info. Retrying in 10 seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } + } else { + Write-Host " Revision info not available yet. Waiting..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } + } else { + Write-Host " Waiting for active revision..." -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } +} + +if (-not $isReady) { + Write-Host "`nWarning: New image did not become fully active within expected time." -ForegroundColor Yellow + Write-Host "Current state: Running=$runningState | Provisioning=$provisioningState | Health=$healthState | Replicas=$activeReplicaCount" -ForegroundColor Yellow + Write-Host "The deployment may still be in progress. Please check the Azure Portal for more details." -ForegroundColor Yellow +} + +$ErrorActionPreference = 'Stop' +Write-Host "" + +# Retrieve and display the application URL +Write-Host "Retrieving application URL..." -ForegroundColor Yellow +$ErrorActionPreference = 'Continue' +$appUrl = az containerapp show ` + --name $ContainerAppName ` + --resource-group $ResourceGroupName ` + --query "properties.configuration.ingress.fqdn" ` + --output tsv ` + 2>&1 | Where-Object { $_ -notmatch 'cryptography' -and $_ -notmatch 'UserWarning' -and $_ -notmatch 'WARNING:' } +$ErrorActionPreference = 'Stop' + +if ($appUrl) { + Write-Host "" + Write-Host "===========================================" -ForegroundColor Green + Write-Host " Application updated successfully!" -ForegroundColor Green + Write-Host "===========================================" -ForegroundColor Green + Write-Host " Launch URL: https://$appUrl" -ForegroundColor Cyan + Write-Host " Registry: JFrog ($JFrogRegistryServer)" -ForegroundColor Cyan + Write-Host "===========================================" -ForegroundColor Green + Write-Host "" +} else { + Write-Host "Unable to retrieve application URL. Please check the Azure Portal." -ForegroundColor Yellow +} diff --git a/MongoMigrationWebApp/Dockerfile.jfrog b/MongoMigrationWebApp/Dockerfile.jfrog new file mode 100644 index 0000000..c19c228 --- /dev/null +++ b/MongoMigrationWebApp/Dockerfile.jfrog @@ -0,0 +1,77 @@ +# Dockerfile for JFrog Registry +# Uses JFrog Artifactory as the container registry for base images and final push +# +# PREREQUISITES: +# - JFrog Artifactory with Docker repository configured +# - .NET 9.0 base images mirrored/proxied in JFrog (aspnet:9.0 and sdk:9.0) +# +# BUILD ARGS: +# JFROG_REGISTRY - Your JFrog Docker registry URL (e.g., yourcompany.jfrog.io/docker-virtual) +# +# EXAMPLE BUILD: +# docker build -f Dockerfile.jfrog \ +# --build-arg JFROG_REGISTRY=yourcompany.jfrog.io/docker-virtual \ +# -t yourcompany.jfrog.io/docker-local/mongomigration:latest . +# +# See https://aka.ms/customizecontainer for container customization details + +# JFrog Registry ARG - must be provided at build time +ARG JFROG_REGISTRY + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM ${JFROG_REGISTRY}/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# This stage is used to build the service project +FROM ${JFROG_REGISTRY}/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["MongoMigrationWebApp/MongoMigrationWebApp.csproj", "MongoMigrationWebApp/"] +COPY ["OnlineMongoMigrationProcessor/OnlineMongoMigrationProcessor.csproj", "OnlineMongoMigrationProcessor/"] +RUN dotnet restore "./MongoMigrationWebApp/MongoMigrationWebApp.csproj" +COPY . . +WORKDIR "/src/MongoMigrationWebApp" +RUN dotnet build "./MongoMigrationWebApp.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./MongoMigrationWebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final + +# Switch to root to install MongoDB tools +USER root + +# Install MongoDB Database Tools +RUN apt-get update && \ + apt-get install -y wget gnupg && \ + wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | apt-key add - && \ + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/7.0 main" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list && \ + apt-get update && \ + apt-get install -y mongodb-database-tools && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create the migration data directory structure that the app expects +# The app looks for ResourceDrive/home/ directory structure +RUN mkdir -p /app/migration-data/home && \ + chown -R $APP_UID:$APP_UID /app/migration-data && \ + chmod -R 755 /app/migration-data + +# Switch back to non-root user for security +RUN mkdir -p /app/mongodump /tmp/mongodump && \ + chown -R $APP_UID:$APP_UID /app/mongodump /tmp/mongodump && \ + chmod -R 755 /app/mongodump /tmp/mongodump + +# Switch back to non-root user for security +USER $APP_UID + +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "MongoMigrationWebApp.dll"]