PC-Kitting mit winget + PowerShell automatisieren — Aus dem Handbuch ein ausführbares Skript machen

· · winget, PowerShell, Windows, PC-Kitting, Unternehmens-IT, Automatisierung, Betriebsoptimierung, Betriebliche Effizienz

Jedes Mal, wenn ein neuer Mitarbeiter beginnt oder ein PC ersetzt wird, setzt sich jemand mit dem Handbuch hin und konfiguriert Rechner einen nach dem anderen — in der IT-Abteilung eines kleinen oder mittelständischen Unternehmens ist das nach wie vor der Normalfall. Und das Problem ist nicht nur die Zeit. Manuelle Arbeit hat keine Reproduzierbarkeit, sodass Ärger der Form „nur bei diesem Rechner sind die Einstellungen anders“ später einholt. Das Handbuch veraltet, weil es niemand aktualisiert, und wechselt die zuständige Person, gehen die Details verloren.

Windows bringt standardmäßig den Paketmanager winget mit, und das Installieren einer Anwendung ist eine Ein-Zeilen-Angelegenheit. Gehen Sie mit WinGet Configuration noch weiter, können Sie Anwendungen und Einstellungen in einer einzigen deklarativen YAML-Datei ausdrücken. Und die Bereiche, die winget nicht abdeckt — Drucker, Netzlaufwerke, unternehmensweite Registrierungseinstellungen und Ähnliches — lassen sich mit PowerShell ergänzen.

Dieser Artikel legt dar, wie Sie ein Kitting-Handbuch durch eine „ausführbare Datei“ ersetzen, in einem Detailgrad, den Sie tatsächlich betreiben können.

1. Das Wichtigste zuerst

  • Überlassen Sie die Anwendungsinstallation winget. winget install unterstützt standardmäßig eine stille Installation.1
  • Die Konfiguration eines bestehenden PCs können Sie mit winget export extrahieren. Das beschränkt sich jedoch auf von winget verwaltete Pakete, Einstellungen sind nicht enthalten.2
  • Möchten Sie deklarativ vorgehen, verwenden Sie WinGet Configuration (winget configure). Es basiert auf PowerShell DSC und lässt Sie Anwendungen und Einstellungen in eine einzige YAML-Datei schreiben. Erforderlich sind Windows 10 1809 oder höher plus winget 1.6 oder höher.3
  • Ergänzen Sie mit PowerShell, was winget nicht erledigt. Drucker, freigegebene Laufwerke, die Registrierung, Windows-Features, lokale Konten und Ähnliches.
  • Um es aus PowerShell heraus zu steuern, gibt es das Modul Microsoft.WinGet.Client. Es bietet Cmdlets wie Install-WinGetPackage.4
  • Die Ausführung im Systemkontext erfordert Vorsicht. Microsoft führt sie weiterhin als zukünftiges Entwicklungsvorhaben, eine Verifikation mit dem tatsächlichen Ausführungskonto ist daher unerlässlich.3
  • Sorgen Sie stets für Idempotenz. Kitting scheitert mitten im Vorgang, und es muss möglich sein, es beliebig oft erneut auszuführen.
  • Zwingen Sie interne Anwendungen nicht in winget hinein — einen stillen Installer aus PowerShell auszuführen ist die realistische Option.

2. winget-Grundlagen — Wie Sie eine unbeaufsichtigte Installation schreiben

Zunächst die Standardform für eine zuverlässige Installation ohne jede Interaktion.1

# Die ID exakt angeben (-e bedeutet exakte Übereinstimmung, --id gibt die ID an)
#   --silent                     : keine Oberfläche anzeigen
#   --accept-package-agreements  : die Nutzungsbedingungen des Pakets akzeptieren
#   --accept-source-agreements   : die Nutzungsbedingungen der Quelle akzeptieren
#   --scope machine              : für alle Benutzer installieren (nur unterstützte Pakete)
# Den Zeilenfortsetzungs-Backtick an das Zeilenende setzen. Ein Kommentar danach unterbricht die Fortsetzung
winget install --id Google.Chrome -e --silent `
    --accept-package-agreements --accept-source-agreements --scope machine

# Die ID nachschlagen
winget search "Visual Studio Code"
winget show --id Microsoft.VisualStudioCode

Vergessen Sie die --accept-*-Flags, stockt ein unbeaufsichtigter Lauf beim Warten auf eine Zustimmung. Das ist die erste Stolperfalle bei der Automatisierung des Kittings.

--scope machine lässt sich nicht bei jedem Paket verwenden; manche Anwendungen können nur pro Benutzer installiert werden. In diesem Fall gestalten Sie den Ablauf so, dass er bei der ersten Anmeldung im Benutzerkontext läuft.

3. Die Konfiguration eines bestehenden PCs extrahieren — export / import

Haben Sie bereits einen vollständig eingerichteten „Standard-PC“, können Sie dessen Konfiguration in eine Datei überführen.2

# Vom Standard-PC aus eine Liste der installierten Pakete als JSON schreiben
winget export --output D:\kitting\apps.json --include-versions

# Auf dem neuen PC wiederherstellen
winget import --import-file D:\kitting\apps.json `
    --accept-package-agreements --accept-source-agreements --ignore-unavailable

Wichtig ist, die Grenzen zu verstehen.

Was es leistet Was es nicht leistet
Die Liste der von winget verwalteten Pakete reproduzieren Einstellungen innerhalb der Anwendungen migrieren
Versionen festlegen (--include-versions) Anwendungen, die außerhalb von winget installiert wurden, oder interne Anwendungen
Nicht mehr vorhandene Pakete überspringen (--ignore-unavailable) Lizenzaktivierung, Anmeldezustand

Mit anderen Worten: winget import ist ein Ausgangspunkt, nicht das gesamte Kitting. Wie Sie den Rest ergänzen, ist das eigentliche Thema.

4. Deklarativ schreiben — WinGet Configuration

winget configure ist ein Mechanismus, um den gewünschten Endzustand in YAML zu deklarieren und ihn über PowerShell DSC anzuwenden.3 Im Vergleich zu einem prozeduralen Skript hat es drei Vorteile.

  • Es geschieht nichts, wenn der gewünschte Zustand bereits vorliegt (idempotent)
  • Anwendungsinstallation sowie Windows-/Anwendungseinstellungen lassen sich in einer Datei ausdrücken
  • Scheitert es mitten im Vorgang, führen Sie einfach dieselbe Datei erneut aus
# kitting.winget — den gewünschten Zustand eines Standardrechners deklarieren
properties:
  configurationVersion: 0.2.0
  resources:
    - resource: Microsoft.WinGet.DSC/WinGetPackage
      id: chrome
      directives:
        description: Google Chrome installieren
        allowPrerelease: true
      settings:
        id: Google.Chrome
        source: winget

    - resource: Microsoft.WinGet.DSC/WinGetPackage
      id: vscode
      directives:
        description: Visual Studio Code installieren
      settings:
        id: Microsoft.VisualStudioCode
        source: winget

    - resource: Microsoft.Windows.Developer/DeveloperMode
      id: devmode
      directives:
        description: Entwicklermodus aktivieren (nur Entwicklungsrechner)
        allowPrerelease: true
      settings:
        Ensure: Present
# Den Inhalt vor dem Anwenden prüfen (zeigt, was ausgeführt würde)
winget configure show --file D:\kitting\kitting.winget

# Anwenden
winget configure --file D:\kitting\kitting.winget --accept-configuration-agreements

Die Voraussetzungen sind Windows 10 Version 1809 (Build 17763) oder höher, oder Windows 11, sowie winget 1.6.2631 oder höher.3 In einer Umgebung, in der ältere Rechner verbleiben, ist die PowerShell-basierte Konfiguration aus dem nächsten Abschnitt verlässlicher.

5. Lücken mit PowerShell füllen — was winget nicht erledigt

Beim tatsächlichen Kitting steckt der Großteil des Aufwands in Wirklichkeit in allem außer der Anwendungsinstallation. Hier kommt PowerShell ins Spiel. Entscheidend ist, alles in der Form „nichts tun, wenn es bereits erledigt ist“ zu schreiben.

Dieses Skript liest kitting.config.json. Ein vollständiges Beispiel der Konfigurationsdatei liegt als kitting.config.json im Beispielcode dieses Artikels (das zip am Ende des Artikels) bei, hier vorab die Struktur.

{
  "folders": [ "C:\\Work", "C:\\KsTools" ],
  "registry": [
    { "key": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge",
      "name": "HomepageLocation", "value": "https://intra.example.co.jp/", "type": "String" }
  ],
  "userRegistry": [
    { "key": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
      "name": "HideFileExt", "value": 0, "type": "DWord" }
  ],
  "windowsFeatures": [ "NetFx3" ],
  "wingetPackages": [ { "id": "Google.Chrome", "scope": "machine" } ],
  "internalApps": [
    { "displayName": "KsApp Business System Client",
      "installer": "\\\\fileserver\\deploy\\KsApp\\KsAppSetup.msi",
      "arguments": "/quiet /norestart INSTALLDIR=\"C:\\Program Files\\KsApp\"",
      "version": "3.2.0" }
  ],
  "drives":   [ { "letter": "S", "path": "\\\\fileserver\\share" } ],
  "printers": [ { "name": "\\\\printserver\\MFP-1F", "connection": "\\\\printserver\\MFP-1F" } ]
}

registry und userRegistry sind getrennt, weil sie an unterschiedlichen Stellen wirken. Eine Einstellung wie das „Dateierweiterungen anzeigen“ des Explorers (HideFileExt) liest das benutzerbezogene HKCU, keine HKLM-Richtlinie. Ein Schreiben nach HKLM in der Administratorphase ändert die Anzeige nicht. Legen Sie benutzerbezogene Einstellungen wie diese in userRegistry und wenden Sie sie in der später beschriebenen, nicht erhöhten Phase an.

Das displayName unter internalApps muss exakt dem Namen entsprechen, der unter „Programme und Features“ angezeigt wird. Geben Sie version an, prüft die Kontrolle zusätzlich, ob diese Version oder eine neuere installiert ist (fehlt sie, erfolgt die Prüfung allein anhand des Namens).

Die Anwendungsinstallation selbst können Sie auch mit winget import oder WinGet Configuration aus den vorherigen Abschnitten erledigen, dieses Skript installiert jedoch auch wingetPackages mit. Die Konfiguration in einer einzigen Datei zu halten bedeutet, dass die Definition von „was auf diesen Rechner gehört“ an einem Ort liegt und ein Lauf alles erledigt. Umgekehrt gilt: Lassen Sie in der Konfigurationsdatei geschriebene Einträge vom Skript ungelesen, verbuchen Sie am Ende einen Rechner, dem sie fehlen, fälschlich als „Kitting erfolgreich“.

#Requires -RunAsAdministrator
[CmdletBinding()]
param(
    [string] $ConfigPath = "$PSScriptRoot\kitting.config.json"
)

$ErrorActionPreference = 'Stop'
# Get-Content liest unter 5.1 ohne BOM über die ANSI-Codepage.
# UTF-8-JSON auf einem deutschsprachigen Rechner zu lesen, verstümmelt es, geben Sie die Kodierung deshalb explizit an
$config = Get-Content $ConfigPath -Raw -Encoding utf8 | ConvertFrom-Json
$rebootRequired  = $false
$rebootInitiated = $false
$log    = "C:\ProgramData\KsKitting\kitting_$(Get-Date -f yyyyMMdd_HHmmss).log"
$null   = New-Item -Path (Split-Path $log) -ItemType Directory -Force

# Die später beschriebene benutzerbezogene Phase läuft in einem eigenen Prozess, verteilen Sie die
# Konfiguration deshalb an einen für jeden Benutzer lesbaren Ort.
# Legen Sie das Verteilungspaket an diesem Ort ab und führen Sie es von dort aus, sind Kopierquelle
# und -ziel dieselbe Datei. Copy-Item behandelt ein Kopieren auf sich selbst als Fehler, sodass unter
# $ErrorActionPreference = 'Stop' das gesamte Skript stoppt, bevor das Kitting überhaupt beginnt
$sharedConfig = 'C:\ProgramData\KsKitting\kitting.config.json'
$sourceFull   = (Resolve-Path $ConfigPath).ProviderPath
$sharedFull   = if (Test-Path $sharedConfig) { (Resolve-Path $sharedConfig).ProviderPath } else { '' }
if (-not [string]::Equals($sourceFull, $sharedFull, 'OrdinalIgnoreCase')) {
    Copy-Item $ConfigPath $sharedConfig -Force
}
Start-Transcript -Path $log -Append

try {
    # --- 1. Standardordner ----------------------------------------------
    foreach ($dir in $config.folders) {
        if (-not (Test-Path $dir)) {
            $null = New-Item -Path $dir -ItemType Directory
            Write-Verbose "Erstellt: $dir"
        }
    }

    # --- 2. Unternehmensweite Registrierungseinstellungen ----------------------------
    foreach ($reg in $config.registry) {
        if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
        $item   = Get-Item -Path $reg.key
        $exists = $item.GetValueNames() -contains $reg.name

        # Vergleichen Sie auch die "Art" zusammen mit dem Wert. Ein REG_SZ "1" und ein DWORD 1
        # vergleichen sich in PowerShell als gleich, wodurch eine tatsächlich nicht wirksame
        # Einstellung wie „angewendet“ aussieht
        $sameValue = $exists -and $item.GetValue($reg.name) -eq $reg.value
        $sameKind  = $exists -and $item.GetValueKind($reg.name).ToString() -eq $reg.type

        if (-not ($sameValue -and $sameKind)) {
            Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
            Write-Verbose "Gesetzt: $($reg.key)\$($reg.name) = $($reg.value) ($($reg.type))"
        }
    }

    # --- 3. Windows-Features ----------------------------------------------
    foreach ($feature in $config.windowsFeatures) {
        $state = Get-WindowsOptionalFeature -Online -FeatureName $feature
        if ($state.State -ne 'Enabled') {
            # Wenn Sie den Neustart mit -NoRestart unterdrücken, kommt die Information, ob einer
            # nötig ist, im Rückgabewert als RestartNeeded zurück. Werten Sie das nicht aus, "beendet
            # es sich mit 0, obwohl ein Neustart erforderlich ist"
            $result = Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart
            if ($result.RestartNeeded) { $rebootRequired = $true }
        }
    }

    # --- 4. winget-Pakete -----------------------------------------------
    # Der Exit-Code von winget kann auch dann ungleich null sein, wenn das Paket "bereits installiert"
    # ist, und umgekehrt 0 sein, obwohl das Paket tatsächlich nicht vorhanden ist. Beurteilen Sie den
    # Erfolg durch eine Zustandsabfrage.
    # Ohne --scope erfassen Sie die eigene benutzerbezogene Installation des Administrators und
    # schließen fälschlich, es sei "für den Rechner installiert"
    function Test-KsWingetPackage {
        param([string] $Id, [string] $Scope)
        $arguments = @('list', '--id', $Id, '--exact', '--accept-source-agreements')
        if ($Scope) { $arguments += @('--scope', $Scope) }
        $null = winget @arguments 2>&1
        return ($LASTEXITCODE -eq 0)
    }

    foreach ($package in $config.wingetPackages) {
        if (Test-KsWingetPackage -Id $package.id -Scope $package.scope) { continue }

        # Den Zeilenfortsetzungs-Backtick an das Zeilenende setzen. Ein Kommentar danach unterbricht die Fortsetzung
        winget install --id $package.id -e --silent `
            --accept-package-agreements --accept-source-agreements `
            --scope $package.scope
        $wingetExit = $LASTEXITCODE

        # Geben Sie hier nur eine Warnung aus und machen weiter, verbucht das Bereitstellungswerkzeug
        # einen Rechner, dem eine erforderliche Anwendung fehlt, als "Kitting erfolgreich"
        if (-not (Test-KsWingetPackage -Id $package.id -Scope $package.scope)) {
            throw "Installation von $($package.id) fehlgeschlagen (winget ExitCode=$wingetExit)"
        }
    }

    # --- 5. Interne Anwendungen (Installer aus einem freigegebenen Ordner still ausführen) ----
    # Die Liste der installierten Anwendungen mit sowohl der 32-Bit- als auch der 64-Bit-Ansicht
    # explizit öffnen. Läuft 32-Bit-PowerShell auf 64-Bit-Windows (was je nach Intune-Konfiguration
    # vorkommen kann), verweist HKLM:\SOFTWARE\... durch die WOW64-Umleitung auf die 32-Bit-Ansicht,
    # sodass 64-Bit-Anwendungen fälschlich als "nicht installiert" beurteilt und jedes Mal neu installiert werden
    $installedApps = foreach ($view in 'Registry64', 'Registry32') {
        $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view)
        try {
            $uninstall = $baseKey.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
            if (-not $uninstall) { continue }
            try {
                foreach ($name in $uninstall.GetSubKeyNames()) {
                    $appKey = $uninstall.OpenSubKey($name)
                    if (-not $appKey) { continue }
                    try {
                        $displayName = $appKey.GetValue('DisplayName')
                        if ($displayName) {
                            [pscustomobject]@{
                                DisplayName    = [string] $displayName
                                DisplayVersion = [string] $appKey.GetValue('DisplayVersion')
                            }
                        }
                    }
                    finally { $appKey.Dispose() }
                }
            }
            finally { $uninstall.Dispose() }
        }
        finally { $baseKey.Dispose() }
    }

    foreach ($app in $config.internalApps) {
        $installed = $installedApps | Where-Object DisplayName -eq $app.displayName

        # Beurteilen Sie "installiert" nicht allein anhand einer DisplayName-Übereinstimmung. Ein Rechner,
        # der noch eine alte Version trägt, erhält so nie die neue, und ein erneuter Lauf konvergiert nie
        # zum Zustand der Konfigurationsdatei. Enthält die Konfiguration eine Version, prüfen Sie, ob diese
        # Version oder eine neuere vorhanden ist
        $upToDate = if ($app.version) {
            $wanted = [version] $app.version
            [bool]($installed | Where-Object {
                $cur = $_.DisplayVersion -as [version]   # Notationen, die keine Versionsform haben, sind außen vor
                $cur -and $cur -ge $wanted
            })
        } else {
            [bool] $installed
        }
        if ($upToDate) { continue }

        # Das -ArgumentList von Start-Process fügt das Array mit Leerzeichen zu einer einzigen
        # Befehlszeile zusammen, deshalb Werte mit Leerzeichen auf Seiten der Konfigurationsdatei
        # in Anführungszeichen setzen
        # z. B. '/quiet /norestart INSTALLDIR="C:\Program Files\KsApp"'
        # Eine .msi ist keine ausführbare Datei, das direkte Übergeben an Start-Process schlägt mit
        # "is not a valid application" fehl. Über msiexec starten
        if ([System.IO.Path]::GetExtension($app.installer) -eq '.msi') {
            $msiArgs = @('/i', "`"$($app.installer)`"") + $app.arguments
            $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
        }
        else {
            $proc = Start-Process -FilePath $app.installer -ArgumentList $app.arguments -Wait -PassThru -NoNewWindow
        }
        # Bei Windows Installer ist "nur 0 ist Erfolg" falsch.
        # Sowohl 3010 als auch 1641 sind Erfolg; nur die Behandlung des Neustarts unterscheidet sich
        switch ($proc.ExitCode) {
            0    { }                                   # Erfolg
            3010 { $rebootRequired = $true }           # Erfolg. Neustart erforderlich (ERROR_SUCCESS_REBOOT_REQUIRED)
            1641 { $rebootInitiated = $true }          # Erfolg. Der Installer hat einen Neustart eingeleitet
            default {
                throw "Installation von $($app.displayName) fehlgeschlagen (ExitCode=$($proc.ExitCode))"
            }
        }
        # Hat bereits ein Neustart begonnen, würden nachfolgende Installationen ohnehin unterbrochen
        if ($rebootInitiated) { break }
    }

    if ($rebootInitiated) {
        # 1641 bedeutet "Erfolg, aber ein Neustart wurde bereits eingeleitet". Geben Sie 0 zurück,
        # behandelt das Bereitstellungswerkzeug dies als "beendet, dann von selbst neu gestartet",
        # geben Sie den Code deshalb durch
        Write-Host 'Der Installer hat einen Neustart eingeleitet. Nach dem Neustart erneut ausführen' -ForegroundColor Yellow
        exit 1641
    }

    if ($rebootRequired) {
        # Die unveränderte Rückgabe von 3010 lässt Intune oder das Bereitstellungswerkzeug dies als
        # "Erfolg, Neustart erforderlich" interpretieren und den Neustart planen und melden. Geben Sie
        # hier 0 zurück, gerät der Neustart in Vergessenheit
        Write-Host 'Kitting abgeschlossen (Neustart erforderlich)' -ForegroundColor Yellow
        exit 3010
    }

    Write-Host 'Kitting abgeschlossen' -ForegroundColor Green
    exit 0
}
catch {
    Write-Warning "Fehlgeschlagen: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1
}
finally {
    Stop-Transcript
}

Beachten Sie, dass dieses Administrator-Skript das Zuordnen von Netzlaufwerken bewusst auslässt. Laufwerksbuchstaben-Zuordnungen sind sitzungsbezogene Einstellungen pro Anmeldung, sodass eine in einer erhöhten Administratorsitzung erstellte Zuordnung im gewöhnlichen Explorer des Benutzers unter UAC nicht sichtbar ist. Führen Sie es mit Systemrechten aus Intune oder Ähnlichem aus, wird ohnehin in die Sitzung von SYSTEM zugeordnet, was dem Benutzer nichts nützt. Der korrekte Ansatz ist, benutzerbezogene Einstellungen nicht erhöht, bei der Anmeldung dieses Benutzers, auszuführen.

# Benutzerbezogene Einstellungen (aus einem Anmeldeskript oder aus HKCUs Run ausführen, nicht erhöht)

# Dies ist ein eigener Prozess gegenüber dem Administrator-Skript, $config wird deshalb nicht vererbt.
# Die von der Administratorphase nach ProgramData verteilte Konfiguration erneut einlesen
$configPath = 'C:\ProgramData\KsKitting\kitting.config.json'
if (-not (Test-Path $configPath)) {
    Write-Warning "Konfigurationsdatei nicht gefunden: $configPath"
    exit 1
}
$config = Get-Content $configPath -Raw -Encoding utf8 | ConvertFrom-Json

# Benutzerbezogene Registrierungseinstellungen (HideFileExt und Ähnliches. Schreiben nach HKLM hat keine Wirkung)
foreach ($reg in $config.userRegistry) {
    if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }

    $item   = Get-Item -Path $reg.key
    $exists = $item.GetValueNames() -contains $reg.name
    $same   = $exists -and
              $item.GetValue($reg.name) -eq $reg.value -and
              $item.GetValueKind($reg.name).ToString() -eq $reg.type

    if (-not $same) {
        Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
    }
}

# Netzlaufwerke
foreach ($drive in $config.drives) {
    $local    = "$($drive.letter):"
    $existing = Get-SmbMapping -LocalPath $local -ErrorAction SilentlyContinue
    if ($existing) {
        # Beurteilen Sie nur anhand von "gibt es eine Zuordnung", bleibt beim Verschieben der Freigabe
        # und Ändern der Einstellung die alte Zuordnung bestehen und wird dennoch als "Erfolg" gemeldet.
        # Vergleichen Sie auch das Ziel
        if ($existing.RemotePath.TrimEnd('\') -eq $drive.path.TrimEnd('\')) { continue }
        Remove-SmbMapping -LocalPath $local -Force
    }
    New-SmbMapping -LocalPath $local -RemotePath $drive.path -Persistent $true
}

# Die Verbindung zu freigegebenen Druckern ist ebenfalls benutzerbezogen. Führen Sie es als
# Administrator oder als SYSTEM aus, entsteht die Verbindung nur für dieses Konto, unsichtbar für den Benutzer
foreach ($printer in $config.printers) {
    if (-not (Get-Printer -Name $printer.name -ErrorAction SilentlyContinue)) {
        Add-Printer -ConnectionName $printer.connection
    }
}

Die Verbindung zu einem freigegebenen Drucker (Add-Printer -ConnectionName) ist von derselben Art. Sie erstellt eine benutzerbezogene Verbindung, sodass sie, im Administrator-Skript oder in Intunes SYSTEM-Ausführung erledigt, für den sich anschließend anmeldenden Mitarbeiter unsichtbar bleibt. Möchten Sie, dass jeder Rechner sie besitzt, verwenden Sie entweder einen Mechanismus, der Drucker pro Rechner verteilt (etwa die Verteilung über Druckserverrichtlinien), oder führen Sie es in dieser nicht erhöhten Phase aus.

Aus demselben Grund gehören das Ablegen von Dateien unter dem Benutzerprofil, das Schreiben nach HKCU und das Erstellen von Benutzerverknüpfungen alle in diese nicht erhöhte Phase. „Rechnerweite Einstellungen einmal, als Administrator; benutzerbezogene Einstellungen bei jeder Anmeldung, nicht erhöht“ — diese zweistufige Struktur ist die Grundform eines Kitting-Skripts. Zu den Fallstricken von UNC-Pfaden und Laufwerkszuordnungen siehe auch „Fallstricke von Netzlaufwerken und UNC-Pfaden“.

Einige Designpunkte:

  • Trennen Sie Einstellungen, die eine Rechteerhöhung benötigen, von benutzerbezogenen Einstellungen. Wie oben führt eine Vermischung zu dem Zwischenfall „das von mir zugeordnete Laufwerk ist nicht sichtbar“
  • Externalisieren Sie die Einstellungen nach JSON. Unterschiede pro Abteilung oder pro Modell lassen sich ausdrücken, ohne das Skript zu ändern
  • Prüfen Sie vor jedem Vorgang den aktuellen Zustand. Das macht es sicher, beliebig oft auszuführen
  • Hinterlassen Sie mit Start-Transcript einen Nachweis. „Was auf diesem Rechner getan wurde“ lässt sich nachträglich nachvollziehen (siehe „Design von PowerShell-Ausgabeströmen und Protokollierung“)
  • Geben Sie einen Exit-Code zurück. So können Intune oder Ihr Bereitstellungswerkzeug Erfolg oder Fehlschlag beurteilen. Beachten Sie, dass bei Windows Installer 0 nicht der einzige Erfolgscode ist. Sowohl 3010 (ERROR_SUCCESS_REBOOT_REQUIRED, Neustart erforderlich) als auch 1641 (ERROR_SUCCESS_REBOOT_INITIATED, Neustart bereits eingeleitet) sind Erfolge.5 Lassen Sie diese in default fallen und behandeln sie als Fehlschlag, werden erfolgreiche Installationen im Bereitstellungswerkzeug rot vermerkt. Behandeln Sie sie als Erfolg, und der entscheidende Punkt ist, denselben Code am Ende an den Aufrufer zurückzugeben. Geben Sie 0 zurück, verliert das Bereitstellungswerkzeug jede Möglichkeit zu erkennen, dass ein Neustart nötig ist (siehe „PowerShell-Fehlerbehandlung und Retry-Design“)
  • Achten Sie auf die Anführung der Installer-Argumente. Start-Process -ArgumentList fügt das Array lediglich mit Leerzeichen zusammen, sodass Argumentgrenzen nicht erhalten bleiben. Setzen Sie entweder Pfade mit Leerzeichen auf Seiten der Konfigurationsdatei in Anführungszeichen, oder verwenden Sie ProcessStartInfo.ArgumentList (PowerShell 7) (siehe „Externe ausführbare Dateien aus PowerShell korrekt aufrufen“)

6. winget aus dem PowerShell-Modul verwenden

Die Verwendung des PowerShell-Moduls ist robuster, als die Kommandozeilenausgabe als Zeichenketten zu parsen. Microsoft.WinGet.Client stellt Cmdlets wie Find-WinGetPackage / Install-WinGetPackage / Get-WinGetPackage / Update-WinGetPackage bereit.4

Install-Module -Name Microsoft.WinGet.Client -Scope AllUsers -Force

# Vor der Installation prüfen, ob bereits installiert (idempotent)
foreach ($id in 'Google.Chrome', 'Microsoft.VisualStudioCode', '7zip.7zip') {
    if (Get-WinGetPackage -Id $id -ErrorAction SilentlyContinue) {
        Write-Verbose "Bereits installiert: $id"
        continue
    }
    Install-WinGetPackage -Id $id -Mode Silent -Scope System
}

Da Ergebnisse als Objekte zurückkommen, lassen sich Erfolgs-/Fehlschlagsprüfungen und der Abgleich von Listen direkt schreiben — das ist der Vorteil.

7. Vorsichtsmaßnahmen bei unbeaufsichtigter Ausführung und Ausführung im Systemkontext

Versuchen Sie, das Kitting vollständig zu automatisieren, stoßen Sie immer auf die Frage, „unter welchem Konto läuft das“.

  • Teile von winget setzen eine Ausführung im Benutzerkontext voraus. Die Ausführung im Systemkontext führt Microsoft weiterhin als zukünftiges Feature3
  • Trennen Sie die Verarbeitung, die eine Rechteerhöhung benötigt, von der benutzerbezogenen Verarbeitung. Eine Struktur, in der rechnerweite Einstellungen mit Administratorrechten laufen und Einstellungen unter dem Benutzerprofil bei der ersten Anmeldung, ist einfacher zu handhaben
  • Verifizieren Sie stets mit dem tatsächlichen Ausführungskonto. „Es funktionierte unter meinem eigenen Administratorkonto, aber nicht nach der Bereitstellung“ ist der häufigste Fehlschlag in diesem Bereich (siehe „Wenn Aufgaben der Aufgabenplanung nicht laufen“)

8. Praktische Faustregeln (Entscheidungstabelle)

Was zu tun ist Mittel Anmerkungen
Kommerzielle/Open-Source-Produkte installieren winget install / configure --silent --accept-* sind verpflichtend1
Konfiguration eines bestehenden Standardrechners extrahieren winget export Einstellungen sind nicht enthalten. Als Ausgangspunkt verwenden2
Anwendungen + Windows-Einstellungen, deklarativ winget configure (YAML) Win10 1809 oder höher + winget 1.6 oder höher3
Registrierung, Windows-Features, interne Anwendungen PowerShell (als Administrator) Zustand vor der Änderung prüfen (idempotent)
Freigegebene Laufwerke, freigegebene Drucker, benutzerbezogene Einstellungen PowerShell (bei Anmeldung, nicht erhöht) Erhöht oder als SYSTEM erstellt, sind sie für den Benutzer unsichtbar
Interne Fachanwendungen PowerShell + stiller Installer Ein dediziertes Repository aufzubauen ist im kleinen Maßstab überdimensioniert
Aus PowerShell heraus steuern Microsoft.WinGet.Client Keine Notwendigkeit, die Ausgabe als Zeichenketten zu parsen4
Unbeaufsichtigte Ausführung Mit dem Ausführungskonto verifizieren Der Systemkontext hat Einschränkungen3
Nachweise dessen, was getan wurde Start-Transcript + Exit-Code Einen Nachweis von “was mit diesem Rechner getan wurde” hinterlassen
Wenn ein Neustart erforderlich ist Exit-Code 3010 / 1641 zurückgeben Beide sind Erfolge. Geben Sie 0 zurück, kann das Bereitstellungswerkzeug den Neustart nicht erkennen5

9. Zusammenfassung

  • Ein Kitting-Handbuch lässt sich durch eine ausführbare Datei ersetzen. Eine realistische Aufteilung ist winget für die Anwendungsinstallation und PowerShell für alles Übrige.
  • Nehmen Sie bei winget install stets --silent sowie --accept-package-agreements und --accept-source-agreements mit auf. Vergessen Sie sie, stockt ein unbeaufsichtigter Lauf.
  • winget export kann die Konfiguration eines bestehenden Standardrechners extrahieren, Einstellungen und Anwendungen außerhalb der Verwaltung durch winget sind jedoch nicht enthalten.
  • Verwenden Sie WinGet Configuration (winget configure), können Sie Anwendungen und Einstellungen in eine einzige deklarative Datei legen, was Ihnen eine Konfiguration verschafft, die eine erneute Ausführung verträgt.
  • Schreiben Sie die PowerShell-Seite so, dass sie stets „vor der Änderung den aktuellen Zustand prüft“, um Idempotenz sicherzustellen.
  • Teilen Sie die Ausführungsphasen auf: rechnerweite Einstellungen mit Administratorrechten, und benutzerbezogene Einstellungen wie Netzlaufwerke und freigegebene Drucker bei der Anmeldung, nicht erhöht. In einer erhöhten Sitzung oder als SYSTEM erstellte Verbindungen sind für den Benutzer unsichtbar.
  • Bei unbeaufsichtigter Ausführung zählt die Verifikation des Ausführungskontos am meisten. Gestalten Sie unter der Annahme, dass die Ausführung von winget im Systemkontext Einschränkungen unterliegt.

Beispielcode herunterladen

Der in diesem Artikel behandelte Code wird als direkt lauffähiges Paket bereitgestellt. Er umfasst die Administratorphase, die Benutzerphase und ein vollständiges Beispiel der Konfigurationsdatei.

Beispielcode herunterladen (zip)

Da die Beispiele zu diesem Artikel von Windows und einem Mandanten abhängen, wurden sie nicht durch Ausführung verifiziert. Die Syntaxanalyse und die statische Analyse mit PSScriptAnalyzer wurden für jede Datei durchgeführt, prüfen Sie das Verhalten aber unbedingt auf Ihrem eigenen Testrechner.

# Syntaxanalyse + statische Analyse (läuft auch auf Nicht-Windows-Plattformen)
./Invoke-SampleTests.ps1

Die Konfigurationswerte (Pfade, Servernamen, Mandanten-IDs und Ähnliches) sind Beispiele. Führen Sie sie nicht unverändert gegen die Produktivumgebung aus — passen Sie sie an Ihre eigene Umgebung an.

Verwandte Artikel

Verwandte Beratungsleistungen

KomuraSoft LLC übernimmt PC-Kitting und die Automatisierung unternehmensweiter Standardumgebungen, macht Vorgänge, die nur in Handbüchern und individuellem Wissen existieren, ausführbar und unterstützt beim Design von Bereitstellungsskripten.

  1. Microsoft Learn, install command (winget). Zur Angabe des Ziels mit –id / -e, zur unbeaufsichtigten Installation mit –silent, zur Annahme der Nutzungsbedingungen mit –accept-package-agreements / –accept-source-agreements sowie zur Angabe des Installationsumfangs (Benutzer / Rechner) mit –scope. Auch zur Befehlsliste in Use WinGet to install and manage applications 2 3

  2. Microsoft Learn, export command (winget). Dazu, dass sich die Liste der installierten Pakete als JSON schreiben lässt, zur Aufzeichnung der Versionen mit –include-versions, zur Wiederherstellung mit dem import command und dem Verhalten von –ignore-unavailable sowie dazu, dass sich der Export auf von winget verwaltete Pakete beschränkt.  2 3

  3. Microsoft Learn, WinGet Configuration. Dazu, dass WinGet Configuration ein Mechanismus ist, der den gewünschten Zustand in YAML deklariert und ihn mit PowerShell DSC anwendet, dazu, dass es sich für unbeaufsichtigte Einrichtung eignet, zur Anforderung von Windows 10 Version 1809 (Build 17763) oder höher oder Windows 11 sowie WinGet v1.6.2631 oder höher, zur Behandlung von UAC bei Ausführung aus einer Administrator-Shell sowie dazu, dass die Ausführung im Systemkontext als zukünftiges Entwicklungsvorhaben geführt wird. Auch zu show und –accept-configuration-agreements im configure command 2 3 4 5 6 7

  4. GitHub, microsoft/winget-cli — Microsoft.WinGet.Client PowerShell module. Dazu, dass sich das Modul Microsoft.WinGet.Client aus der PowerShell Gallery installieren lässt, sowie dazu, dass es Cmdlets wie Find-WinGetPackage / Get-WinGetPackage / Install-WinGetPackage / Update-WinGetPackage / Uninstall-WinGetPackage bereitstellt, deren Ergebnisse sich als Objekte handhaben lassen.  2 3

  5. Microsoft Learn, Windows Installer Error Codes. Dazu, dass ERROR_SUCCESS_REBOOT_REQUIRED (3010) bedeutet, „ein Neustart ist erforderlich, damit die Änderungen wirksam werden; die Installation selbst war erfolgreich“, sowie dazu, dass ERROR_SUCCESS_REBOOT_INITIATED (1641) bedeutet, „der Installer hat einen Neustart eingeleitet; ein Code, der Erfolg anzeigt“.  2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Kann winget import das gesamte PC-Kitting automatisieren?
Es kann die Installation der Anwendungen automatisieren, das allein reicht aber nicht aus. Was winget import reproduziert, ist eine Paketliste; Einstellungen innerhalb der Anwendungen, das Hinzufügen von Druckern, das Zuordnen von Netzlaufwerken, Energieeinstellungen, per Registrierung angewendete unternehmensweite Standardeinstellungen und Ähnliches liegen außerhalb des Umfangs. Anwendungen, die auf anderem Weg als über winget installiert wurden, sowie interne Fachanwendungen sind ebenfalls nicht im Export enthalten. In der Praxis ist ein zweistufiges Vorgehen der realistische Ansatz: Die Anwendungsinstallation winget überlassen und die übrigen Einstellungen mit einem PowerShell-Skript ergänzen.
Was ist der Unterschied zwischen winget und WinGet Configuration (winget configure)?
wingets install/import sind prozedurale Anweisungen der Form „installiere dies, in dieser Reihenfolge“, während WinGet Configuration eine Möglichkeit ist, in einer YAML-Datei zu deklarieren, „das ist der Zustand, in dem ich am Ende sein möchte“. Intern wird PowerShell DSC verwendet, sodass eine einzelne Datei nicht nur die Anwendungsinstallation, sondern auch Windows-Einstellungen und Anwendungskonfiguration ausdrücken kann. Da nichts geschieht, wenn der gewünschte Zustand bereits vorliegt, können Sie bei einem Fehlschlag mitten im Vorgang einfach dieselbe Datei erneut ausführen — das macht es robust, wenn das Kitting wiederholt werden muss. Erforderlich sind Windows 10 1809 oder höher und winget 1.6 oder höher.
Ist es sicher, winget mit SYSTEM-Rechten aus der Aufgabenplanung oder Intune heraus auszuführen?
Hier ist Vorsicht geboten. Teile von winget setzen eine Ausführung im Benutzerkontext voraus, und die Ausführung im Systemkontext führt Microsoft weiterhin als zukünftiges Entwicklungsvorhaben. In der Praxis behilft man sich, indem man mit --scope machine für alle Benutzer installiert, über das PowerShell-Modul (Microsoft.WinGet.Client) geht oder es bei der ersten Anmeldung im Benutzerkontext ausführt. In jedem Fall sollten Sie unbedingt mit dem tatsächlich verwendeten Ausführungskonto verifizieren.
Sollte ein Kitting-Skript beliebig oft sicher ausführbar sein?
Ja. Betrachten Sie Idempotenz (dasselbe Ergebnis unabhängig davon, wie oft es läuft) als verpflichtend. Kitting scheitert routinemäßig mitten im Vorgang, und jedes Mal müssen Sie von vorn beginnen können. Schreiben Sie es so, dass die Ordnererstellung zuerst mit Test-Path das Vorhandensein prüft, Registrierungseinstellungen zuerst den aktuellen Wert prüfen und Anwendungsinstallationen zuerst prüfen, ob die Anwendung bereits installiert ist, können Sie an der fehlgeschlagenen Stelle fortsetzen. WinGet Configuration hat dieses Denken von Anfang an eingebaut.
Können wir interne Fachanwendungen über winget verteilen?
Das ist möglich, wenn Sie ein internes privates Repository (eine REST-API-Quelle) einrichten, das bedeutet jedoch, dafür einen Server aufzubauen und zu pflegen. Für nur eine Handvoll Anwendungen ist es einfacher, einen Installer aus einem freigegebenen Ordner mit einem PowerShell-Skript still auszuführen. Für kleine Organisationen ist die realistischste Aufteilung, kommerzielle und Open-Source-Produkte winget zu überlassen und interne Anwendungen mit PowerShell zu installieren.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog