8/7/2020: there’s a more updated version available here.

# Update-Remote-Windows-2016-Servers.ps1

$servers="SERVER1","SERVER2"

function applyWindowsUpdates{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=1)]
[string[]]$computer
)

#starts up a remote powershell session to the computer
do{
$session = New-PSSession -ComputerName $computer
"connecting remotely to $computer"
sleep -seconds 10
} until ($session.state -match "Opened")

function checkPSVersion{
$version=$PSVersionTable.PSVersion.Major;
if ($version -ge 5){return $True;} else {return $False;}
}

function installPrerequisites{
# Set PowerShell Gallery as Trusted to bypass prompts
If(!("NuGet" -in (get-packageprovider).Name)){Install-PackageProvider -Name Nuget -Force -Confirm:$False -InformationAction SilentlyContinue}
#If(!("PSGallery" -in (get-psrepository).Name)){Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -InformationAction SilentlyContinue -WarningAction SilentlyContinue}

# Include the PowerShell Windows Update module
$checkModule=Get-Module -ListAvailable -Name PSWindowsUpdate
if(!($checkModule)){
Install-Module PSWindowsUpdate -Force -Confirm:$false;
Import-Module PSWindowsUpdate -Force;
}

# Register the user of Windows Update Service if it has not been registered
$MicrosoftUpdateID="7971f918-a847-4430-9279-4a52d1efe18d"
$registered=(Get-WUServiceManager).ServiceID | Where-Object {$MicrosoftUpdateID -contains $_}
if (!($registered)){
Add-WUServiceManager -ServiceID $MicrosoftUpdateID -Confirm:$false
}
}

function triggerUpdateJob{
# Only update Microsoft products with no reboots
$Script = {Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot}
Invoke-WUjob -ComputerName $computer -Script $Script -Confirm:$false -RunNow
}

function triggerWindowsUpdate{
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot;
#Get-WUInstall -IgnoreUserInput -Acceptall -Download -Install -Verbose;
#$Script = {import-module PSWindowsUpdate; Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log}
#Invoke-WUjob -ComputerName $_ -Script $Script -Confirm:$false -RunNow
}

function listPendingUpdates{
[DateTime]$last24Hours=(Get-Date).AddHours(-24)
$Computername = $env:COMPUTERNAME
$updatesession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session",$Computername))
$UpdateSearcher = $updatesession.CreateUpdateSearcher()
$searchresult = $updatesearcher.Search("IsInstalled=0") # 0 = NotInstalled | 1 = Installed
$searchresult.Updates.Count

$updates = If ($searchresult.Updates.Count -gt 0) {

#Updates are waiting to be installed
$count = $searchresult.Updates.Count

#Cache the count to make the For loop run faster
For ($i=0; $i -lt $Count; $i++) {

$item = $searchresult.Updates.Item($i)
[DateTime]$timeStamp=$item.LastDeploymentChangeTime

if ($timeStamp -ge $last24Hours){
#Create new custom object
[pscustomobject]@{
Title = $item.Title
KB = $($item.KBArticleIDs)
SecurityBulletin = $($item.SecurityBulletinIDs)
Severity = $item.MsrcSeverity
IsDownloaded = $item.IsDownloaded
Url = $item.MoreInfoUrls
Categories = ($item.Categories | Select-Object -ExpandProperty Name)
BundledUpdates = @($item.BundledUpdates)|ForEach{
[pscustomobject]@{
Title = $_.Title
DownloadUrl = @($_.DownloadContents).DownloadUrl
}
}
}#bundledUpdates
}#pscustomObject
}#forloop
}#if-condition
$updates |ft kb,title,severity -autosize
}

#removes schedule task from computer
#invoke-command -computername $computer -ScriptBlock {Unregister-ScheduledTask -TaskName PSWindowsUpdate -Confirm:$false}

<# another workaround to WinRM limitation on Windows Updates
$pass = ConvertTo-SecureString "PA$$W0RD" -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential "User", $pass
Start-Process -Credential $creds powershell -ArgumentList "-Command & { ... whatever you want to do ... }"
#>

if (checkPSVersion){
installPrerequisites;

# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU"
$wuKey="UseWUServer"
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey

if($wuIsOn){
# Perform updates by manipulating WSUS keys
"Turn WSUS settings OFF temporarily..."
setRegKey -path $wuPath -name $wuKey -value 0
restart-service wuauserv

# Perform Updates
triggerWindowsUpdate;
listPendingUpdates;

"Turning WSUS settings back to ON status"
setRegKey -path $wuPath -name $wuKey -value 1
restart-service wuauserv
}else{
# Perform Updates without manipulating WSUS keys
triggerWindowsUpdate;
listPendingUpdates;
}
}else{
"PowerShell version required is 5.0.`nHere is the link to download PS 5.1: if this fails."
}

}

$servers|%{applyWindowsUpdates $_}

<# this snippet has bugs
# Start jobs on muliple targets simultaneously
ForEach ($server in $servers) {
"Starting process on $server..."
$running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
if ($running.Count -le 8) {
Start-Job {
applyWindowsUpdates $server
}
}else{
$running | Wait-Job
}
Get-Job | Receive-Job
}
#>

<# invoke command on a list of servers
Invoke-Command -computername $servers -ScriptBlock {
param($updateWindows)
[ScriptBlock]::Create($updateWindows).Invoke()
} -Args ${function:applyWindowsUpdates} -ThrottleLimit 1
#>

<#
New-PSSession : [NODE007] Connecting to remote server NODE007failed with the following error message : WinRM cannot process the request. The following
error with errorcode 0x80090324 occurred while using Kerberos authentication: There is a time and/or date difference between the client and server.
Possible causes are:
-The user name or password specified are invalid.
-Kerberos is used when no authentication method and no user name are specified.
-Kerberos accepts domain user names, but not local user names.
-The Service Principal Name (SPN) for the remote computer name and port does not exist.
-The client and remote computers are in different domains and there is no trust between the two domains.
After checking for the above issues, try the following:
-Check the Event Viewer for events related to authentication.
-Change the authentication method; add the destination computer to the WinRM TrustedHosts configuration setting or use HTTPS transport.
Note that computers in the TrustedHosts list might not be authenticated.
-For more information about WinRM configuration, run the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help
topic.
At line:10 char:20
+ $session = New-PSSession -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OpenError: (System.Manageme....RemoteRunspace:RemoteRunspace) [New-PSSession], PSRemotingTransportException
+ FullyQualifiedErrorId : 1398,PSSessionOpenFailed

#>

Older Version

# This script works on any Windows version with PowerShell 5.0 or higher installed. Get that from here: 
function checkPSVersion{
$version=$PSVersionTable.PSVersion.Major;
if ($version -ge 5){return $True;} else {return $False;}
}
function installPrerequisites{
# Set PowerShell Gallery as Trusted to bypass prompts
If(!("NuGet" -in (get-packageprovider).Name)){Install-PackageProvider -Name Nuget -Force}
If(!("PSGallery" -in (get-psrepository).Name)){Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -WarningAction SilentlyContinue}

# Include the PowerShell Windows Update module
$checkModule=Get-Module -ListAvailable -Name PSWindowsUpdate
if(!($checkModule)){Install-Module PSWindowsUpdate -Confirm:$false;}

# Register the user of Windows Update Service if it has not been registered
$MicrosoftUpdateID="7971f918-a847-4430-9279-4a52d1efe18d"
$registered=(Get-WUServiceManager).ServiceID | Where-Object {$MicrosoftUpdateID -contains $_}
if (!($registered)){
Add-WUServiceManager -ServiceID $MicrosoftUpdateID -Confirm:$false
}
}

function applyWindowsUpdates{
if (checkPSVersion){
installPrerequisites;

# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU"
$wuKey="UseWUServer"
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey

if($wuIsOn){
# Turn WU Server OFF temporarily
setRegKey -path $wuPath -name $wuKey -value 0
restart-service wuauserv

# Perform Updates
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot;

# Turn Windows Update Server back to ON
setRegKey -path $wuPath -name $wuKey -value 1
restart-service wuauserv

"Windows Update sequence is completed.";
}else{
# Perform Updates
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot;
"Windows Update sequence is completed.";
}
} else {
"Please update PowerShell version before retrying this script.`nHere is the link to download PS 5.1: "
}
}

applyWindowsUpdates;