Azure Virtual Desktops Gold Image Windows Update Automation
Howdy Folks
During a recent customer engagement, I had to deploy Azure Virtual Desktop solutios 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 and saved gold image files for the diffrent hostpools
This lead me thinking of a way to update these gold images without spending much time and deploy 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 usaul 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.”
I’m combining packer templates with bicep to complete the tasks that I need
Explanation
Azure Pipeline
Stage 1 - In stage 1 Im 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 Im deploying virtual machines, I tag the VM name with image version as an example
Tagging the VM name with image version is helpfull identify new and old vms when it comes to when we put them on to drain mode etc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- task: AzurePowerShell@5
displayName: 'Generate Image Version Details'
inputs:
azureSubscription: $
ScriptType: 'inlineScript'
Inline: |
Select-AzSubscription -SubscriptionId $
$source_imageVerions = Get-AzGalleryImageVersion -ResourceGroupName $(extaccimageGRgName) -GalleryName $ -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
Create tempory resource group
Permission delegations to perform the changes
Policy exclusions
Create a packer image and save in image galary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- task: AzurePowerShell@5
displayName: Create Temporary Resource Group
inputs:
azureSubscription: $
ScriptType: InlineScript
Inline: |
Select-AzSubscription -SubscriptionId $
$tempname = 'rg-pkr-'+(new-guid).ToString().Substring(0,10)
New-AzResourceGroup -Name $tempname -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: $
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: $
ScriptType: InlineScript
Inline: |
Set-AzContext "$"
$ResourceGroup = Get-AzResourceGroup -Name "$(TempResourceGroup)"
$policies = "$" -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 tempalate has 3 sections
Variables - variable required for packer image build.
Builders - Configuration settings for the builder image
Provisioners - Tasks to perform ontop of the build vm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
{
"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 save in the azure compute gallary.
Once the packer template completed, I have another tasks to deploy the new set of AVD hosts to the same pool. And enable old ones. When deploying the all I do is pointing the VM image location to be the compute gallery
Conclusion
There are few key points to this solution.
Tagging the VM name with Image version details (which helps to identify and keep track of serves and also have old and new VM is the same host pool incase if we want to roll back after an update)
Running Windows update functio 3 time in packer image, which will slim the chance of missing out VM updates
Using packer version 1.8.2 or above
I’m sure there are other ways to perform this. But for me this pipeline fit the solution. For this pipeline to complete it will take around 45min but, all we got to do is run the pipeline. Again make the lives easy and make things efficient.
Hope this helps someone
As alwasy comment if you have any questions or reach out to me. Happy to help anyway I can
Untill next time………..