Skip to main content

Azure Virtual Desktops Gold Image Windows Update Automation

Howdy Folks

During a recent customer engagement, I had to deploy Azure Virtual Desktop solutions via DevOps pipelines. Due to some reasons, we decided to go down the gold image option for the shared hostpools. So I ended up creating compute galleries and saved gold image files for the different hostpools

This leads me to think of a way to update these gold images without spending much time and deploying them to hostpools. And found a way to do it.

There may be other way to do it but, I think based on the scenario and the services I used int this solution, this method is the ideal one

As usual below diagram explains the flow of my pipeline.


For this process other than azure pipelines, the main component that I will be using is packer template.

For those who are new to packer...

"Packer is HashiCorp's open-source tool for creating machine images from source configuration. You can configure Packer images with an operating system and software for your specific use-case. Terraform configuration for a compute instance can use a Packer image to provision your instance without manual configuration."more details

I'm combining packer templates with bicep to complete the tasks that I need

Explanation

Azure Pipeline

Stage 1 - In stage 1 I'm constructing the image version details including the current and new version details, reason for this is I'm using these for grabbing and saving the new image version. And also when I'm deploying virtual machines, I tag the VM name with the image version as an example

Tagging the VM name with the image version is helpful to identify new and old VMs when it comes to when we put them on drain mode etc

    - task: AzurePowerShell@5
displayName: 'Generate Image Version Details'
inputs:
azureSubscription: ${{ variables.azureServiceConnection }}
ScriptType: 'inlineScript'
Inline: |
Select-AzSubscription -SubscriptionId ${{ variables.extAccSubID }}

$source_imageVerions = Get-AzGalleryImageVersion -ResourceGroupName $(extaccimageGRgName) -GalleryName ${{ variables.extaccimageGName }} -GalleryImageDefinitionName $(imageDefinitionName) `
| where-object {$_.PublishingProfile.excludeFromLatest -eq $False} | Select-object Name -ExpandProperty Name -Last 1 | Sort-Object Name

$source_image = [decimal]$source_imageVerions.replace('.','')
$startingCount = $source_image | Measure-Object -Character
$target_imageVerions = $source_image + 1
$endCount = [string]$target_imageVerions| Measure-Object -Character

for ( [int]$endCount.Characters -eq [int]$startingCount.Characters){
if ([int]$endCount.Characters -ge [int]$startingCount.Characters) {
break;
$target_imageVerions = "0" + $target_imageVerions
$endCount = [string]$target_imageVerions| Measure-Object -Character
}
}

$target_imageVerions_array = $target_imageVerions -split ""
$target_imageVerions = $target_imageVerions_array[1] + '.' + $target_imageVerions_array[2] + '.'+ $target_imageVerions_array[3]

Write-Host "##vso[task.setvariable variable=target_imageVerions;]$target_imageVerions"
Write-Host "##vso[task.setvariable variable=source_imageVerions;]$source_imageVerions"
FailOnStandardError: false
azurePowerShellVersion: LatestVersion
pwsh: true


Stage 2 - In my second stage I'm performing the tasks required for the packer image build. Tasks are as per below

  1. Create a temporary resource group
  2. Permission delegations to perform the changes
  3. Policy exclusions
  4. Create a packer image and save in image gallery

    - task: AzurePowerShell@5
displayName: Create Temporary Resource Group
inputs:
azureSubscription: ${{ variables.azureServiceConnection }}
ScriptType: InlineScript
Inline: |
Select-AzSubscription -SubscriptionId ${{ variables.extAccSubID }}
$tempname = 'rg-pkr-'+(new-guid).ToString().Substring(0,10)
New-AzResourceGroup -Name $tempname -Location ${{ variables.Location }}
write-output ("##vso[task.setvariable variable=TempResourceGroup;]$tempname")
FailOnStandardError: false
azurePowerShellVersion: LatestVersion
pwsh: true

- task: AzureCLI@2
displayName: Role Base Access Controls
inputs:
azureSubscription: ${{ variables.azureServiceConnection }}
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
az role assignment create --assignee 2a44b716-f6b8-4d21-90b5-a89e23734bad --role Contributor --scope /subscriptions/<sub Id>

- task: AzurePowerShell@5
displayName: Set temporary policy exemption on Build resource group
inputs:
azureSubscription: ${{ variables.azureServiceConnection }}
ScriptType: InlineScript
Inline: |
Set-AzContext "${{ variables.extAccSubID }}"
$ResourceGroup = Get-AzResourceGroup -Name "$(TempResourceGroup)"
$policies = "${{ variables.policyExemption }}" -split ','
$assignment = Get-AzPolicyAssignment -Id '/providers/Microsoft.Management/managementGroups/ExtAccess/providers/Microsoft.Authorization/policyAssignments/SecGovAssign'
New-AzPolicyExemption -Name "AvdBuildExemption-$($policy)" -PolicyAssignment $Assignment -Scope $ResourceGroup.ResourceId -ExemptionCategory Mitigated

$assignment = Get-AzPolicyAssignment -Id '/providers/Microsoft.Management/managementGroups/Symal/providers/Microsoft.Authorization/policyAssignments/SecGovAssign'
New-AzPolicyExemption -Name "AvdBuildExemption-RootPolicy" -PolicyAssignment $Assignment -Scope $ResourceGroup.ResourceId -ExemptionCategory Mitigated

FailOnStandardError: false
azurePowerShellVersion: LatestVersion
pwsh: true

- task: PackerBuild@1
displayName: 'Build packer image'
timeoutInMinutes: 120
inputs:
templateType: 'custom'
customTemplateLocation: 'bicep/main/data-platform/extacc/image-build/packer.json'
customTemplateParameters: '{"client_id":"$(packerBuildClientID)","client_secret":"$(packerClientSecret)","subscription_id":"$(extAccSubID)","tenant_id":"$(tenantID)","gallery_subscription_id":"$(extAccSubID)","build_resource_group_name":"$(TempResourceGroup)","gallery_resource_group_name":"$(extaccimageGRgName)","gallery_name":"$(extaccimageGName)","image_name":"$(imageDefinitionName)","source_image_version":"$(source_imageVerions)","target_image_version":"$(target_imageVerions)"}'
packerVersion: 1.8.2
imageId: 'managedImageID'

For packer Im using the native task packerbuild to deploy


Packer Template

packer template has 3 sections


  1. Variables - variable required for packer image build.

  2. Builders - Configuration settings for the builder image

  3. Provisioners - Tasks to perform ontop of the build vm
{
"variables": {
"client_id": "",
"client_secret": "",
"tenant_id": "",
"subscription_id": "",
"gallery_subscription_id": "",
"resource_group_name": "",
"build_resource_group_name": "",
"gallery_resource_group_name": "",
"gallery_name": "",
"image_name": "",
"source_image_version": "",
"target_image_version": "",
"WorkingDirectory": "c:\\users\\packer",
"buildartifactsCont": "build",
"admin_user": "packer"

},
"builders": [
{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"tenant_id": "{{user `tenant_id`}}",
"subscription_id": "{{user `subscription_id`}}",
"managed_image_resource_group_name": "{{user `build_resource_group_name`}}",
"managed_image_name": "packer-image",
"build_resource_group_name": "{{user `build_resource_group_name`}}",
"os_type": "Windows",
"shared_image_gallery": {
"subscription": "{{user `gallery_subscription_id`}}",
"resource_group": "{{user `gallery_resource_group_name`}}",
"gallery_name": "{{user `gallery_name`}}",
"image_name": "{{user `image_name`}}",
"image_version": "{{user `source_image_version`}}"
},
"shared_image_gallery_destination": {
"subscription": "{{user `gallery_subscription_id`}}",
"resource_group": "{{user `gallery_resource_group_name`}}",
"gallery_name": "{{user `gallery_name`}}",
"image_name": "{{user `image_name`}}",
"image_version": "{{user `target_image_version`}}",
"replication_regions": [
"australiaeast"
],
"storage_account_type": "Standard_LRS"
},
"communicator": "winrm",
"winrm_use_ssl": true,
"winrm_insecure": true,
"winrm_timeout": "60m",
"winrm_username": "{{user `admin_user`}}",
"vm_size": "Standard_D2_v2",
"async_resourcegroup_delete": true
}
],
"provisioners": [
{
"type": "windows-restart",
"restart_timeout": "15m",
"max_retries": 3
},
{
"type": "powershell",
"inline": [
"$ErrorActionPreference='Stop'",
"Write-Host \"[UPDATES]:: Install updates - PASS 1!\"",
"Write-Host \"Installing Required Powershell Modules\"",
"Get-PackageProvider -name nuget -force",
"Install-Module -Name PSWindowsUpdate -Force -Confirm:$false",
"$Updates = Get-WindowsUpdate",
"Write-Host \"[UPDATES]:: Found $($Updates.count) updates to install - PASS 1!\"",
"if( $Updates.count -gt 0 ){ Install-WindowsUpdate -AcceptAll -Install -AutoReboot }"
],
"elevated_user": "{{user `admin_user`}}",
"elevated_password": "{{.WinRMPassword}}"
},
{
"type": "powershell",
"pause_before": "3m",
"inline": [
"$ErrorActionPreference='Stop'",
"Write-Host \"[UPDATES]:: Install updates - PASS 2!\"",
"$Updates = Get-WindowsUpdate",
"Write-Host \"[UPDATES]:: Found $($Updates.count) updates to install - PASS 2!\"",
"if( $Updates.count -gt 0 ){ Install-WindowsUpdate -AcceptAll -Install -AutoReboot }"
],
"elevated_user": "{{user `admin_user`}}",
"elevated_password": "{{.WinRMPassword}}"
},
{
"type": "windows-restart",
"restart_timeout": "15m",
"max_retries": 3
},
{
"type": "powershell",
"inline": [
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
]
}
]
}

In my above packer json template, I use 3 passes to install the windows update and restart the image vm automatically. And generalize the VM template before it saves in the azure compute gallery.

Once the packer template is completed, I have another task to deploy the new set of AVD hosts to the same pool. And enable old ones. When deploying all I do is point the VM image location to be the compute gallery


Conclusion

There are a few key points to this solution.


  1. Tagging the VM name with Image version details (which helps to identify and keep track of serves and also have old and new VM in the same host pool in case if we want to roll back after an update)


  2. Running the Windows update function 3 times in packer image will slim the chance of missing out on VM updates

  3. Using packer version 1.8.2 or above

I'm sure there are other ways to perform this. But for me this pipeline fits the solution. For this pipeline to complete it will take around 45min but, all we got to do is run the pipeline. Again make lives easy and make things efficient.

Hope this helps someone

As always comment if you have any questions or reach out to me. Happy to help any way I can


Until next time...........


Comments

  1. […] https://hungryboysl.wordpress.com/2022/10/10/azure-virtual-desktops-gold-image-windows-update-automa… […]

    ReplyDelete

Post a Comment

Popular posts from this blog

Updating Azure VM Data Disk Sizes

Summary Hope you are doing great, this time I came up with a simple azure DevOps solution for updating VM disk sizes. The current project that I'm in has a bunch of virtual machines. So, we need a way to update the VM disk with minimal administrative effort and changes.  In this scenario, we have used Bicep as the IAC language Azure DevOps pipelines YAML variable files  Here is the high-level workflow for a particular VM in the solution YAML Pipeline file got two workflows, firstly the VM build pipeline, and the second is the disk update one. If you focus on the green arrow and the purple arrow, basically I'm modifying the same bicep module file and passing the same set of variables. You may wonder why we cannot use the same flow to build the VM and update the disk later, that's because for the disk updates VM needs to be in a shutdown state, and other components in the 1st flow need the VM up and running especially the extension modules YAML Variable Files I have decided t

Deploying an Automation Account with a Runbook and Schedule Using Bicep

Introduction Automation is a key component in many organizations' cloud strategy. Azure Automation allows you to automate the creation, deployment, and management of resources in your Azure environment. In this post, we will walk through the process of deploying an Automation Account with a Runbook and Schedule using Bicep, a new domain-specific language for deploying Azure resources. Intention My intention at the  end is to run a PowerShell  script to start and shutdown Azure VMs based on tag values. PowerShell  script that I have used is from below l ink.  And two  of me   collogue s ( Michael Turnley   and Saudh Mohomad helped to modify the  PowerShell  script. Prerequisites Before we begin, you will need the following: An Azure subscription The Azure CLI installed on your machine. The Azure Bicep extension for the Azure CLI Creating the Automation Account The first step in deploying an Automation Account with a Runbook and Schedule is to create the Aut

Securing Azure Services with Fetian FIDO

Hey Folks  Here again with another security topic with Fetian Fido. And once again Fetian devices proved their excellent quality and stability. For this I choose Fetian K33 -  AllinPass FIDO Security Key – FEITIAN (ftsafe.com) and  K39 -  Single-button FIDO Security Keys | FEITIAN (ftsafe.com) Use case  In an organization following changes needs to be implemented.  1. Update the password policy 2. Update the user session time out to 30 minutes Once these changes being implemented, the following issues need to be addressed 1. Users' complaint new passwords need to be so long 2. Users complain sessions time out makes them work so much slower with the longer passwords 3. Etc... Solution  One of my friends reached out to me to help solve this problem. All I could think of was using passwordless auth with FIDO devices. We have decided to use Fido2 keys for better security and flexibility for the users. The FIDO (Fast IDentity Online) Alliance helps to promote open authentication stand