
かつて山市良と呼ばれたおじさんのブログ
セイテクエンジニアのブログ かつて山市良と呼ばれたおじさんのブログ vol.128 インストールから再起動まで、更新の自動化に挑む|どうする!?残る閉域網の更新管理(3)
2025年08月07日配信
執筆者:山内 和朗
前回(vol.127)は、更新カタログファイル(wsusscn2.cab)を使用したオフラインスキャンを自動化しました。オフラインスキャンの結果を確認し、必要な更新プログラムをMicrosoft Updateカタログからダウンロードし、共有フォルダーに配置するまでは手作業になります。今回は、共有フォルダーに配置済みの更新プログラムのインストールから再起動までの自動化に挑戦します。
図1に、構想している更新管理環境を示します。クローズドネットワーク(閉域網)内のファイルサーバーの1台に専用の共有フォルダーを設け、ここに更新カタログ(オフラインスキャン用のwsuscn2.cab)と更新プログラムを配置して、利用可能な更新プログラムを確認し、更新プログラムをダウンロードしてインストールするという方法です。
図1 共有フォルダーを利用した、クローズドネットワーク(閉域網)内の更新管理環境
共有フォルダーのサブフォルダー(この例ではWS2022MSU、※マシンごとまたはWindowsのバージョンごとに分けることをお勧めします)に配置した更新プログラムパッケージ(MSU)は、更新対象のサーバーからダブルクリックして開始すれば、対話的にインストールを開始できます(画面1)。
画面1 対話的にMSUファイルを実行すればインストールできるが、これを自動化したい
これを人手を介すことなく、PowerShellスクリプトで自動実行させたいと思います。ヒントになるのは、以前、以下の連載で紹介した、仮想ハードディスク(VHD、VHDX)のオフラインパッチです。仮想ハードディスク(VHD、VHDX)をローカルマウントし、特定のフォルダーに配置した更新プログラムパッケージ(MSU)をDISM /Image.../Add-Package /PackagePath:...コマンドでマウント先イメージに適用して、最後にマウント解除するというものでした。
vol.29 Windowsのオフラインパッチ|続・ラボ環境 on Azure(1)
vol.30 VHD(x)に対するオフラインパッチ|続・ラボ環境 on Azure(2)
vol.31 VMテンプレートのオフラインパッチスクリプト|続・ラボ環境 on Azure(3)
今回のPowerShellスクリプト「onlinepatch.ps1」(上記のオフラインパッチに対してのスクリプト名、インターネット接続に関してはオフライン)、共有フォルダー上のサブフォルダーに配置した更新プログラムパッケージ(MSU)を1つずつ、Windows Updateスタンドアロンインストーラー(wusa.exe)を使用してサイレントインストールし、最後に再起動させます。このスクリプトが更新対象のサーバーの「C:¥tools」にあると仮定して、次のように実行します(画面2)。なお、悪意のある悪意のあるソフトウェアの削除ツール(EXE)については、後で紹介するログ出力スクリプトに含めています。
PS C:¥tools> .¥onlinepatch.ps1 -PackageDir <更新プログラムパッケージ(MSU)がある共有のUNCパス> |
Param($PackageDir)
if ($PSBoundParameters.Count -ne 1) { Write-host "Error: -PackageDir <path>" ;exit 1}
If ( -not (Test-Path $packagedir)) { Write-host "Error: -PackageDir <path> does not exist." ;exit 1}
$packages = (Get-ChildItem -Path $packagedir| where {$_.extension -eq ".msu"} | Sort Name | %{$_.FullName})
$success = $true
if ($packages.Count -gt 0){
Write-Output "Installation of updates is starting... "
foreach ($package in $packages){
Start-Process wusa.exe -Wait -ArgumentList "$package /quiet /norestart"
if (!($LASTEXITCODE -eq 0)) {
$success = $false
$package = "+ " + (Get-Item $package).Name + ": Failed"
} else {
$package = "+ " + (Get-Item $package).Name + ": Success"
}
Write-Output $package
}
Write-Output "Finished installing updates."
if ($success) {
Write-Output "Info: All updates were successfully installed."
} else {
Write-Output "Info: There are failed updates."
}
} else {
Write-Host "There are no updates (msu)."
#Pause
exit 0
}
if (Test-Path "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") {
Write-Output "Pending reboot. Press Enter to initiate reboot."
#Pause
Restart-Computer -Force
} else {
Write-Output "No reboot is required."
#Pause
}
exit 0
画面2 PowerShellスクリプト「onlinepatch.ps1」を実行すると、再起動後、更新が完了する。動作確認のためスクリプト内のPAUSEを有効にしている(行頭の#を削除)
スクリプトの要点だけ解説すると、以下のコードで「wusa.exe MSUパッケージのパス /quiet /norestart」コマンドを実行し、パッケージをサイレントインストールしています(再起動が必要でも抑制します)。その実行完了を待って、終了コード($LASTEXITCODE)でインストールの成否を判断しています。なお、対象の更新プログラムが既にインストール済みでインストール不要な場合は、インストールはスキップされ、インストール失敗を報告します。
foreach ($package in $packages){ if (!($LASTEXITCODE -eq 0)) { #インストール失敗(既にインストール済みの場合を含む) } else { #インストール成功 |
最後に、レジストリ「HKEY_LOCAL_MACHINE¥SOFTWARE¥Microsoft¥Windows¥CurrentVersion¥WindowsUpdate¥Auto Update¥RebootRequired」の存在を確認し、存在している場合は再起動が保留中と判断して再起動を実施します。
if (Test-Path "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") { Write-Output "Pending reboot. Press Enter to initiate reboot." Restart-Computer -Force } else { Write-Output "No reboot is required." } |
次のPowerShellスクリプトは、上記の「onlinepatch.ps1」の改良版です。改良版のスクリプトは、次のように実行します。
PS C:¥tools> .¥onlinepatch.ps1 -PackageDir <更新プログラムパッケージ(MSU)がある共有のUNCパス> -LogDir <ログの出力先の共有のUNCパス> |
悪意のあるソフトウェアの削除ツール(MSRT)(KB890830)はEXE形式で提供されますが、改良版スクリプトではそのインストールにも対応しました。また、インストール後、MSRTのログを共有フォルダー上のパスにコンピューター名付きのファイル名(コンピューター名_mrt.log)でコピーします。さらに、更新プログラムパッケージ(MSU)のインストール結果と再起動の履歴をログファイル(コンピューター名_onlinepatch.log)に出力します(画面3)。なお、サブディレクトリの最初のインストーラーファイルが、ログファイルの日時よりも古い日時の場合、インストール済みと見なしてすべてをスキップします。
[onlinepatch.ps1(ログ出力対応)](プレーンテキストで表示)
Param($PackageDir, $LogDir)
if ($PSBoundParameters.Count -ne 2) { Write-host "Error: -PackageDir <path> -LogDir <Path>" ;exit 1}
If ( -not (Test-Path $packagedir)) { Write-host "Error: -PackageDir <path> does not exist." ;exit 1}
If ( -not (Test-Path $LogDir)) { Write-host "Error: -LogDir <path> does not exist." ;exit 1}
function Write-Log {
param (
[string]$Message,
[string]$LogPath = "$env:TEMP\onlinepatch.log"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$entry = "[$timestamp] $Message"
Add-Content -Path $LogPath -Value $entry
}
$LogPath = "$LogDir\$(hostname)_onlinepatch.log"
if (Test-Path $logPath) {
$logTime = (get-item $logPath).LastWriteTime
} else {
$logTime = get-Date("1900/1/1")
}
Write-Log -Message "Start onlinepatch.ps1" -LogPath $LogPath
$packages = (Get-ChildItem -Path $packagedir| where {$_.extension -eq ".msu"} | Sort Name | %{$_.FullName})
if ($packages.Count -gt 0){
if ((get-item $packages[0]).LastWriteTime -lt $logTime) {
$outmsg = "It was already installed,"
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
exit 0
}
}
# run MSRT, if exist
# https://support.microsoft.com/kb/891716
if (Test-Path "$PackageDir\Windows-KB890830-*.exe") {
Write-Log -Message "Run MSRT (result: $(hostname)_mrt.log)" -LogPath $LogPath
Copy-Item "$PackageDir\Windows-KB890830-*.exe" "$env:Temp\mymsrt.exe" -Force
Start-Process "$env:TEMP\mymsrt.exe" -Wait -ArgumentList "/q"
#Remove-Item "$env:Temp\mymsrt.exe"
Copy-Item "$env:WinDir\debug\mrt.log" "$LogDir\$(hostname)_mrt.log" -Force
}
#
#$packages = (Get-ChildItem -Path $packagedir| where {$_.extension -eq ".msu"} | Sort Name | %{$_.FullName})
$success = $true
if ($packages.Count -gt 0){
$outmsg = "Installation of updates is starting... "
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
foreach ($package in $packages){
Start-Process wusa.exe -Wait -ArgumentList "$package /quiet /norestart"
if (!($LASTEXITCODE -eq 0)) {
$success = $false
$package = "+ " + (Get-Item $package).Name + ": Failed"
} else {
$package = "+ " + (Get-Item $package).Name + ": Success"
}
Write-Output $package
Write-Log -Message $package -LogPath $LogPath
}
$outmsg = "Finished installing updates."
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
if ($success) {
$outmsg = "Info: All updates were successfully installed."
} else {
$outmsg = "Info: There is a failed update."
}
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
} else {
$outmsg = "There are no updates (msu)."
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
#Pause
exit 0
}
if (Test-Path "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") {
$outmsg = "Pending reboot."
Write-Output $outmsg + "Press Enter to initiate reboot."
Write-Log -Message $outmsg -LogPath $LogPath
#Pause
$outmsg = "Reboot now!"
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
Restart-Computer -Force
} else {
$outmsg = "No reboot is required."
Write-Output $outmsg
Write-Log -Message $outmsg -LogPath $LogPath
#Pause
}
exit 0
このスクリプトの要点は、MSRTのインストール(実行)を行う以下の部分です。UNCパスからの実行は、信頼できない場所とSmartScreenなどで判断されると(例えば、IPアドレス指定はインターネットゾーン)、実行がブロックされてしまします。そのため、ローカルの一時フォルダー($env:Temp)にコピーしてから実行し、実行が完了したら、MSRTのログ(過去の履歴を含みます)を共有フォルダーのログ出力先にコピーします。
if (Test-Path "$PackageDir\Windows-KB890830-*.exe") { Write-Log -Message "Run MSRT (result: $(hostname)_mrt.log)" -LogPath $LogPath Copy-Item "$PackageDir\Windows-KB890830-*.exe" "$env:Temp\mymsrt.exe" -Force Start-Process "$env:TEMP\mymsrt.exe" -Wait -ArgumentList "/q" Copy-Item "$env:WinDir\debug\mrt.log" "$LogDir\$(hostname)_mrt.log" -Force } |
画面3 改良版スクリプトを実行すると、MSRTのインストールとそのログ、およびMSUのインストールと再起動のログが記録される
改良版のスクリプトを実行するタスクをタスクスケジューラに登録して、都合の良い日(週末など)にスケジュール実行させることで、インストールから再起動を完全に自動化することができます。タスクは、ログインしているかどうかに関係なく、ビルトインAdministratorなどローカルユーザーの権限で自動実行させるようにしてください(画面4)。ただし、対話的な認証なしで共有フォルダーに接続できる必要があります(パススルー認証または接続状態の保存で)。共有フォルダーに接続するため、SYSTEM権限で実行させる場合、共有への接続処理が追加で必要になります。
画面4 改良版のonlinepatch.ps1をタスクで都合のよい日に自動実行させる