PowerShell ile Windows Güncelleme Yönetimi

Windows ortamlarında güncelleme yönetimi, sysadmin hayatının en can sıkıcı ama bir o kadar da kritik parçalarından biri. WSUS konsolunda tıkla bırak yapmak, onlarca sunucuyu tek tek RDP ile açıp “Windows Update” çalıştırmak… Bu döngüden çıkmanın en temiz yolu PowerShell. Bugün sıfırdan, production ortamında kullanabileceğin scriptlerle Windows güncelleme yönetimini ele alacağız.

Neden PowerShell ile Güncelleme Yönetimi?

Elle yapılan güncelleme süreçlerinde en büyük sorun tutarsızlık. Bir sunucuya bağlandın, güncelleme yaptın, kayıt tutmayı unuttun. Üç ay sonra hangi sunucunun hangi patch seviyesinde olduğunu bilmiyorsun. Audit geldiğinde panik başlıyor.

PowerShell burada iki temel sorunu çözüyor: otomasyon ve raporlama. Script bir kez yazılır, scheduled task olarak çalışır, log dosyasına yazar. Sabah işe geldiğinde mail kutunda rapor seni bekliyor.

Bunun yanı sıra büyük ortamlarda PSRemoting sayesinde 50 sunucuya aynı anda güncelleme uygulayabilir, sonuçları merkezi olarak toplayabilirsin. Bu yazıda hem tek sunucu hem de çoklu sunucu senaryolarını işleyeceğiz.

PSWindowsUpdate Modülü: Temel Silahın

Windows’un built-in cmdlet’leri güncelleme yönetimi için oldukça kısıtlı. Bu yüzden sektörün fiili standardı haline gelmiş PSWindowsUpdate modülünü kullanacağız. Modülü kurmak için:

# PowerShell Gallery'den kurulum
Install-Module -Name PSWindowsUpdate -Force -AllowClobber

# Kurulumu doğrula
Get-Module -Name PSWindowsUpdate -ListAvailable

# Modülü import et
Import-Module PSWindowsUpdate

Modül kurulduktan sonra mevcut cmdlet’leri görmek için:

Get-Command -Module PSWindowsUpdate

Çıktıda göreceğin önemli cmdlet’ler şunlar:

  • Get-WindowsUpdate: Mevcut güncellemeleri listeler
  • Install-WindowsUpdate: Güncellemeleri indirir ve yükler
  • Remove-WindowsUpdate: Belirli bir güncellemeyi kaldırır
  • Get-WUHistory: Güncelleme geçmişini getirir
  • Get-WURebootStatus: Bekleyen yeniden başlatma durumunu kontrol eder

Mevcut Güncelleme Durumunu Kontrol Etmek

İlk adım her zaman mevcut durumu anlamak. Hangi güncellemeler bekliyor, sistem ne zaman son güncellendi, reboot bekliyor mu?

# Bekleyen tüm güncellemeleri listele
Get-WindowsUpdate -AcceptAll -IgnoreReboot

# Sadece kritik güncellemeleri filtrele
Get-WindowsUpdate -Category "Critical Updates" -AcceptAll

# Security patch'leri getir
Get-WindowsUpdate -Category "Security Updates" -AcceptAll

# Güncelleme geçmişinin son 20 kaydını göster
Get-WUHistory -Last 20 | Select-Object -Property Date, Title, Result | Format-Table -AutoSize

# Reboot bekliyor mu kontrol et
Get-WURebootStatus -Silent

Get-WURebootStatus özellikle önemli. Production sunucularda bazen güncelleme uygulanmış ama reboot yapılmamış olabiliyor. Bu durumu script içinde yakalayıp uyarı oluşturabilirsin.

Temel Güncelleme Script’i

Basit ama işlevsel bir güncelleme scripti yazalım. Bu scripti küçük ortamlar için doğrudan kullanabilirsin:

# update_server.ps1
# Tek sunucu üzerinde güncelleme çalıştıran temel script

param(
    [switch]$AutoReboot,
    [string]$LogPath = "C:LogsWindowsUpdate"
)

# Log dizinini oluştur
if (-not (Test-Path $LogPath)) {
    New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
}

$LogFile = "$LogPathupdate_$(Get-Date -Format 'yyyyMMdd_HHmm').log"
$ServerName = $env:COMPUTERNAME
$StartTime = Get-Date

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogEntry = "[$TimeStamp] [$Level] $ServerName - $Message"
    Write-Host $LogEntry
    Add-Content -Path $LogFile -Value $LogEntry
}

Write-Log "Güncelleme süreci başlatıldı"

# Modülü import et
Import-Module PSWindowsUpdate -ErrorAction Stop

# Bekleyen güncellemeleri kontrol et
$PendingUpdates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
$UpdateCount = ($PendingUpdates | Measure-Object).Count

Write-Log "Toplam bekleyen güncelleme sayısı: $UpdateCount"

if ($UpdateCount -eq 0) {
    Write-Log "Yüklenecek güncelleme bulunamadı. Script sonlandırılıyor."
    exit 0
}

# Güncellemeleri listele
foreach ($Update in $PendingUpdates) {
    Write-Log "Bekleyen: $($Update.Title) - $($Update.Size) KB"
}

# Güncellemeleri yükle
Write-Log "Güncelleme yükleme başlıyor..."
Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1 | 
    ForEach-Object { Write-Log $_.ToString() }

$EndTime = Get-Date
$Duration = ($EndTime - $StartTime).TotalMinutes
Write-Log "Güncelleme tamamlandı. Süre: $([math]::Round($Duration, 2)) dakika"

# Reboot kontrolü
$RebootStatus = Get-WURebootStatus -Silent
if ($RebootStatus) {
    Write-Log "SİSTEM YENİDEN BAŞLATMA GEREKTİRİYOR" "WARNING"
    if ($AutoReboot) {
        Write-Log "Otomatik reboot başlatılıyor (60 saniye sonra)..."
        shutdown /r /t 60 /c "Windows Update - Otomatik Reboot"
    }
}

Bu scripti çalıştırırken dikkat etmen gereken: production saatlerinde reboot olmayacak şekilde scheduled task oluştur. -AutoReboot parametresini maintenance window dışında asla kullanma.

Çoklu Sunucu Yönetimi

Asıl güç çoklu sunucu senaryolarında ortaya çıkıyor. PSRemoting aktif olan bir ortamda 50 sunucuya aynı anda güncelleme uygulayabilirsin:

# multi_server_update.ps1
# Birden fazla sunucuda paralel güncelleme

param(
    [Parameter(Mandatory=$true)]
    [string[]]$ComputerList,
    [int]$ThrottleLimit = 10,
    [string]$ReportPath = "C:ReportsWUReport_$(Get-Date -Format 'yyyyMMdd').csv"
)

$Results = [System.Collections.Concurrent.ConcurrentBag[PSObject]]::new()

$ScriptBlock = {
    param($Computer)
    
    $Result = [PSCustomObject]@{
        ComputerName    = $Computer
        Status          = "Bilinmiyor"
        UpdateCount     = 0
        InstalledCount  = 0
        FailedCount     = 0
        RebootRequired  = $false
        ErrorMessage    = ""
        Timestamp       = Get-Date
    }
    
    try {
        $Session = New-PSSession -ComputerName $Computer -ErrorAction Stop
        
        $UpdateResult = Invoke-Command -Session $Session -ScriptBlock {
            Import-Module PSWindowsUpdate -ErrorAction Stop
            
            $Updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
            $Count = ($Updates | Measure-Object).Count
            
            if ($Count -gt 0) {
                $Installed = Install-WindowsUpdate -AcceptAll -IgnoreReboot -PassThru
                $Success = ($Installed | Where-Object { $_.Result -eq "Installed" } | Measure-Object).Count
                $Failed  = ($Installed | Where-Object { $_.Result -ne "Installed" } | Measure-Object).Count
                
                return @{
                    Total     = $Count
                    Installed = $Success
                    Failed    = $Failed
                    Reboot    = (Get-WURebootStatus -Silent)
                }
            }
            return @{ Total = 0; Installed = 0; Failed = 0; Reboot = $false }
        }
        
        $Result.Status         = "Başarılı"
        $Result.UpdateCount    = $UpdateResult.Total
        $Result.InstalledCount = $UpdateResult.Installed
        $Result.FailedCount    = $UpdateResult.Failed
        $Result.RebootRequired = $UpdateResult.Reboot
        
        Remove-PSSession $Session
    }
    catch {
        $Result.Status       = "Hata"
        $Result.ErrorMessage = $_.Exception.Message
    }
    
    return $Result
}

# Paralel çalıştır
$ComputerList | ForEach-Object -Parallel {
    $res = & $using:ScriptBlock -Computer $_
    $using:Results.Add($res)
} -ThrottleLimit $ThrottleLimit

# Sonuçları CSV'ye yaz
$Results | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8

Write-Host "`nÖzet Rapor:"
Write-Host "Toplam Sunucu: $($Results.Count)"
Write-Host "Başarılı: $(($Results | Where-Object { $_.Status -eq 'Başarılı' }).Count)"
Write-Host "Hatalı: $(($Results | Where-Object { $_.Status -eq 'Hata' }).Count)"
Write-Host "Reboot Gereken: $(($Results | Where-Object { $_.RebootRequired }).Count)"
Write-Host "Rapor: $ReportPath"

ForEach-Object -Parallel özelliği PowerShell 7 ile geldi. Eğer hala Windows PowerShell 5.1 kullanıyorsan Invoke-Command -ComputerName $ComputerList -AsJob yapısını tercih et.

WSUS Entegrasyonu

Kurumsal ortamlarda güncellemeler doğrudan Microsoft’tan değil, iç WSUS sunucusundan gelir. PSWindowsUpdate bu durumu da destekliyor:

# WSUS sunucusundan güncelleme al
# Önce sunucuyu WSUS'a yönlendir
Add-WUServiceManager -ServiceID "7971f918-a847-4430-9279-4a52d1efe18d" -AddServiceFlag 7

# Veya registry üzerinden WSUS ayarını kontrol et
$WUSettings = Get-ItemProperty -Path "HKLM:SOFTWAREPoliciesMicrosoftWindowsWindowsUpdateAU"
Write-Host "WSUS Sunucusu: $((Get-ItemProperty 'HKLM:SOFTWAREPoliciesMicrosoftWindowsWindowsUpdate').WUServer)"

# WSUS üzerinden güncelleme al (MicrosoftUpdate yerine WindowsUpdate servisi)
Get-WindowsUpdate -ServiceID "7971f918-a847-4430-9279-4a52d1efe18d" -AcceptAll

# Belirli bir KB numarasını WSUS'tan zorla yükle
Install-WindowsUpdate -KBArticleID "KB5025228" -AcceptAll -IgnoreReboot

Büyük ortamlarda WSUS güncelleme onay süreçleri kritik. Scriptin WSUS onaylı güncellemeleri mi yoksa tüm güncellemeleri mi alacağını net olarak belirlemek gerekiyor. Bunu yanlış yapılandırırsan WSUS politikalarını bypass ederek Microsoft’tan direkt güncelleme çekmiş olursun, bu da hem bant genişliği hem uyumluluk açısından sorun yaratır.

Güncelleme Raporlama ve Compliance Kontrolü

Sadece güncelleme yapmak yetmez, neyin güncellendiğini belgemen gerekiyor. Özellikle PCI DSS veya ISO 27001 kapsamındaki sistemlerde yama yönetimi kayıtları zorunlu:

# patch_compliance_report.ps1
# Sistemin güncel olup olmadığını ve son 30 günlük yama geçmişini raporlar

param(
    [string[]]$Servers,
    [int]$DaysBack = 30,
    [string]$OutputPath = "C:Reports"
)

if (-not (Test-Path $OutputPath)) {
    New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
}

$ReportDate = Get-Date -Format "yyyyMMdd_HHmm"
$ReportFile = "$OutputPathPatchCompliance_$ReportDate.html"

$AllResults = @()

foreach ($Server in $Servers) {
    try {
        $Data = Invoke-Command -ComputerName $Server -ScriptBlock {
            param($Days)
            Import-Module PSWindowsUpdate
            
            # Son N gündeki yüklenen güncellemeler
            $CutoffDate = (Get-Date).AddDays(-$Days)
            $History = Get-WUHistory | Where-Object { 
                $_.Date -ge $CutoffDate -and $_.Result -eq "Installed" 
            }
            
            # Bekleyen güncellemeler
            $Pending = Get-WindowsUpdate -AcceptAll -IgnoreReboot
            
            # Son başarılı güncelleme tarihi
            $LastUpdate = (Get-WUHistory | 
                Where-Object { $_.Result -eq "Installed" } | 
                Sort-Object Date -Descending | 
                Select-Object -First 1).Date
            
            return @{
                InstalledCount  = ($History | Measure-Object).Count
                PendingCount    = ($Pending | Measure-Object).Count
                LastUpdateDate  = $LastUpdate
                RebootRequired  = (Get-WURebootStatus -Silent)
                OSVersion       = (Get-WmiObject Win32_OperatingSystem).Caption
                InstalledKBs    = $History | Select-Object Title, Date, KB
            }
        } -ArgumentList $DaysBack
        
        $ComplianceStatus = if ($Data.PendingCount -eq 0) { "Uyumlu" } 
                            elseif ($Data.PendingCount -le 5) { "Kısmi" }
                            else { "Uyumsuz" }
        
        $AllResults += [PSCustomObject]@{
            Sunucu          = $Server
            OSVersiyon      = $Data.OSVersion
            SonGuncelleme   = $Data.LastUpdateDate
            YukluSayisi     = $Data.InstalledCount
            BekleyenSayisi  = $Data.PendingCount
            RebootGerekli   = $Data.RebootRequired
            Uyumluluk       = $ComplianceStatus
        }
    }
    catch {
        $AllResults += [PSCustomObject]@{
            Sunucu        = $Server
            Uyumluluk     = "BAĞLANTI HATASI"
            BekleyenSayisi = -1
        }
    }
}

# Sonuçları ekrana yazdır
$AllResults | Format-List

# CSV olarak kaydet
$AllResults | Export-Csv -Path "$OutputPathCompliance_$ReportDate.csv" -NoTypeInformation -Encoding UTF8
Write-Host "Rapor kaydedildi: $OutputPathCompliance_$ReportDate.csv"

Scheduled Task ile Otomatik Güncelleme

Script hazır, şimdi bunu otomatik çalışacak şekilde ayarlayalım. Maintenance window genellikle Cuma gecesi 02:00 ile Cumartesi 06:00 arası belirlenir:

# Scheduled Task oluştur
$TaskName    = "AutoWindowsUpdate"
$ScriptPath  = "C:Scriptsupdate_server.ps1"
$LogPath     = "C:LogsWindowsUpdate"
$TaskUser    = "DOMAINsvc_wsupdate"  # Dedicated service account kullan

# Action tanımla
$Action = New-ScheduledTaskAction `
    -Execute "PowerShell.exe" `
    -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$ScriptPath`" -LogPath `"$LogPath`""

# Trigger: Her Cuma 02:30
$Trigger = New-ScheduledTaskTrigger `
    -Weekly `
    -DaysOfWeek Friday `
    -At "02:30AM"

# Ayarlar: Güç kaynağı fark etmeksizin çalış, maksimum 4 saat
$Settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Hours 4) `
    -RunOnlyIfNetworkAvailable `
    -WakeToRun `
    -StartWhenAvailable

# Principal: SYSTEM yerine dedicated account kullan
$Principal = New-ScheduledTaskPrincipal `
    -UserId $TaskUser `
    -LogonType Password `
    -RunLevel Highest

# Task'ı kaydet
Register-ScheduledTask `
    -TaskName $TaskName `
    -TaskPath "CustomTasks" `
    -Action $Action `
    -Trigger $Trigger `
    -Settings $Settings `
    -Principal $Principal `
    -Description "Haftalık otomatik Windows güncelleme - Maintenance Window"

Write-Host "Scheduled Task oluşturuldu: $TaskName"

# Task durumunu kontrol et
Get-ScheduledTask -TaskName $TaskName | Select-Object TaskName, State, LastRunTime, NextRunTime

Servis hesabı kullanımı burada kritik. SYSTEM hesabıyla da çalışır ama domain ortamında, özellikle PSRemoting kullanan scriptlerde, dedicated bir servis hesabı çok daha yönetilebilir bir yapı sunar. Bu hesaba minimum gerekli yetkileri ver: Local Admin veya wsus_admin rolü yeterli.

Gerçek Dünya Senaryosu: Acil Security Patch Deployment

Diyelim ki zero-day açığı çıktı ve Microsoft acil patch yayınladı. IT manager sabah 7’de seni arıyor, “Bu KB’yi bugün mesai bitimine kadar tüm sunuculara uyguladık mı?” diyor. İşte bu senaryo için pratik script:

# emergency_patch.ps1
# Belirli bir KB'yi hedef sunuculara acil olarak uygular

param(
    [Parameter(Mandatory=$true)]
    [string]$KBNumber,
    
    [Parameter(Mandatory=$true)]
    [string]$ServerListFile,  # Sunucu listesi TXT dosyası, her satırda bir sunucu
    
    [string]$ReportPath = "C:ReportsEmergencyPatch_$(Get-Date -Format 'yyyyMMdd_HHmm').csv",
    [switch]$WhatIf
)

$Servers = Get-Content $ServerListFile | Where-Object { $_ -ne "" -and $_ -notlike "#*" }
Write-Host "Hedef sunucu sayısı: $($Servers.Count)"
Write-Host "Hedef KB: $KBNumber"

if ($WhatIf) {
    Write-Host "WHATIF MODU: Gerçek kurulum yapılmayacak"
}

$Results = @()

foreach ($Server in $Servers) {
    Write-Host "[$Server] İşleniyor..." -ForegroundColor Cyan
    
    $Result = [PSCustomObject]@{
        Sunucu      = $Server
        KB          = $KBNumber
        Durum       = ""
        Mesaj       = ""
        Tarih       = Get-Date
    }
    
    try {
        $PingResult = Test-Connection -ComputerName $Server -Count 1 -Quiet
        if (-not $PingResult) {
            $Result.Durum = "PING_FAIL"
            $Result.Mesaj = "Sunucuya erişilemiyor"
            $Results += $Result
            continue
        }
        
        $PatchResult = Invoke-Command -ComputerName $Server -ScriptBlock {
            param($KB, $TestMode)
            Import-Module PSWindowsUpdate
            
            # Güncelleme mevcut mu?
            $Update = Get-WindowsUpdate -KBArticleID $KB -AcceptAll -IgnoreReboot
            
            if (($Update | Measure-Object).Count -eq 0) {
                # Zaten yüklü mü kontrol et
                $History = Get-WUHistory | Where-Object { $_.KB -eq $KB }
                if ($History) {
                    return @{ Status = "ZATEN_YUKLU"; Message = "Önceden yüklendi: $($History[0].Date)" }
                }
                return @{ Status = "BULUNAMADI"; Message = "KB güncelleme listesinde yok" }
            }
            
            if ($TestMode) {
                return @{ Status = "WHATIF"; Message = "WhatIf modu, yüklenmedi" }
            }
            
            $Install = Install-WindowsUpdate -KBArticleID $KB -AcceptAll -IgnoreReboot
            $RebootNeeded = Get-WURebootStatus -Silent
            
            return @{ 
                Status  = "YUKLENDI"
                Message = "Başarıyla yüklendi. Reboot gerekli: $RebootNeeded"
                Reboot  = $RebootNeeded
            }
        } -ArgumentList $KBNumber, $WhatIf.IsPresent
        
        $Result.Durum = $PatchResult.Status
        $Result.Mesaj = $PatchResult.Message
        
        $Color = switch ($PatchResult.Status) {
            "YUKLENDI"     { "Green" }
            "ZATEN_YUKLU"  { "Yellow" }
            "BULUNAMADI"   { "Red" }
            default        { "White" }
        }
        Write-Host "[$Server] $($PatchResult.Status): $($PatchResult.Message)" -ForegroundColor $Color
    }
    catch {
        $Result.Durum = "HATA"
        $Result.Mesaj = $_.Exception.Message
        Write-Host "[$Server] HATA: $($_.Exception.Message)" -ForegroundColor Red
    }
    
    $Results += $Result
}

$Results | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8

Write-Host "`n=== ÖZET ===" -ForegroundColor White
Write-Host "Toplam: $($Results.Count)"
Write-Host "Yüklendi: $(($Results | Where-Object Durum -eq 'YUKLENDI').Count)" -ForegroundColor Green
Write-Host "Zaten Yüklü: $(($Results | Where-Object Durum -eq 'ZATEN_YUKLU').Count)" -ForegroundColor Yellow
Write-Host "Hata: $(($Results | Where-Object { $_.Durum -in 'HATA','PING_FAIL','BULUNAMADI' } ).Count)" -ForegroundColor Red
Write-Host "Rapor: $ReportPath"

Bu scripti önce -WhatIf parametresiyle çalıştır, hangi sunuculara ulaşıldığını gör, sonra gerçek kurulumu başlat. Acil durumlarda bile test et sonra uygula prensibinden vazgeçme.

Sık Karşılaşılan Sorunlar ve Çözümleri

Production ortamında bu scriptleri kullanırken karşılaşacağın bazı yaygın sorunlar var:

PSRemoting Bağlantı Sorunları

  • WinRM servisinin çalışıp çalışmadığını Test-WSMan -ComputerName sunucu ile kontrol et
  • Firewall’da 5985 (HTTP) veya 5986 (HTTPS) portlarının açık olduğundan emin ol
  • Domain dışı sunucular için TrustedHosts ayarını yapılandırman gerekebilir

PSWindowsUpdate Modül Hatası

  • Modül remote sunucuda yüklü olmayabilir. Scripte Install-Module bloğu ekle ama bunu dikkatli yönet
  • Execution Policy kısıtlaması: Set-ExecutionPolicy RemoteSigned -Scope LocalMachine ile çöz
  • Proxy arkasındaki sunucularda PowerShell Gallery erişimi olmayabilir, bu durumda modülü manuel dağıt

Güncelleme Takılı Kalıyor

  • Windows Update Agent bazen corrupt duruma geliyor. wuauclt.exe /detectnow veya UsoClient.exe StartScan ile servis sıfırlama yapabilirsin
  • Disk doluysa güncelleme indirme başarısız olur. Script içine disk kontrolü ekle

Reboot Sonrası Script Tamamlanmıyor

  • Uzun güncelleme süreçlerinde sistem reboot olursa script yarıda kalır
  • Çözüm: Get-WURebootStatus kontrolünü döngü içine al, reboot öncesi sonuçları kaydet

Sonuç

PowerShell ile güncelleme yönetimi, başlangıçta karmaşık görünse de doğru yapılandırılmış scriptlerle sysadmin hayatını ciddi ölçüde kolaylaştırıyor. Bu yazıda anlattığımız yaklaşımı kendi ortamına adapte ederken şu noktaları aklından çıkarma:

Her script önce test ortamında çalışmalı. Özellikle -AutoReboot gibi parametreler production saatlerinde felaket yaratabilir. Maintenance window’ları Scheduled Task tetikleyicilerinde net olarak tanımla, değiştiğinde güncellemeyi unutma.

Raporlama kısmını atlama. Güncelleme yapmak zaten işin yarısı; yaptığını kanıtlamak diğer yarısı. Compliance ve audit süreçlerinde CSV raporları hayat kurtarıyor.

Büyüyen ortamlarda bu scriptleri SCCM, Ansible veya benzeri araçlarla entegre etmeyi düşün. Ama o araçlara geçene kadar, iyi yazılmış bir PowerShell script seti seni çok iyi götürür.

Son olarak: bu scriptleri kopyalayıp direkt kullanma. Kendi ortamına göre yolları, log mekanizmalarını, hata kontrollerini adapte et. Her ortam farklı, her sysadmin’in workflow’u farklı. Scripti senin ihtiyacına göre şekillendir, sahiplen.

Yorum yapın