PowerShellで複数のマシンに対してWindowsUpdateを自動化

沢山あるサーバに対してWindowsUpdateを手動でかけていく作業が面倒＋時間がかかりすぎたので、
PowerShellを使って自動化。

WindowsUpdateする部分に関しては下記の記事をコードをほぼ転用した。
参考：https://qiita.com/asterisk9101/items/8a52562ade6d2a47a467

2019/12/4
スクリプトに例外処理などを追加


#概要
管理端末から対象マシンにリモートでWindowsUpdateを自動実行させたい。
下記の三種のスクリプトを作成し、3で2を、2でマシンのタスクを、マシンのタスクで1を実行する。
CSVにマシン名を列挙して、3.Job発行スクリプトを実行すれば各マシンでWindowsUpdateが実行される。

1.WindowsUpdate自動化1(本体)
→参考のスクリプトをps1で各マシンに保存し、タスクスケジューラにスクリプトを実行するタスクを登録する。(本当はInvoke-Commandで実行したいが、Updateファイルをダウンロードする際にアクセス拒否されるため。)

2.WindowsUpdate自動化2(制御)
→WindowsUpdateは再起動が絡むので1のスクリプトだけで完結できない。
 インストール→再起動→インストール のサイクルが終わって必要なパッチが無くなるまでWindowsUpdateをし続ける制御

3.Job発行
→複数PowerShell画面を立ち上げたりしたくないけど並列でやってもらいたいので、対象マシンをCSVファイルから読み込んで各マシンに対してWindowsUpdateを実行するバックグラウンドジョブを発行するスクリプト。


#1.WindowsUpdate自動化1(本体)
スクリプト化してるので、必要な情報がログに残るように改変。
また、前後のパッチの適用状況も知りたいので追加。
タスクの終了ステータスによって制御をかけたいので終了コードを指定している。

```Powershell:AutoWinUpd.ps1
# 1. ログの記録を開始する
$log = ".\$($env:ComputerName)_$(date -f yyyyMMdd).txt"
Start-Transcript $log -Append

#適用KB
Write-Host "適用KB一覧"
get-hotfix | ft HotFixID,InstalledOn -AutoSize

# 2. Windows Update を確認するセッションを開始
Write-Host "2. Windows Update を確認するセッションを開始"
$updateSession = New-Object -com Microsoft.Update.Session

# 3. Windows Update の検索
Write-Host "3. Windows Update の検索"
$searcher = $updateSession.CreateUpdateSearcher()
$searchResult = $searcher.search("IsInstalled=0 and Type='software'")

# 4. Windows Update の結果確認
Write-Host "4. Windows Update の結果確認"
$result = $searchResult.Updates | % { $_.title -replace ".*(KB\d+).*", "`$1`t$&" }
$result
if (($result | Measure-Object).Count -eq 0){
	Write-Host "適用が必要なUpdateはありません。"
	#適用KB
	Write-Host "適用KB一覧"
	get-hotfix | ft HotFixID,InstalledOn -AutoSize
	exit 0
}

# 5. 手作業が不要な項目だけを抽出する（通常は検索結果すべて）
Write-Host "5. 手作業が不要な項目だけを抽出する（通常は検索結果すべて）"
$updatesToDownload = New-Object -com Microsoft.Update.UpdateColl
$searchResult.Updates | ? { -not $_.InstallationBehavior.CanRequestUserInput } | ? { $_.EulaAccepted } | % { [void]$updatesToDownload.add($_) }
$updatesToDownload | FL Title

# 6. ダウンロードする
Write-Host "6. ダウンロードする"
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updatesToDownload
$downloader.Download()

# 7. ダウンロードが完了したものだけ抽出（通常は全てダウンロードされる）
Write-Host "7. ダウンロードが完了したものだけ抽出（通常は全てダウンロードされる）"
$updatesToInstall = New-Object -com Microsoft.Update.UpdateColl
$searchResult.Updates | ? { $_.IsDownloaded } | % { [void]$updatesToInstall.add($_) }
$updatesToInstall | FL Title

# 8. インストールする
Write-Host "8. インストールする"
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
$installationResult = $installer.Install()

# 9. インストールの結果を確認する（ResultCode 2 なら成功、3以上なら一部または全て失敗）
Write-Host "9. インストールの結果を確認する（ResultCode 2 なら成功、3以上なら一部または全て失敗）"
$installationResult
if ($installationResult.RebootRequired -eq $true){
	# 10. 再起動する（自動でStop-Transcriptされる）
	Write-Host "10. 再起動する（自動でStop-Transcriptされる）"
	date
    exit 1
}else{
	Stop-Transcript
    exit 2
}

```

#2.WindowsUpdate自動化2(制御)
今回の環境では接続元・接続先サーバでユーザ名・パスワードが一致しているので認証関連を省略。
一致していないならCSVからその辺も読むようにしてCredential情報を渡せばよいかな。
リモート実行するので、関わるサーバ間で`Enable-PSRemoting`とか`Set-Item`でWSManのTrustedHostを書き換えたりとか必要であればやっておく必要がある。
AD環境なら基本不要。
Switch文の中でWhileのループ`break`する方法がわからなかったのでアホなコードで分岐終了している。

```PowerShell:WindowsUpdateリモート実行.ps1
param($server)

$TaskName = "AutoWinUpd"


$loop = $true

function Wait-StartSchedule() {
    try{
        return (Get-Service -ComputerName $server -Name Schedule -ErrorAction Stop).Status -ne 'Running'
    }catch{
        return $true
    }

}

function Wait-StartWinRM() {
    try{
        return (Get-Service -ComputerName $server -Name WinRM -ErrorAction Stop).Status -ne 'Running'
    }catch{
        return $true
    }

}

function Wait-CompleteSchedule() {
    try{
        return Invoke-Command -ComputerName $server -ScriptBlock{(Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop).State -eq 'Running'} -ArgumentList $TaskName -ErrorAction Stop
    }catch{
        return $true
    }
}

function Check-TaskResult() {
    try{
        return Invoke-Command -ComputerName $server -ScriptBlock {(Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop | Get-ScheduledTaskInfo -ErrorAction Stop).LastTaskResult} -ArgumentList $TaskName -ErrorAction Stop
    }catch{
        return 1
    }
}

While($loop){

    #WinRMサービスが上がるまで待機
    Write-Host "WinRMサービスの待機中"
    While(Wait-StartWinRM){
    Start-Sleep 10
    }

    #タスクスケジューラサービスが上がるまで待機
    Write-Host "タスクスケジューラサービスの待機中"
    While(Wait-StartSchedule){
    Start-Sleep 10
    }

    #タスク実行
    try{
    Invoke-Command -ComputerName $server -ScriptBlock{Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop} -ArgumentList $TaskName -ErrorAction Stop
    }catch{
        Write-Host "タスクの実行に失敗しました。" -ForegroundColor Red
        Write-Host $_ -ForegroundColor Red
        break
    }

    #タスクが完了するまで待機
    Write-Host 'タスクを実行中です'
    While(Wait-CompleteSchedule){
        Start-Sleep 1
    }
    Write-Host 'タスクが完了しました'

    Switch(Check-TaskResult){
        0 {
            Write-Host "適用が必要なUpdateはありません。処理を終了します。"
            $loop = $false
            
            
            }
        1 {Write-Host "再起動します。"
           Restart-Computer -ComputerName $server -Force -Wait
        } 
        2 {Write-Host "再起動は不要です。再度WindowsUpdateを確認します。"
        }
    }
 

}


```

#3.Job発行
Jobを発行するだけ。
2のスクリプトを同ディレクトリに配置する必要がある。

```PowerShell:Job発行.ps1
cd $PSScriptRoot
$csv = Import-Csv -Path ".\servers.csv"
$time = Get-Date -Format yyyyMMddhhmm
foreach($server in $csv.serverName){
    Start-Job {powershell -File "$($args[0])\WindowsUpdateリモート実行.ps1" $args[1]} -ArgumentList $PSScriptRoot,$server -Name "$($time)_$($server)"
}
```