Windows update powershell script

Jan Vávra 446 Reputation points
2025-11-07T13:34:31.8733333+00:00

I wanna write following orchestration that will be run regulary:

  1. Remove VM from load balancer
  2. Test if there are some active connections to a service on a VM.
  3. Invoke a powershell script that will install windows updates and do a OS reboot.
  4. Check status page of service.
  5. Return a VM to a pool.

I have problem with the pwsh script on Windows Server 2022. It declares that updates were installed but doesn't make restart and the list of updates from GUI / Settings / Updates is not empty.

The core calls

Get-WindowsUpdate -MicrosoftUpdate -AcceptAll |
    Install-WindowsUpdate -AcceptAll -AutoReboot -Verbose 4           
            

Should I use powershell 7?
The default is
PSVersion 5.1.20348.2849

Isn't there a standard solution?

The full script is


param(
    [string]$Command
)

# --- Global paths & logging ---
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ScriptPath = $MyInvocation.MyCommand.Path
Write-Host $ScriptPath

$LogFile   = Join-Path $ScriptDir ("AutoUpdate_" + (Get-Date -Format "yyyy-MM") + ".log")
# Ensure log directory exists
New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null


function Write-Log {
    param([Parameter(Mandatory)][string]$Message)
    $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $line = "[$ts] $Message"
    Write-Host $line
    Add-Content -Path $LogFile -Value $line
}

function Ensure-PSWindowsUpdate {
    Write-Log "Checking PSWindowsUpdate module..."
    try {
        if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) {
            Write-Log "Installing NuGet provider and PSWindowsUpdate..."
            Install-PackageProvider -Name NuGet -Force -ErrorAction Stop
            # Try global (preferred when running as admin/SYSTEM)
            try {
                Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -ErrorAction Stop
            } catch {
                # Fallback to CurrentUser scope (may work for interactive runs)
                Install-Module -Name PSWindowsUpdate -Force -Scope CurrentUser -ErrorAction Stop
            }
        }
        Import-Module PSWindowsUpdate -ErrorAction Stop
        Write-Log "PSWindowsUpdate is available."
        return $true
    } catch {
        Write-Log "ERROR ensuring PSWindowsUpdate: $($_.Exception.Message)"
        return $false
    }
}

function Invoke-AutoUpdate {
    <#
      .SYNOPSIS
        Searches, downloads and installs updates; reboots automatically if needed.
      .NOTES
        Run elevated. Best run as SYSTEM for reliability on servers.
    #>
    Write-Log "===== Starting Windows Update Process ====="
    try {
        if (-not (Ensure-PSWindowsUpdate)) {
            throw "PSWindowsUpdate module unavailable."
        }

        Write-Log "Ensuring Windows Update service is running..."
        Set-Service wuauserv -StartupType Manual
        Start-Service wuauserv

        Write-Log "Searching for and installing updates..."
        # -AutoReboot triggers reboot when required after install        
        # Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot -Install -Verbose 4>&1 |
        Get-WindowsUpdate -MicrosoftUpdate -AcceptAll |
            Install-WindowsUpdate -AcceptAll -AutoReboot -Verbose 4>&1 |
            ForEach-Object {
                if ($_ -is [System.Management.Automation.VerboseRecord]) {
                    Write-Log $_.Message
                }
            elseif ($_ -is [string]) {
                Write-Log $_
            }
        }


        Write-Log "Update workflow finished (system may reboot if required)."
    } catch {
        Write-Log "ERROR during update: $($_.Exception.Message)"
        throw
    } finally {
        Write-Log "===== Windows Update Script Completed ====="
    }
}

function Register-AutoUpdateTask {
    <#
      .SYNOPSIS
        Registers a weekly scheduled task to run this script.
      .PARAMETER RunAs
        'System' (default), 'NetworkService', or 'LocalService'
      .PARAMETER DayOfWeek
        Day to run (default: Monday)
      .PARAMETER Time
        Time to run in HH:mm (24h) (default: 04:00)
      .PARAMETER TaskName
        Name of the scheduled task
    #>
    param(
        [ValidateSet('System','NetworkService','LocalService')]
        [string]$RunAs = 'System',
        [ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')]
        [string]$DayOfWeek = 'Monday',
        [ValidatePattern('^[0-2]\d:[0-5]\d$')]
        [string]$Time = '04:00',
        [string]$TaskName = 'Software602\WinUpdate'
    )

    # Map to principal identities
    switch ($RunAs) {
        'System'         { $userId = 'SYSTEM' }
        'NetworkService' { $userId = 'NT AUTHORITY\NetworkService' }
        'LocalService'   { $userId = 'NT AUTHORITY\LocalService' }
    }


    try {            
    
        # Build action to run this script
        $escaped = $ScriptPath.Replace('"','`"')
        $action = New-ScheduledTaskAction -Execute 'powershell.exe' `
            -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$escaped`" -Command Invoke-AutoUpdate"

        # Trigger
        $hour, $min = $Time.Split(':')
        $at = Get-Date -Hour $hour -Minute $min -Second 0
        $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $DayOfWeek -At $at

        # Principal
        $principal = New-ScheduledTaskPrincipal -UserId $userId -RunLevel Highest -LogonType ServiceAccount

        # Settings (retry if missed, stop on battery constraints off)
        $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries `
            -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew

        # Register
        Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
            -Principal $principal -Settings $settings -Description "Installs Windows Updates and reboots if needed." -Force

        $msg = "Scheduled task '$TaskName' registered to run as '$RunAs' every $DayOfWeek at $Time."
        Write-Log $msg
        Write-Host $msg

    }
    catch {
        $err = $_.Exception.Message
        $msg = "Failed to create scheduled task action: $err"
        Write-Log $msg
        Write-Warning $msg
    }    
}

switch ($Command) {
    'Invoke-AutoUpdate' { Invoke-AutoUpdate }
    'Register-AutoUpdate' { Register-AutoUpdateTask -RunAs System }
    default { Write-Host "No valid -Command specified. Options: Invoke-AutoUpdate, Register-AutoUpdate" }
}


Azure Virtual Machines
Azure Virtual Machines
An Azure service that is used to provision Windows and Linux virtual machines.
{count} votes

2 answers

Sort by: Most helpful
  1. Q&A Assist
    2025-11-07T13:34:57.8033333+00:00
    AI generated content. This question contains an answer created with AI from Q&A Assist. Learn more

    It appears that you are facing issues with your PowerShell script for installing Windows updates on Windows Server 2022, specifically regarding the automatic reboot not occurring after updates are installed. Here are some considerations and suggestions:

    1. PowerShell Version: You are currently using PowerShell 5.1. While PowerShell 7 offers more features and improvements, it is not strictly necessary for running Windows Update commands. However, ensure that the commands you are using are compatible with the version you are running.
    2. Update Installation: The command Get-WindowsUpdate is typically used to check for available updates. To install updates and handle reboots, you might want to use Install-WindowsUpdate from the PSWindowsUpdate module. Make sure this module is installed and imported in your script.
    3. Reboot Handling: If updates require a reboot, you can force a reboot after installation by using the -Force parameter in the Install-WindowsUpdate command. Additionally, you can check the -AcceptAll parameter to automatically accept all updates.
    4. Logging and Error Handling: Ensure that your script includes proper logging and error handling to capture any issues that may arise during the update process. This can help identify why the reboot is not occurring.
    5. Scheduled Task: If you are running this script as a scheduled task, ensure it is set to run with elevated privileges. Running as the SYSTEM account can sometimes resolve permission-related issues with updates.
    6. Manual Check: After running the script, manually check the Windows Update settings to see if updates are still pending. This can help confirm whether the updates were applied successfully.

    Here’s a simplified example of how you might structure your update function:

    function Update-Windows {
        Import-Module PSWindowsUpdate
        $updates = Get-WindowsUpdate
        if ($updates) {
            Install-WindowsUpdate -AcceptAll -AutoReboot -Force
        } else {
            Write-Host "No updates available."
        }
    }
    

    Make sure to test the script in a safe environment before deploying it in production.


    References:

    0 comments No comments

  2. Jan Vávra 446 Reputation points
    2025-11-21T09:17:08.8966667+00:00

    I took another versions of Windows, eg. Windows Server 2022 from Sep 2025 Datatacenter.
    The Windows update page declares there is a pending install even I ran my script.

    obrazek

    If I run

    Get-WUHistory | Select Date, Title, Result
    

    I can see that pending install "B2267602 (Version 1.441.378.0)" was installed.
    And also I cannot see anything at View update history page.obrazek

    I think that Install-WindowsUpdate doesn't do all the work ...

    $updates = Get-WindowsUpdate -MicrosoftUpdate -AcceptAll -ErrorAction SilentlyContinue
    $updates | Install-WindowsUpdate -AcceptAll -AutoReboot -Verbose 4>&1


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.