LoginSignup
2
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-29

沢山あるサーバに対して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(本体)

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

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する方法がわからなかったのでアホなコードで分岐終了している。

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のスクリプトを同ディレクトリに配置する必要がある。

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)"
}
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3