Home / Azure

Azure Local | DevOps – Custom Image Part II

Azure Local | DevOps – Custom Image Part II


Reading Time: 16 minutes

In the previous post: https://www.gettothe.cloud/azure-local-devops-custom-image-part-i/ we have created a pipeline which runs powershell to replicate the image from a Azure Shared Image Gallery to Azure Local.

I assume that the reader already has a basic understanding of Azure DevOps and how pipelines are structured. In my setup, I included all required values, such as image names, gallery IDs, and Azure Local configuration, directly within the pipeline definition to keep everything self‑contained. However, if you are working with multiple images, or if you want to separate configuration from logic, you could just as easily place these values into a Variable Group within Azure DevOps.

A variable group allows you to centralize parameters and reuse them across multiple pipelines or stages. This becomes especially useful when managing several image definitions or environments, ensuring consistency while reducing duplication. Whether the information sits inside the pipeline or in a variable group ultimately depends on your preferred level of flexibility and maintainability.

At the beginning of this, I have two images called 11-252-x. I have determined that I always want to keep only one image on my azure local environment because the rest is in a Azure Share Image Gallery.

The Pipeline

Parameters

parameters:
- name: environmentName
  displayName: 'Target Environment'
  type: string
  default: 'Production'
  values:
  - 'Development'
  - 'Test'
  - 'Production'

- name: subscriptionId
  displayName: 'Azure Subscription ID'
  type: string
  default: ''

- name: resourceGroup
  displayName: 'AZL Resource Group'
  type: string
  default: ''

- name: customLocationName
  displayName: 'Custom Location Name'
  type: string
  default: ''

- name: galleryName
  displayName: 'Azure Compute Gallery Name'
  type: string
  default: ''

- name: imageDefinition
  displayName: 'Image Definition Name'
  type: string
  default: ''

- name: imgResourceGroup
  displayName: 'Gallery Resource Group'
  type: string
  default: ''

- name: imagesToKeep
  displayName: 'Number of Images to Keep'
  type: number
  default: 1

- name: dryRun
  displayName: 'Dry Run (Preview Only - No Deletion)'
  type: boolean
  default: false
ParameterTypeDescriptionDefault
environmentNamestringTarget environment (Development/Test/Production)Production
subscriptionIdstringAzure subscription ID
resourceGroupstringAZL resource group name
customLocationNamestringAzure Arc custom location name
galleryNamestringAzure Compute Gallery name
imageDefinitionstringImage definition name in the gallery
imgResourceGroupstringResource group containing the gallery
imagesToKeepnumberNumber of most recent images to retain1
dryRunbooleanPreview mode without actual deletionfalse

Variables

variables:
- name: azureServiceConnection
  value: ''  # Update with your service connection name

This variable is essential for establishing the connection to Azure. It provides the authentication context that allows the pipeline to interact with your subscription and deploy the required resources. Without this value, the pipeline would not be able to authenticate against Azure Resource Manager, meaning no deployments, updates, or image transfers could be executed. By defining this variable, either directly in the pipeline or within a variable group, you ensure that the DevOps pipeline can securely access Azure and perform the operations needed to automate your workflow.

Pipeline architecture

The pipeline is structured into four distinct stages, each with a clear purpose to ensure the deployment process remains organized, maintainable, and easy to follow. By breaking the workflow into stages, it becomes easier to manage responsibilities, troubleshoot issues, and extend the pipeline when new requirements arise. This staged approach also makes the automation more transparent, allowing you to see exactly where each task is executed and how the overall process flows from start to finish.

  1. ValidateEnvironment – Environment validation and prerequisites check
  2. AnalyzeImages – Discover and analyze images for cleanup
  3. CleanupImages – Delete old images
  4. Verification – Verify successful image import
Image

1. ValidateEnvironment

The purpose of this stage is to validate the execution environment and ensure that all required prerequisites are in place before the pipeline proceeds. This includes checking that the necessary Azure DevOps variables are available, verifying the connection to Azure, confirming that required modules or tooling are installed, and ensuring that all configuration values are correctly set. By performing these checks upfront, the pipeline reduces the risk of failures in later stages and guarantees that the automation can run reliably from start to finish.

Steps:
  1. Check PowerShell Version
    • Displays PowerShell environment information
    • Validates compatibility with required cmdlets
  2. Verify and Install Required Modules
    • Checks for required modules:
      • Az.Accounts
      • Az.Compute
      • Az.Resources
      • Az.StackHCI
      • Az.StackHCIVM
    • Automatically installs missing modules
    • Fails pipeline if installation errors occur
  3. Verify Azure Connectivity
    • Tests Azure authentication
    • Displays subscription, account, and tenant details
    • Ensures service principal has necessary permissions
- stage: ValidateEnvironment
  displayName: 'Validate Environment & Modules'
  jobs:
  - job: PreflightChecks
    displayName: 'Preflight Checks'
    pool:
      vmImage: 'windows-latest'

    steps:
    - task: PowerShell@2
      displayName: 'Check PowerShell Version'
      inputs:
        targetType: 'inline'
        script: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "PowerShell Environment Check" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
          Write-Host "OS: $($PSVersionTable.OS)"
          Write-Host "Platform: $($PSVersionTable.Platform)"

    - task: AzurePowerShell@5
      displayName: 'Verify and Install Required Modules'
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "PowerShell Module Verification" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan

          $requiredModules = @(
            'Az.Accounts',
            'Az.Compute',
            'Az.Resources',
            'Az.StackHCI',
            'Az.StackHCIVM'
          )

          foreach ($moduleName in $requiredModules) {
            Write-Host "`nChecking module: $moduleName" -ForegroundColor Yellow
            $module = Get-Module -ListAvailable -Name $moduleName | Select-Object -First 1
            
            if ($module) {
              Write-Host "  ✓ Found $moduleName version $($module.Version)" -ForegroundColor Green
            } else {
              Write-Host "  ⚠ Module $moduleName not found. Installing..." -ForegroundColor Yellow
              try {
                Install-Module -Name $moduleName -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop
                Write-Host "  ✓ Successfully installed $moduleName" -ForegroundColor Green
              }
              catch {
                Write-Host "  ✗ Failed to install $moduleName : $($_.Exception.Message)" -ForegroundColor Red
                exit 1
              }
            }
          }

          Write-Host "`n========================================" -ForegroundColor Green
          Write-Host "All required modules verified" -ForegroundColor Green
          Write-Host "========================================" -ForegroundColor Green
        azurePowerShellVersion: 'LatestVersion'

    - task: AzurePowerShell@5
      displayName: 'Verify Azure Connectivity'
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "Verifying Azure connectivity..." -ForegroundColor Cyan

          $context = Get-AzContext
          Write-Host "Connected to Azure" -ForegroundColor Green
          Write-Host "  Subscription: $($context.Subscription.Name)" -ForegroundColor White
          Write-Host "  Account: $($context.Account.Id)" -ForegroundColor White
          Write-Host "  Tenant: $($context.Tenant.Id)" -ForegroundColor White
        azurePowerShellVersion: 'LatestVersion'

2. AnalyzeImages

This stage analyzes the existing images within the Azure Local environment and evaluates them against the defined retention policy. It identifies which images should be kept, typically the most recent or currently deployed versions, and which older or obsolete images are eligible for removal. By applying this retention logic, the pipeline prevents unnecessary growth of stored images, optimizes storage usage, and keeps the environment clean and manageable. This automated housekeeping step ensures that only relevant and compliant images remain available, eliminating the risk of accumulating outdated versions over time. As a result, your Azure Local environment stays efficient, organized, and aligned with your operational lifecycle and governance practices.

Steps:
  1. Checkout Repository
    • Ensures access to pipeline configuration
  2. Set Azure Subscription ContextSet-AzContext -SubscriptionId “${{ parameters.subscriptionId }}”
    • Sets the target subscription for all operations
  3. Resolve Custom Location (Output: customLocationID)
    • Retrieves Azure Arc custom location resource ID
    • Validates the custom location exists
    • Required for AZL image operations
  4. Get Latest Gallery Image Version (Output: imageVersionimagePattern)
    • Queries Azure Compute Gallery for the latest image version
    • Filters out images marked as ExcludeFromLatest
    • Creates a pattern for matching related images
    Example:Image version: 1.0.20250115 Pattern: 1-0*
  5. List Current AZL Images (Output: imagesToDeletedeleteCountcleanupRequired)
    • Retrieves all images from AZL cluster
    • Filters images matching the version pattern
    • Determines which images exceed retention limit
    • Calculates images to delete and images to keep
Image
- stage: AnalyzeImages
  displayName: 'Analyze Images'
  dependsOn: ValidateEnvironment
  jobs:
  - job: ImageAnalysis
    displayName: 'Analyze Current Images'
    pool:
      vmImage: 'windows-latest'

    steps:
    - checkout: self
      displayName: 'Checkout Repository'

    - task: AzurePowerShell@5
      displayName: 'Set Azure Subscription Context'
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "Setting Azure subscription context..." -ForegroundColor Cyan
          Write-Host "Subscription ID: ${{ parameters.subscriptionId }}" -ForegroundColor Yellow

          Set-AzContext -SubscriptionId "${{ parameters.subscriptionId }}"

          $context = Get-AzContext
          Write-Host "Subscription context set successfully" -ForegroundColor Green
          Write-Host "  Name: $($context.Subscription.Name)" -ForegroundColor White
        azurePowerShellVersion: 'LatestVersion'

    - task: AzurePowerShell@5
      displayName: 'Resolve Custom Location'
      name: ResolveCustomLocation
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "Resolving Custom Location resource ID..." -ForegroundColor Cyan
          Write-Host "Resource Group: ${{ parameters.resourceGroup }}" -ForegroundColor Yellow
          Write-Host "Custom Location: ${{ parameters.customLocationName }}" -ForegroundColor Yellow

          try {
            $customLocation = Get-AzResource `
              -ResourceGroupName "${{ parameters.resourceGroup }}" `
              -ResourceType "Microsoft.ExtendedLocation/customLocations" `
              -Name "${{ parameters.customLocationName }}" `
              -ErrorAction Stop
            
            $customLocationID = $customLocation.ResourceId
            
            if ([string]::IsNullOrEmpty($customLocationID)) {
              throw "Custom Location ID is null or empty"
            }
            
            Write-Host "Custom Location ID resolved: $customLocationID" -ForegroundColor Green
            Write-Host "##vso[task.setvariable variable=customLocationID;isOutput=true]$customLocationID"
          }
          catch {
            Write-Host "##vso[task.logissue type=error]Failed to resolve Custom Location: $($_.Exception.Message)"
            exit 1
          }
        azurePowerShellVersion: 'LatestVersion'

    - task: AzurePowerShell@5
      displayName: 'Get Latest Gallery Image Version'
      name: GetImageVersion
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Image Version Discovery" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Gallery: ${{ parameters.galleryName }}" -ForegroundColor Yellow
          Write-Host "Definition: ${{ parameters.imageDefinition }}" -ForegroundColor Yellow
          Write-Host "Resource Group: ${{ parameters.imgResourceGroup }}" -ForegroundColor Yellow

          try {
            $sourceImgVer = Get-AzGalleryImageVersion `
              -GalleryImageDefinitionName "${{ parameters.imageDefinition }}" `
              -GalleryName "${{ parameters.galleryName }}" `
              -ResourceGroupName "${{ parameters.imgResourceGroup }}" `
              -ErrorAction Stop |
              Where-Object { $_.PublishingProfile.ExcludeFromLatest -eq $false } |
              Select-Object -Last 1
            
            if ($null -eq $sourceImgVer) {
              throw "No valid image version found in the gallery"
            }
            
            $imageVersion = $sourceImgVer.Name
            $imagePattern = $sourceImgVer.Name.Replace(".", "-").split("-")[0..1] -join "-"
            
            Write-Host "`nFound latest image version:" -ForegroundColor Green
            Write-Host "  Version: $imageVersion" -ForegroundColor White
            Write-Host "  Pattern: $imagePattern*" -ForegroundColor White
            
            Write-Host "##vso[task.setvariable variable=imageVersion;isOutput=true]$imageVersion"
            Write-Host "##vso[task.setvariable variable=imagePattern;isOutput=true]$imagePattern"
          }
          catch {
            Write-Host "##vso[task.logissue type=error]Failed to retrieve image version: $($_.Exception.Message)"
            exit 1
          }
        azurePowerShellVersion: 'LatestVersion'

    - task: AzurePowerShell@5
      displayName: 'List Current AZL Images'
      name: ListImages
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Current AZL Images Analysis" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan

          $imagePattern = "$(GetImageVersion.imagePattern)"

          try {
            $allImages = Get-AzStackHciVMimage -ResourceGroupName "${{ parameters.resourceGroup }}" -ErrorAction Stop
            $existingHCIImages = $allImages.Name
            
            Write-Host "Total images in AZL cluster: $($existingHCIImages.Count)" -ForegroundColor White
            
            # Filter images matching the pattern
            $customImages = $existingHCIImages | Where-Object { $_ -like "$imagePattern*" }
            
            if ($customImages) {
              Write-Host "`nImages matching pattern '$imagePattern*':" -ForegroundColor Cyan
              $customImages | ForEach-Object { Write-Host "  - $_" -ForegroundColor White }
              Write-Host "`nTotal matching images: $($customImages.Count)" -ForegroundColor Yellow
              Write-Host "Images to keep: ${{ parameters.imagesToKeep }}" -ForegroundColor Yellow
              
              if ($customImages.Count -gt ${{ parameters.imagesToKeep }}) {
                $deleteCount = $customImages.Count - ${{ parameters.imagesToKeep }}
                $imagesToKeep = $customImages | Select-Object -Last ${{ parameters.imagesToKeep }}
                $imagesToDelete = $customImages | Select-Object -First $deleteCount
                
                Write-Host "`n⚠ CLEANUP REQUIRED ⚠" -ForegroundColor Yellow
                Write-Host "Number of images to delete: $deleteCount" -ForegroundColor Red
                
                Write-Host "`nImages that will be KEPT:" -ForegroundColor Green
                $imagesToKeep | ForEach-Object { Write-Host "  ✓ $_" -ForegroundColor Green }
                
                Write-Host "`nImages that will be DELETED:" -ForegroundColor Red
                $imagesToDelete | ForEach-Object { Write-Host "  ✗ $_" -ForegroundColor Red }
                
                $deleteList = $imagesToDelete -join ','
                Write-Host "##vso[task.setvariable variable=imagesToDelete;isOutput=true]$deleteList"
                Write-Host "##vso[task.setvariable variable=deleteCount;isOutput=true]$deleteCount"
                Write-Host "##vso[task.setvariable variable=cleanupRequired;isOutput=true]true"
              } else {
                Write-Host "`n✓ No cleanup required" -ForegroundColor Green
                Write-Host "Image count ($($customImages.Count)) is within retention limit (${{ parameters.imagesToKeep }})" -ForegroundColor Green
                Write-Host "##vso[task.setvariable variable=cleanupRequired;isOutput=true]false"
              }
            } else {
              Write-Host "`nNo images found matching pattern '$imagePattern*'" -ForegroundColor Yellow
              Write-Host "##vso[task.setvariable variable=cleanupRequired;isOutput=true]false"
            }
          }
          catch {
            Write-Host "##vso[task.logissue type=error]Failed to list AZL images: $($_.Exception.Message)"
            exit 1
          }
        azurePowerShellVersion: 'LatestVersion'

3. CleanUpImages

This stage is responsible for deleting outdated images that exceed the defined retention policy. After the previous step identifies which versions are no longer required, this cleanup stage safely removes those older images from the Azure Local environment. By automatically enforcing the retention rules, the pipeline prevents unnecessary storage consumption, reduces operational clutter, and ensures that only relevant, compliant, and actively maintained image versions remain available. This structured removal process helps maintain a clean and efficient environment while minimizing the risk of confusion or accidental use of deprecated image versions. Ultimately, this step ensures long‑term sustainability and alignment with organizational lifecycle and governance standards.

Steps:
  1. Delete Old AZL Images
    • Condition: Only if dryRun is false
    • Iterates through each image to delete
    • Uses Remove-AzStackHciVMimage cmdlet
    • Executes with -NoWait for asynchronous deletion
    • Tracks success/failure for each image
Screenshot 2026 01 22 at 13.53.01
- stage: CleanupImages
  displayName: 'Cleanup Old Images'
  dependsOn: AnalyzeImages
  condition: and(succeeded(), eq(dependencies.AnalyzeImages.outputs['ImageAnalysis.ListImages.cleanupRequired'], 'true'))
  jobs:
  - job: DeleteImages
    displayName: 'Delete Old Images'
    condition: succeeded()
    pool:
      vmImage: 'windows-latest'

    variables:
      imagesToDelete: $[ stageDependencies.AnalyzeImages.ImageAnalysis.outputs['ListImages.imagesToDelete'] ]
      deleteCount: $[ stageDependencies.AnalyzeImages.ImageAnalysis.outputs['ListImages.deleteCount'] ]

    steps:
    - task: AzurePowerShell@5
      displayName: 'Delete Old AZL Images'
      condition: ne('${{ parameters.dryRun }}', 'true')
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Deleting Old AZL Images" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan

          Set-AzContext -SubscriptionId "${{ parameters.subscriptionId }}"

          $imagesToDelete = "$(imagesToDelete)" -split ','

          Write-Host "Number of images to delete: $($imagesToDelete.Count)" -ForegroundColor Yellow
          Write-Host "`nStarting deletion process..." -ForegroundColor Cyan

          $deletionResults = @()

          foreach ($img in $imagesToDelete) {
            Write-Host "`nDeleting image: $img" -ForegroundColor Yellow
            try {
              $result = Remove-AzStackHciVMimage `
                -ResourceGroupName "${{ parameters.resourceGroup }}" `
                -Name $img `
                -Force `
                -NoWait `
                -ErrorAction Stop
              
              Write-Host "  ✓ Deletion initiated for: $img" -ForegroundColor Green
              $deletionResults += [PSCustomObject]@{
                ImageName = $img
                Status = "Initiated"
                Timestamp = Get-Date
              }
            }
            catch {
              Write-Host "  ✗ Failed to delete: $img" -ForegroundColor Red
              Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
              Write-Host "##vso[task.logissue type=warning]Failed to delete image $img : $($_.Exception.Message)"
              $deletionResults += [PSCustomObject]@{
                ImageName = $img
                Status = "Failed"
                Error = $_.Exception.Message
                Timestamp = Get-Date
              }
            }
          }

          Write-Host "`n========================================" -ForegroundColor Green
          Write-Host "Deletion Summary" -ForegroundColor Green
          Write-Host "========================================" -ForegroundColor Green
          $deletionResults | Format-Table -AutoSize

          $successCount = ($deletionResults | Where-Object { $_.Status -eq "Initiated" }).Count
          $failCount = ($deletionResults | Where-Object { $_.Status -eq "Failed" }).Count

          Write-Host "`nSuccessfully initiated: $successCount" -ForegroundColor Green
          Write-Host "Failed: $failCount" -ForegroundColor $(if($failCount -gt 0){'Red'}else{'Green'})
        azurePowerShellVersion: 'LatestVersion'

    - task: PowerShell@2
      displayName: 'Dry Run - Preview Only'
      condition: eq('${{ parameters.dryRun }}', 'true')
      inputs:
        targetType: 'inline'
        script: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "DRY RUN MODE - No Changes Made" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan

          $imagesToDelete = "$(imagesToDelete)" -split ','

          Write-Host "`nThe following images WOULD BE deleted:" -ForegroundColor Yellow
          $imagesToDelete | ForEach-Object {
            Write-Host "  ✗ $_" -ForegroundColor Yellow
          }

          Write-Host "`nTotal images that would be deleted: $($imagesToDelete.Count)" -ForegroundColor Yellow
          Write-Host "`n⚠ DRY RUN MODE: No actual deletion performed" -ForegroundColor Cyan
          Write-Host "Set 'Dry Run' parameter to 'false' to execute actual deletion" -ForegroundColor Cyan

4. Verification

This stage verifies that the cleanup operation completed successfully and ensures that all images flagged for removal were properly deleted from the Azure Local environment. It performs a final validation pass by comparing the post‑cleanup state against the retention policy to confirm that only authorized images remain. Any discrepancies, failures, or skipped items are captured and reported. In addition, the stage generates a clear and concise execution summary, providing insight into which images were removed, which were retained, and whether any follow‑up actions are required. By closing the workflow with validation and reporting, the pipeline ensures accuracy, transparency, and confidence in the automated cleanup process.

Screenshot 2026 01 22 at 13.55.26
- stage: Verification
  displayName: 'Verify Cleanup'
  dependsOn:
  - AnalyzeImages
  - CleanupImages
  condition: and(succeeded(), eq(dependencies.AnalyzeImages.outputs['ImageAnalysis.ListImages.cleanupRequired'], 'true'))
  jobs:
  - job: VerifyCleanup
    displayName: 'Verify Image Cleanup'
    pool:
      vmImage: 'windows-latest'

    variables:
      imagePattern: $[ stageDependencies.AnalyzeImages.ImageAnalysis.outputs['GetImageVersion.imagePattern'] ]

    steps:
    - task: AzurePowerShell@5
      displayName: 'Verify Remaining Images'
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'inlineScript'
        inline: |
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Post-Cleanup Verification" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan

          Set-AzContext -SubscriptionId "${{ parameters.subscriptionId }}"

          $imagePattern = "$(imagePattern)"

          # Wait a few seconds for deletions to register
          Write-Host "Waiting for deletion operations to register..." -ForegroundColor Yellow
          Start-Sleep -Seconds 10

          try {
            $allImages = Get-AzStackHciVMimage -ResourceGroupName "${{ parameters.resourceGroup }}" -ErrorAction Stop
            $remainingImages = $allImages.Name | Where-Object { $_ -like "$imagePattern*" }
            
            Write-Host "`nRemaining images matching pattern '$imagePattern*':" -ForegroundColor Cyan
            if ($remainingImages) {
              $remainingImages | ForEach-Object { Write-Host "  - $_" -ForegroundColor White }
              Write-Host "`nTotal remaining images: $($remainingImages.Count)" -ForegroundColor Green
              
              if ($remainingImages.Count -le ${{ parameters.imagesToKeep }}) {
                Write-Host "`n✓ Cleanup verification successful" -ForegroundColor Green
                Write-Host "Image count is within retention limit (${{ parameters.imagesToKeep }})" -ForegroundColor Green
              } else {
                Write-Host "`n⚠ Warning: Image count ($($remainingImages.Count)) still exceeds limit (${{ parameters.imagesToKeep }})" -ForegroundColor Yellow
                Write-Host "Some deletion operations may still be in progress." -ForegroundColor Yellow
              }
            } else {
              Write-Host "  No images remaining" -ForegroundColor Yellow
            }
          }
          catch {
            Write-Host "##vso[task.logissue type=warning]Could not verify cleanup: $($_.Exception.Message)"
          }
        azurePowerShellVersion: 'LatestVersion'

    - task: PowerShell@2
      displayName: 'Pipeline Execution Summary'
      inputs:
        targetType: 'inline'
        script: |
          Write-Host ""
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Pipeline Execution Summary" -ForegroundColor Cyan
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host "Environment: ${{ parameters.environmentName }}" -ForegroundColor White
          Write-Host "Subscription: ${{ parameters.subscriptionId }}" -ForegroundColor White
          Write-Host "AZL Resource Group: ${{ parameters.resourceGroup }}" -ForegroundColor White
          Write-Host "Gallery: ${{ parameters.galleryName }}" -ForegroundColor White
          Write-Host "Definition: ${{ parameters.imageDefinition }}" -ForegroundColor White
          Write-Host "Images to Keep: ${{ parameters.imagesToKeep }}" -ForegroundColor White
          Write-Host "Dry Run Mode: ${{ parameters.dryRun }}" -ForegroundColor White
          Write-Host "========================================" -ForegroundColor Cyan
          Write-Host ""
          Write-Host "Next Steps:" -ForegroundColor Yellow
          Write-Host "1. Verify remaining images in Azure portal" -ForegroundColor White
          Write-Host "2. Monitor AZL storage utilization" -ForegroundColor White
          Write-Host "3. Review deletion logs for any failures" -ForegroundColor White
          Write-Host "4. Schedule regular cleanup runs" -ForegroundColor White
          Write-Host ""
          Write-Host "Verification Command:" -ForegroundColor Yellow
          Write-Host "Get-AzStackHciVMimage -ResourceGroupName '${{ parameters.resourceGroup }}'" -ForegroundColor Cyan

In addition to the core functionality, the pipeline includes a dry‑run mode that allows you to execute the full workflow without performing any actual deletions. This feature is particularly useful when validating your retention policy, reviewing which images would be removed, or testing changes to the pipeline logic. By enabling dry‑run, you can safely simulate cleanup operations and verify expected behavior before applying any destructive actions in your Azure Local environment.

Furthermore, the pipeline’s schedule trigger is intentionally commented out by default. This gives you complete flexibility to define your own desired execution frequency, whether it’s daily, weekly, or aligned with your internal release cycles. By customizing the schedule, you ensure the automation runs at the most appropriate time for your operational processes without modifying the pipeline’s core logic.

Image

Share and Enjoy !

Shares

Designer (23)

Stay close to the action—follow GetToThe.Cloud across social!
Deep dives and hands‑on how‑tos on Azure Local, hybrid cloud, automation, PowerShell/Bicep, AVD + FSLogix, image pipelines, monitoring, networking, and resilient design when the internet/Azure is down.

🔗 Our channels
▶️ YouTube: https://www.youtube.com/channel/UCa33PgGdXt-Dr4w3Ub9hrdQ
💼 LinkedIn Group: https://www.linkedin.com/groups/9181126/
✖️ X (Twitter): https://x.com/Gettothecloud
🎵 TikTok: https://www.tiktok.com/@gettothecloud
🐙 GitHub: https://github.com/GetToThe-Cloud/Website
💬 Slack: DM us for an invite
📲 WhatsApp: DM for the community link

We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners. View more
Cookies settings
Accept
Privacy & Cookie policy
Privacy & Cookies policy
Cookie name Active

Who we are

Our website address is: https://www.gettothe.cloud

Comments

When visitors leave comments on the site we collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.An anonymized string created from your email address (also called a hash) may be provided to the Gravatar service to see if you are using it. The Gravatar service privacy policy is available here: https://automattic.com/privacy/. After approval of your comment, your profile picture is visible to the public in the context of your comment.

Media

If you upload images to the website, you should avoid uploading images with embedded location data (EXIF GPS) included. Visitors to the website can download and extract any location data from images on the website.

Cookies

If you leave a comment on our site you may opt-in to saving your name, email address and website in cookies. These are for your convenience so that you do not have to fill in your details again when you leave another comment. These cookies will last for one year.If you visit our login page, we will set a temporary cookie to determine if your browser accepts cookies. This cookie contains no personal data and is discarded when you close your browser.When you log in, we will also set up several cookies to save your login information and your screen display choices. Login cookies last for two days, and screen options cookies last for a year. If you select "Remember Me", your login will persist for two weeks. If you log out of your account, the login cookies will be removed.If you edit or publish an article, an additional cookie will be saved in your browser. This cookie includes no personal data and simply indicates the post ID of the article you just edited. It expires after 1 day.

Embedded content from other websites

Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other websites behaves in the exact same way as if the visitor has visited the other website.These websites may collect data about you, use cookies, embed additional third-party tracking, and monitor your interaction with that embedded content, including tracking your interaction with the embedded content if you have an account and are logged in to that website.

Who we share your data with

If you request a password reset, your IP address will be included in the reset email.

How long we retain your data

If you leave a comment, the comment and its metadata are retained indefinitely. This is so we can recognize and approve any follow-up comments automatically instead of holding them in a moderation queue.For users that register on our website (if any), we also store the personal information they provide in their user profile. All users can see, edit, or delete their personal information at any time (except they cannot change their username). Website administrators can also see and edit that information.

What rights you have over your data

If you have an account on this site, or have left comments, you can request to receive an exported file of the personal data we hold about you, including any data you have provided to us. You can also request that we erase any personal data we hold about you. This does not include any data we are obliged to keep for administrative, legal, or security purposes.

Where we send your data

Visitor comments may be checked through an automated spam detection service.
Save settings
Cookies settings