Automate Windows Server Patching with PowerShell and Azure Update Manager
Executive Snapshot
| Category | Recommendation |
|---|---|
| Orchestration Layer | Azure Update Manager (AUM) for fleet-wide scheduling |
| Local Automation | PowerShell + PSWindowsUpdate module for on-demand runs |
| Compliance Reporting | Azure Resource Graph queries + Log Analytics Workspace |
| Pre/Post Scripts | PowerShell runbooks in Azure Automation Account |
| Rollback Strategy | Snapshot VMs before patch window; use maintenance configuration dry-runs |
| Minimum OS Support | Windows Server 2012 R2+ (Azure Arc-enabled for hybrid) |
| Typical Patch Cycle | Monthly (Patch Tuesday + 5–7 business days for staging) |
TL;DR
flowchart TD
A[Define machine scope & schedule] --> B[Assess missing updates]
B --> C{Updates available?}
C -->|No| D[Mark compliant - report]
C -->|Yes| E[Wait for maintenance window]
E --> F[Install approved patches]
F --> G{Reboot required?}
G -->|Yes| H[Reboot in window]
G -->|No| I[Skip reboot]
H --> J[Post-patch validation]
I --> J
J --> K[Generate compliance report]
D --> K
classDef ok fill:#1f6f43,stroke:#3ddc84,color:#fff
class D,K ok
- Azure Update Manager replaces the deprecated Log Analytics-based Update Management solution and works natively with Azure VMs, Azure Arc-enabled servers, and hybrid machines.
- PSWindowsUpdate is the Swiss Army knife for ad-hoc and scripted patching on individual Windows Servers — install it once, use it everywhere.
- Maintenance Configurations let you declaratively schedule patch windows per environment (Dev → Staging → Prod) with built-in gating.
- Pre- and post-scripts via Azure Automation Runbooks handle service quiescing, snapshot creation, and health checks automatically.
- Resource Graph queries give you instant fleet-wide compliance visibility without touching each server individually.
Introduction
Manual patching is a liability. A sysadmin copy-pasting wuauclt /detectnow across forty servers at 11 PM on a Thursday is not a patching strategy — it's a time bomb. Missed patches are the single most common vector in ransomware and supply-chain attacks, yet many organisations still treat patch management as a heroic, human-effort task.
The good news: Microsoft's ecosystem now gives you everything you need to fully automate Windows Server patching from assessment through deployment to compliance reporting. Azure Update Manager (GA since late 2023) handles the orchestration layer for cloud and hybrid fleets, while PowerShell fills every gap in between — pre-checks, custom scheduling, local overrides, and reporting.
This tutorial walks through a complete, production-grade automation pipeline: assessing patch status across your fleet, scheduling zero-touch deployments with Maintenance Configurations, wiring in pre- and post-scripts, and proving compliance to your security team — all without a change-management war room.
By the end, you'll have a repeatable, auditable system that cuts your monthly patch cycle from days to hours.
Prerequisites
Before you start, make sure you have the following in place:
- Azure subscription with at least Contributor access on the target resource group
- Azure Update Manager enabled (no extra cost for Azure VMs; Azure Arc license required for on-premises machines)
- Azure Automation Account (for runbooks used as pre/post scripts)
- Log Analytics Workspace connected to Azure Monitor (for compliance data)
- Windows Server 2012 R2 or later on target machines (2016/2019/2022 recommended)
- PowerShell 5.1+ on all target servers; PowerShell 7.x for the management workstation
- NuGet provider available (for PSWindowsUpdate installation)
- Azure Arc agent installed on any on-premises or multi-cloud servers you want to manage
- Az PowerShell module (
Install-Module Az -Scope CurrentUser) on your management workstation - Basic familiarity with Azure Resource Manager concepts and PowerShell remoting
1. Install and Validate the PSWindowsUpdate Module
PSWindowsUpdate is the de-facto PowerShell module for Windows Update automation. It wraps the Windows Update Agent COM API cleanly, supports remote execution, and integrates with WSUS.
# Run on each target server OR push remotely via Invoke-Command
# Requires PowerShell 5.1+ and internet access to PSGallery
# Install NuGet provider silently (required on Server Core)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers
# Install PSWindowsUpdate for all users
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber
# Verify installation
Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object Name, Version
# Check pending updates on the local server
Get-WindowsUpdate -MicrosoftUpdate -Verbose
To push this to a fleet of servers in one shot, use PowerShell remoting:
# Bulk-deploy PSWindowsUpdate to a list of servers
$servers = @('SRV-WEB-01','SRV-WEB-02','SRV-APP-01','SRV-DB-01')
$installBlock = {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber
}
Invoke-Command -ComputerName $servers -ScriptBlock $installBlock -Credential (Get-Credential)
Tip: In air-gapped environments, download the module on an internet-connected machine with
Save-Module -Name PSWindowsUpdate -Path C:\Offline\Modules, then copy and install offline.
2. Assess Patch Compliance Across Your Fleet
Before automating deployments, you need a clear picture of what's missing. This script generates a fleet-wide patch compliance report and exports it to CSV.
# Fleet-wide patch assessment — outputs a structured compliance report
# Run from a jump host with WinRM access to all target servers
param(
[string[]]$ServerList = @('SRV-WEB-01','SRV-WEB-02','SRV-APP-01','SRV-DB-01'),
[string]$ReportPath = "C:\Reports\PatchCompliance_$(Get-Date -Format 'yyyyMMdd').csv",
[PSCredential]$Cred = (Get-Credential -Message 'Enter admin credentials')
)
$assessBlock = {
$updates = Get-WindowsUpdate -MicrosoftUpdate -NotInstalled -Hide:$false 2>$null
[PSCustomObject]@{
ServerName = $env:COMPUTERNAME
OSVersion = (Get-CimInstance Win32_OperatingSystem).Caption
LastBootTime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
PendingUpdates = $updates.Count
CriticalUpdates = ($updates | Where-Object { $_.MsrcSeverity -eq 'Critical' }).Count
SecurityUpdates = ($updates | Where-Object { $_.Categories -match 'Security' }).Count
AssessmentTime = Get-Date
}
}
$results = Invoke-Command -ComputerName $ServerList -ScriptBlock $assessBlock -Credential $Cred
$results | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8
Write-Host "Report saved to $ReportPath" -ForegroundColor Green
$results | Format-Table -AutoSize
3. Understand the Azure Update Manager Architecture
Before configuring AUM, it helps to see how all the components relate to each other.
flowchart TD
A[Management Workstation\nAz PowerShell / Azure Portal] -->|Defines schedule| B[Maintenance Configuration]
B --> C{Scope Assignment}
C -->|Azure VMs| D[Azure Virtual Machines]
C -->|Hybrid / On-Prem| E[Azure Arc-Enabled Servers]
D & E --> F[Azure Update Manager\nOrchestration Engine]
F -->|Triggers| G[Windows Update Agent\non each server]
G -->|Applies patches| H[Server Reboot / Health Check]
H --> I[Azure Monitor\nLog Analytics Workspace]
I --> J[Compliance Dashboard\n& Alerts]
K[Azure Automation Account] -->|Pre-script: snapshot + quiesce| F
K -->|Post-script: health check + notify| H
Key insight: Azure Update Manager communicates with the Windows Update Agent directly — there is no Log Analytics agent or MMA dependency for patch deployment (only for reporting). This makes it significantly lighter than the legacy Update Management solution.
4. Configure Azure Update Manager via PowerShell
You can configure everything through the Azure portal, but scripting it means you can version-control your patching infrastructure and replicate it across subscriptions.
# Authenticate and set context
Connect-AzAccount
Set-AzContext -SubscriptionId '<your-subscription-id>'
# ── Step 1: Enable periodic assessment on all Windows VMs in a resource group ──
$resourceGroup = 'rg-prod-servers'
$vms = Get-AzVM -ResourceGroupName $resourceGroup |
Where-Object { $_.StorageProfile.OsDisk.OsType -eq 'Windows' }
foreach ($vm in $vms) {
$patchSettings = @{
AssessmentMode = 'AutomaticByPlatform' # Triggers assessment every 24 hrs
PatchMode = 'AutomaticByPlatform' # AUM controls the patch schedule
}
$vm.OSProfile.WindowsConfiguration.PatchSettings.AssessmentMode = $patchSettings.AssessmentMode
$vm.OSProfile.WindowsConfiguration.PatchSettings.PatchMode = $patchSettings.PatchMode
Update-AzVM -ResourceGroupName $resourceGroup -VM $vm
Write-Host "Enabled AUM on $($vm.Name)" -ForegroundColor Cyan
}
# ── Step 2: Create a Maintenance Configuration (patch schedule) ──
$maintenanceParams = @{
ResourceGroupName = $resourceGroup
Name = 'mc-prod-monthly-patching'
Location = 'eastus'
MaintenanceScope = 'InGuestPatch'
StartDateTime = '2026-07-08 02:00' # First Patch Tuesday + 7 days
ExpirationDateTime = '9999-12-31 00:00'
TimeZone = 'Eastern Standard Time'
RecurEvery = '1Month Third Tuesday' # Recurring monthly
Duration = '03:00' # 3-hour maintenance window
RebootSetting = 'IfRequired'
WindowsParameterClassificationToInclude = @('Critical','Security','UpdateRollup')
}
New-AzMaintenanceConfiguration @maintenanceParams
Write-Host "Maintenance Configuration created." -ForegroundColor Green
# ── Step 3: Assign the Maintenance Configuration to VMs ──
$maintenanceConfig = Get-AzMaintenanceConfiguration `
-ResourceGroupName $resourceGroup `
-Name 'mc-prod-monthly-patching'
foreach ($vm in $vms) {
New-AzConfigurationAssignment `
-ResourceGroupName $resourceGroup `
-ResourceType 'VirtualMachines' `
-ResourceName $vm.Name `
-ProviderName 'Microsoft.Compute' `
-ConfigurationAssignmentName "$($vm.Name)-patch-assignment" `
-MaintenanceConfigurationId $maintenanceConfig.Id `
-Location $vm.Location
Write-Host "Assigned patch schedule to $($vm.Name)" -ForegroundColor Cyan
}
5. Add Pre- and Post-Patch Scripts via Azure Automation
This is where patching automation becomes production-grade. The pre-script snapshots VMs and drains application load balancers; the post-script validates services and sends a Teams notification.
Pre-Patch Runbook: Pre-PatchWindow.ps1
# Azure Automation Runbook — runs BEFORE the patch window
# Assign to the Maintenance Configuration as a pre-task
param(
[Parameter(Mandatory)][string]$ResourceGroupName,
[Parameter(Mandatory)][string]$VMName
)
# Use System-Assigned Managed Identity (no stored credentials)
Connect-AzAccount -Identity
# 1. Create a disk snapshot for rollback
$vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName
$osDiskName = $vm.StorageProfile.OsDisk.Name
$osDisk = Get-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $osDiskName
$snapshotConfig = New-AzSnapshotConfig `
-SourceUri $osDisk.Id `
-Location $vm.Location `
-CreateOption Copy
$snapshotName = "$VMName-pre-patch-$(Get-Date -Format 'yyyyMMddHHmm')"
New-AzSnapshot -ResourceGroupName $ResourceGroupName `
-SnapshotName $snapshotName `
-Snapshot $snapshotConfig
Write-Output "Snapshot $snapshotName created for $VMName"
# 2. Write pre-patch status to Log Analytics (custom table)
$body = @{
VMName = $VMName
Event = 'PrePatchSnapshot'
SnapshotName = $snapshotName
Timestamp = (Get-Date -Format 'o')
} | ConvertTo-Json
# POST to Log Analytics Data Collector API (replace workspace IDs)
$workspaceId = '<log-analytics-workspace-id>'
$sharedKey = '<log-analytics-primary-key>' # Store in Key Vault in production
$logType = 'PatchEvents'
# [Signature logic omitted for brevity — see MS Docs: Send data to Log Analytics]
Write-Output "Pre-patch tasks complete for $VMName"
Post-Patch Runbook: Post-PatchWindow.ps1
# Azure Automation Runbook — runs AFTER the patch window
# Validates services and sends a Teams webhook notification
param(
[Parameter(Mandatory)][string]$ResourceGroupName,
[Parameter(Mandatory)][string]$VMName,
[string]$TeamsWebhookUrl = ''
)
Connect-AzAccount -Identity
# 1. Check VM power state
$vmStatus = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -Status
$powerState = ($vmStatus.Statuses | Where-Object { $_.Code -match 'PowerState' }).DisplayStatus
Write-Output "$VMName power state: $powerState"
# 2. Run a connectivity test via Run Command
$scriptContent = @'
$services = @('wuauserv','EventLog','Winmgmt')
$results = foreach ($svc in $services) {
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
[PSCustomObject]@{ Service = $svc; Status = if ($s) { $s.Status } else { 'NotFound' } }
}
$results | ConvertTo-Json
'@
$runResult = Invoke-AzVMRunCommand `
-ResourceGroupName $ResourceGroupName `
-VMName $VMName `
-CommandId 'RunPowerShellScript' `
-ScriptString $scriptContent
$serviceStatus = $runResult.Value[0].Message | ConvertFrom-Json
Write-Output ($serviceStatus | Format-Table | Out-String)
# 3. Send Teams notification
if ($TeamsWebhookUrl) {
$allHealthy = ($serviceStatus | Where-Object { $_.Status -ne 'Running' }).Count -eq 0
$colour = if ($allHealthy) { '00b050' } else { 'FF0000' }
$summary = if ($allHealthy) { "✅ $VMName patched successfully" } else { "⚠️ $VMName — service check failed" }
$teamsPayload = @{
type = 'message'
attachments = @(@{
contentType = 'application/vnd.microsoft.card.adaptive'
content = @{
'$schema' = 'http://adaptivecards.io/schemas/adaptive-card.json'
type = 'AdaptiveCard'
version = '1.4'
body = @(@{ type = 'TextBlock'; text = $summary; color = $colour })
}
})
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body $teamsPayload -ContentType 'application/json'
}
6. Query Compliance Status with Azure Resource Graph
Once AUM has run, Resource Graph lets you query patch compliance across your entire subscription in seconds — far faster than iterating over each VM.
# Requires Az.ResourceGraph module
Install-Module -Name Az.ResourceGraph -Force -Scope CurrentUser
# Query: list all Windows VMs with their patch compliance summary
$query = @'
patchassessmentresources
| where type == "microsoft.compute/virtualmachines/patchassessmentresults"
| where properties.osType == "Windows"
| extend
vmName = tostring(split(id, "/")[8]),
criticalMissing = toint(properties.availablePatchCountByClassification.critical),
securityMissing = toint(properties.availablePatchCountByClassification.security),
lastAssessment = tostring(properties.lastModifiedDateTime),
complianceState = tostring(properties.status)
| project vmName, criticalMissing, securityMissing, lastAssessment, complianceState
| order by criticalMissing desc
'@
$results = Search-AzGraph -Query $query
$results | Format-Table -AutoSize
# Export to CSV for security team hand-off
$results | Export-Csv -Path "C:\Reports\AUM_Compliance_$(Get-Date -Format 'yyyyMMdd').csv" `
-NoTypeInformation -Encoding UTF8
7. Patch Workflow — End-to-End Flow
sequenceDiagram
participant MC as Maintenance Configuration
participant AA as Azure Automation Account
participant AUM as Azure Update Manager
participant WUA as Windows Update Agent (VM)
participant LAW as Log Analytics Workspace
MC ->> AA : Trigger pre-script runbook (T-30 min)
AA ->> WUA : Create OS disk snapshot
AA ->> LAW : Log pre-patch event
MC ->> AUM : Patch window opens (T=0)
AUM ->> WUA : Enumerate & download approved updates
WUA ->> WUA : Install updates + reboot if required
WUA ->> AUM : Report install results
AUM ->> LAW : Write patch compliance data
MC ->> AA : Trigger post-script runbook (T+window)
AA ->> WUA : Run health check via Run Command
AA ->> LAW : Log post-patch event
AA ->> MC : Send Teams notification (pass/fail)
8. Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| VMs not assessed after enabling AUM | PatchMode still set to AutomaticByOS |
Re-run the VM update script in Step 4; verify with Get-AzVM |
| Maintenance Configuration shows "Cancelled" | Patch window too short for update count | Increase Duration to at least 4 hours for large fleets |
| PSWindowsUpdate returns no updates | WSUS override pointing to internal server | Run Get-WUServiceManager; switch source to Microsoft Update |
| Pre-script runbook fails with auth error | Managed Identity not granted Contributor | Assign Contributor + Disk Snapshot Contributor roles to the Automation Account's MSI |
| Resource Graph query returns empty results | Assessment not yet completed | Wait 24 hours after enabling AutomaticByPlatform; trigger manual assessment via portal |
| Servers reboot outside the window | RebootSetting set to Always |
Change to IfRequired; servers only reboot when updates mandate it |
| Arc-enabled servers not targeted | Arc agent version outdated | Update agent: azcmagent upgrade on each machine |
| Teams notification not sending | Webhook URL expired or incorrect | Regenerate the incoming webhook in Teams and update the Automation variable |
Common Mistakes and Pitfalls to Avoid
1. Mixing patch modes on the same VM
Setting both AutomaticByPlatform (AUM) and a scheduled task that runs Install-WindowsUpdate locally creates race conditions. Pick one orchestration layer and disable the other.
2. Assigning the same VM to multiple Maintenance Configurations
Azure allows this, but updates may be applied twice within the same month, causing unexpected reboots. Audit assignments regularly with Get-AzConfigurationAssignment.
3. Skipping the staging ring
Going straight from assessment to production patching is how you break things at 2 AM. Always run: Dev → Staging (5-day soak) → Prod. Use separate Maintenance Configurations with staggered StartDateTime values.
4. Storing credentials in runbooks
Never hardcode passwords or keys in Automation runbooks. Use Managed Identity for Azure resources and Azure Key Vault references for any secrets that must exist.
5. Forgetting driver and firmware updates
AUM's WindowsParameterClassificationToInclude doesn't include driver updates by default. For bare-metal or critical workloads, supplement with OEM-specific tooling (Dell OMSA, HP iLO Amplifier).
6. No rollback plan
Snapshots are only useful if you've tested the restore process. Document and fire-drill your rollback procedure before you need it at 3 AM.
7. Ignoring the "assessment vs. deployment" distinction
AUM assessment tells you what would be applied. Don't mistake a clean assessment report for a patched server — always check deployment results separately.
Key Takeaways
- Azure Update Manager is the modern, agent-light replacement for Log Analytics-based Update Management — migrate if you haven't already.
- PSWindowsUpdate fills gaps that AUM doesn't cover: ad-hoc patches, local overrides, and detailed per-update scripting.
- Maintenance Configurations give you declarative, version-controllable patch schedules that can be assigned to individual VMs or dynamic scopes.
- Pre/post runbooks with Managed Identity eliminate credential risk while enabling snapshot-based rollback and automated health validation.
- Resource Graph queries scale instantly to thousands of VMs and give security teams the compliance proof they need without manual reporting overhead.
- Patch rings (Dev → Staging → Prod) are non-negotiable for production environments — automate the promotion gates, don't just compress the timeline.
- Audit your
ConfigurationAssignmentregularly to catch orphaned schedules, duplicate assignments, and servers that have drifted out of scope.
Next Steps
- Enable Azure Policy to automatically enrol new VMs into the correct Maintenance Configuration at provisioning time — no manual assignment needed.
- Build a Power BI dashboard connected to your Log Analytics Workspace to give leadership a real-time patch compliance view by environment, criticality, and SLA.
- Integrate AUM with ServiceNow or Jira via Azure Logic Apps to auto-create and auto-close change tickets when patch windows open and close.
- Set up Azure Monitor Alerts to page on-call if more than 5% of VMs fail post-patch health checks — catch silent failures before users notice.
- Test your snapshot restores on a non-production VM today. The restore runbook should be as automated as the patching runbook itself.
Related Articles
- [Zero-Downtime Blue/Green Deployments with Azure Load Balancer and PowerShell] —
/automation/blue-green-azure-load-balancer/ - [Azure Arc Deep Dive: Manage On-Premises Servers Like Cloud Resources] —
/cloud/azure-arc-on-premises-management/ - [PowerShell Desired State Configuration (DSC) for Windows Server Hardening] —
/security/powershell-dsc-server-hardening/
Found an error or have an improvement? Open an issue or pull request in the IT Pro Insights GitHub repo. All code examples in this article are tested against Azure Update Manager API version 2023-09-01 and PSWindowsUpdate 2.2.1.4.
Leave a Reply