winget + PowerShellでPCキッティングを自動化する ── 手順書を実行可能にする

· · winget, PowerShell, Windows, キッティング, 情報システム, 自動化, 運用改善, 業務効率化

更新履歴(初版のみ・2026年07月25日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547466)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「winget + PowerShellでPCキッティングを自動化する ── 手順書を実行可能にする」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547466

DOI(最新版)
10.5281/zenodo.21547466
DOI(この版)
10.5281/zenodo.21547467

新入社員が入るたび、あるいはPCを入れ替えるたびに、担当者が手順書を見ながら1台ずつ設定していく ── 中小企業の情シスでは、いまだにこれが標準的な光景です。問題は時間だけではありません。手作業は再現性がないため、「この端末だけ設定が違う」というトラブルが後から効いてきます。手順書は更新されないまま古くなり、担当者が変わると細部が失われます。

Windowsには標準でパッケージマネージャー winget が入っており、アプリの導入は1行で書けます。さらにWinGet Configurationを使えば、アプリと設定を宣言的なYAMLファイル1つで表現できます。そしてwingetがカバーしない領域(プリンター、ネットワークドライブ、社内標準のレジストリ設定など)は、PowerShellで補えます。

この記事では、キッティング手順書を「実行可能なファイル」に置き換える方法を、実際に運用できる粒度でまとめます。

1. まず結論

  • アプリ導入はwingetに任せます。winget install は静かなインストールを標準でサポートします。1
  • 既存PCの構成は winget export で吸い出せます。ただしwinget管理下のパッケージに限られ、設定は含まれません。2
  • 宣言的にやるならWinGet Configuration(winget configure)です。PowerShell DSCベースで、アプリと設定を1つのYAMLに書けます。Windows 10 1809以降 + winget 1.6以降が要件です。3
  • wingetで足りない部分はPowerShellで補います。プリンター、共有ドライブ、レジストリ、Windows機能、ローカルアカウントなどです。
  • PowerShellから扱うなら Microsoft.WinGet.Client モジュールがあります。Install-WinGetPackage などのコマンドレットが使えます。4
  • システムコンテキストでの実行は要注意です。Microsoftも今後の開発項目として挙げている段階で、実際の実行アカウントでの検証が必須です。3
  • 冪等性を必ず確保します。キッティングは途中で失敗するもので、何度でも再実行できる必要があります。
  • 社内独自アプリは無理にwingetに載せず、PowerShellでサイレント実行するのが現実的です。

2. wingetの基本 ── 無人インストールの書き方

まず、対話なしで確実に入れるための定型を押さえます。1

# IDを正確に指定して入れる(-e は完全一致、--id はID指定)
#   --silent                     : UIを出さない
#   --accept-package-agreements  : パッケージの使用条件に同意
#   --accept-source-agreements   : ソースの使用条件に同意
#   --scope machine              : 全ユーザー向けに導入(対応パッケージのみ)
# 行継続のバッククォートは行末に置く。後ろにコメントを書くと継続にならない
winget install --id Google.Chrome -e --silent `
    --accept-package-agreements --accept-source-agreements --scope machine

# IDを調べる
winget search "Visual Studio Code"
winget show --id Microsoft.VisualStudioCode

--accept-* を付け忘れると、無人実行が同意待ちで止まります。これがキッティング自動化で最初につまずく点です。

--scope machine はすべてのパッケージで使えるわけではなく、ユーザー単位でしか導入できないアプリもあります。その場合は初回ログオン時にユーザーコンテキストで実行する構成にします。

3. 現行PCの構成を吸い出す ── export / import

すでに整備済みの「標準PC」がある場合、その構成をファイル化できます。2

# 標準PCから、導入済みパッケージの一覧をJSONに書き出す
winget export --output D:\kitting\apps.json --include-versions

# 新しいPCで復元する
winget import --import-file D:\kitting\apps.json `
    --accept-package-agreements --accept-source-agreements --ignore-unavailable

制約を理解しておくことが重要です。

できること できないこと
winget管理下のパッケージ一覧の再現 アプリ内の設定の移行
バージョンの固定(--include-versions) winget以外で入れたアプリ・社内独自アプリ
存在しないパッケージのスキップ(--ignore-unavailable) ライセンス認証、サインイン状態

つまり winget import出発点であって、キッティングの全体ではありません。残りをどう埋めるかが本題です。

4. 宣言的に書く ── WinGet Configuration

winget configure は、「最終的にこうあってほしい」状態をYAMLで宣言し、PowerShell DSCを通じて適用する仕組みです。3 手続き的なスクリプトと比べた利点は3つあります。

  • 既に望ましい状態なら何もしない(冪等)
  • アプリの導入と、Windows・アプリの設定を1ファイルで表現できる
  • 途中で失敗しても、同じファイルを再実行すればよい
# kitting.winget ── 標準端末の望ましい状態を宣言する
properties:
  configurationVersion: 0.2.0
  resources:
    - resource: Microsoft.WinGet.DSC/WinGetPackage
      id: chrome
      directives:
        description: Google Chrome を導入する
        allowPrerelease: true
      settings:
        id: Google.Chrome
        source: winget

    - resource: Microsoft.WinGet.DSC/WinGetPackage
      id: vscode
      directives:
        description: Visual Studio Code を導入する
      settings:
        id: Microsoft.VisualStudioCode
        source: winget

    - resource: Microsoft.Windows.Developer/DeveloperMode
      id: devmode
      directives:
        description: 開発者モードを有効にする(開発端末のみ)
        allowPrerelease: true
      settings:
        Ensure: Present
# 適用前に内容を確認する(何が実行されるかを表示)
winget configure show --file D:\kitting\kitting.winget

# 適用する
winget configure --file D:\kitting\kitting.winget --accept-configuration-agreements

要件はWindows 10 バージョン1809(ビルド17763)以降またはWindows 11、winget 1.6.2631以降です。3 古い端末が残っている環境では、次章のPowerShellによる構成のほうが確実です。

5. PowerShellで補う ── wingetがやらない部分

実務のキッティングで手間の大半を占めるのは、実はアプリ導入以外です。ここはPowerShellの出番になります。すべて「実行済みなら何もしない」形で書くのがポイントです。

このスクリプトは kitting.config.json を読み込みます。設定ファイルの完全な例は、この記事のサンプルコード(記事末尾のzip)に kitting.config.json として同梱していますが、構造だけ先に示しておきます。

{
  "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 業務システム クライアント",
      "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" } ]
}

registryuserRegistry を分けているのは、適用先が違うからです。エクスプローラーの「拡張子を表示する」(HideFileExt)のような設定は、HKLM のポリシーではなく利用者ごとの HKCU を見ます。管理者フェーズで HKLM に書いても表示は変わりません。こうしたユーザー単位の設定は userRegistry に入れ、後述の非昇格フェーズで適用します。

internalAppsdisplayName は「プログラムと機能」に表示される名前と完全に一致させてください。version を指定すると、その版以上が入っているかまで確認します(省略時は名前の一致だけで判定します)。

アプリ導入そのものは前章の winget import や WinGet Configuration でも行えますが、このスクリプトにも wingetPackages の導入を含めています。設定ファイルを1つにまとめておけば、「この端末に何を入れるか」の定義が一箇所に収まり、実行も1回で済むためです。逆に、設定ファイルに書いた項目をスクリプトが読まないまま放置すると、入っていない端末を「キッティング成功」と記録してしまいます。

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

$ErrorActionPreference = 'Stop'
# 5.1の Get-Content はBOMがなければANSIコードページで読む。
# UTF-8のJSONを日本語環境で読むと化けるため、明示する
$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

# 後述のユーザー単位フェーズは別プロセスで動くため、設定を
# 全ユーザーが読める場所へ配っておく。
# 配布物をこの場所に置いてそのまま実行すると、コピー元と先が同じファイルになる。
# Copy-Item は自分自身へのコピーをエラーにするため、$ErrorActionPreference = 'Stop'
# の下ではキッティングが始まる前にスクリプトごと止まってしまう
$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. 標準フォルダー ------------------------------------------------
    foreach ($dir in $config.folders) {
        if (-not (Test-Path $dir)) {
            $null = New-Item -Path $dir -ItemType Directory
            Write-Verbose "作成: $dir"
        }
    }

    # --- 2. 社内標準のレジストリ設定 --------------------------------------
    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

        # 値だけでなく「型」も比較する。REG_SZ の "1" と DWORD の 1 は
        # PowerShellの比較では等しくなってしまい、実際には効いていない設定を
        # 「適用済み」と誤判定する
        $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 "設定: $($reg.key)\$($reg.name) = $($reg.value) ($($reg.type))"
        }
    }

    # --- 3. Windowsの機能 --------------------------------------------------
    foreach ($feature in $config.windowsFeatures) {
        $state = Get-WindowsOptionalFeature -Online -FeatureName $feature
        if ($state.State -ne 'Enabled') {
            # -NoRestart で再起動を抑止した場合、要否は戻り値のRestartNeededに出る。
            # 拾っておかないと「再起動が必要なのに0で終了」してしまう
            $result = Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart
            if ($result.RestartNeeded) { $rebootRequired = $true }
        }
    }

    # --- 4. wingetパッケージ ----------------------------------------------
    # winget の終了コードは「既に導入済み」でも 0 以外になることがあり、
    # 逆に 0 でも実際には入っていない場合がある。成否は状態の照会で判断する。
    # --scope を付けないと、管理者自身のユーザー単位の導入を拾って
    # 「machine で入っている」と誤判定する
    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 }

        # 行継続のバッククォートは行末に置く。後ろにコメントを書くと継続にならない
        winget install --id $package.id -e --silent `
            --accept-package-agreements --accept-source-agreements `
            --scope $package.scope
        $wingetExit = $LASTEXITCODE

        # ここで警告だけ出して先に進むと、必須アプリが入っていない端末を
        # 配布ツールが「キッティング成功」と記録してしまう
        if (-not (Test-KsWingetPackage -Id $package.id -Scope $package.scope)) {
            throw "$($package.id) の導入に失敗しました (winget ExitCode=$wingetExit)"
        }
    }

    # --- 5. 社内アプリ(共有フォルダーのインストーラーをサイレント実行) ----
    # 導入済み一覧は、32bit/64bitの両ビューを明示して開く。
    # 32bitのPowerShellが64bit Windows上で動くと(Intuneの構成によっては起こる)、
    # WOW64のリダイレクトで HKLM:\SOFTWARE\... が32bitビューを指し、
    # 64bitのアプリを「未導入」と誤判定して毎回入れ直してしまう
    $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

        # DisplayNameの一致だけで「導入済み」と判断しない。古い版が入ったままの端末に
        # 新しい版が入らず、再実行しても設定ファイルの状態に収束しなくなる。
        # 設定に version があれば、その版以上が入っているかまで見る
        $upToDate = if ($app.version) {
            $wanted = [version] $app.version
            [bool]($installed | Where-Object {
                $cur = $_.DisplayVersion -as [version]   # 版形式でない表記は対象外
                $cur -and $cur -ge $wanted
            })
        } else {
            [bool] $installed
        }
        if ($upToDate) { continue }

        # Start-Process の -ArgumentList は配列を空白で連結して1本のコマンドラインにするため、
        # 空白を含む値は設定ファイル側で引用符を付けておく
        # 例: '/quiet /norestart INSTALLDIR="C:\Program Files\KsApp"'
        # .msi は実行ファイルではないため、そのまま Start-Process に渡すと
        # 「有効なアプリケーションではありません」で失敗する。msiexec 経由で起動する
        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
        }
        # Windows Installerの終了コードは「0だけが成功」ではない。
        # 3010と1641はどちらも成功で、再起動の扱いだけが違う
        switch ($proc.ExitCode) {
            0    { }                                   # 成功
            3010 { $rebootRequired = $true }           # 成功。要再起動(ERROR_SUCCESS_REBOOT_REQUIRED)
            1641 { $rebootInitiated = $true }          # 成功。インストーラーが再起動を開始した
            default {
                throw "$($app.displayName) のインストールに失敗しました (ExitCode=$($proc.ExitCode))"
            }
        }
        # 再起動が始まっているなら、後続のインストールは走らせても中断される
        if ($rebootInitiated) { break }
    }

    if ($rebootInitiated) {
        # 1641は「成功。ただし再起動を開始済み」。0を返すと配布ツールは
        # 「完了したのに勝手に再起動した」と扱うため、そのまま伝える
        Write-Host 'インストーラーが再起動を開始しました。再起動後に再実行してください' -ForegroundColor Yellow
        exit 1641
    }

    if ($rebootRequired) {
        # 3010をそのまま返すと、Intuneや配布ツール側が「成功。要再起動」と解釈し、
        # 再起動のスケジュールと報告を行える。ここで0を返すと再起動が忘れられる
        Write-Host 'キッティング完了(再起動が必要)' -ForegroundColor Yellow
        exit 3010
    }

    Write-Host 'キッティング完了' -ForegroundColor Green
    exit 0
}
catch {
    Write-Warning "失敗: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1
}
finally {
    Stop-Transcript
}

この管理者スクリプトからネットワークドライブの割り当てを意図的に外している点に注意してください。ドライブレターの割り当てはログオンセッション単位の設定なので、管理者に昇格したセッションで作成しても、UACの下では利用者の通常のエクスプローラーからは見えません。Intuneなどからシステム権限で実行した場合は、そもそもSYSTEMのセッションに割り当てられてしまい、利用者には無関係です。ユーザー固有の設定は、そのユーザーのログオン時に非昇格で実行するのが正解です。

# ユーザー単位の設定(ログオンスクリプト、またはHKCUのRunから非昇格で実行する)

# 管理者スクリプトとは別プロセスなので、$config は引き継がれない。
# 管理者フェーズで ProgramData へ配っておいた設定を読み直す
$configPath = 'C:\ProgramData\KsKitting\kitting.config.json'
if (-not (Test-Path $configPath)) {
    Write-Warning "設定ファイルが見つかりません: $configPath"
    exit 1
}
$config = Get-Content $configPath -Raw -Encoding utf8 | ConvertFrom-Json

# ユーザー単位のレジストリ設定(HideFileExt など。HKLMに書いても効かない)
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
    }
}

# ネットワークドライブ
foreach ($drive in $config.drives) {
    $local    = "$($drive.letter):"
    $existing = Get-SmbMapping -LocalPath $local -ErrorAction SilentlyContinue
    if ($existing) {
        # 「割り当てがあるか」だけで判断すると、共有の移転などで設定を変えても
        # 古い割り当てが残ったまま「成功」と報告されてしまう。接続先まで比較する
        if ($existing.RemotePath.TrimEnd('\') -eq $drive.path.TrimEnd('\')) { continue }
        Remove-SmbMapping -LocalPath $local -Force
    }
    New-SmbMapping -LocalPath $local -RemotePath $drive.path -Persistent $true
}

# 共有プリンターへの接続もユーザー単位。管理者やSYSTEMで実行すると、
# そのアカウントにだけ接続が作られ、利用者からは見えない
foreach ($printer in $config.printers) {
    if (-not (Get-Printer -Name $printer.name -ErrorAction SilentlyContinue)) {
        Add-Printer -ConnectionName $printer.connection
    }
}

共有プリンターへの接続(Add-Printer -ConnectionName)も同じ扱いです。これはユーザーごとの接続を作る操作なので、管理者スクリプトやIntuneのSYSTEM実行で行っても、後からログオンする従業員には見えません。全端末共通で持たせたい場合は、プリンターをマシン単位で展開する仕組み(印刷サーバーのポリシー配布など)を使うか、この非昇格フェーズで実行してください。

同じ理由で、ユーザープロファイル配下のファイル配置、HKCUへの書き込み、ユーザー向けショートカットの作成も、この非昇格フェーズにまとめます。「マシン全体の設定は管理者で1回、ユーザー固有の設定はログオンごとに非昇格で」という二段構えが、キッティングスクリプトの基本形です。UNCパスとドライブ割り当ての注意点は「ネットワーク共有・UNCパスの落とし穴」も参照してください。

設計上のポイントを挙げます。

  • 昇格が必要な設定と、ユーザー単位の設定を分ける。上記のとおり、混ぜると「割り当てたはずのドライブが見えない」事故になります
  • 設定はJSONに外出しする。部署ごと・機種ごとの差分を、スクリプトを変えずに表現できます
  • 各処理の前に現在の状態を確認する。これで何度実行しても安全になります
  • Start-Transcript で証跡を残す。「この端末で何をしたか」が後から追えます(「PowerShellの出力ストリームとログ設計」)
  • 終了コードを返す。Intuneや配布ツールから成否を判定できます。Windows Installerでは0だけが成功ではない点に注意してください。3010(ERROR_SUCCESS_REBOOT_REQUIRED、要再起動)と1641(ERROR_SUCCESS_REBOOT_INITIATED、再起動を開始済み)はいずれも成功です。5 これらを default に落として失敗扱いにすると、成功したインストールが配布ツール上では赤く記録されます。成功扱いにしたうえで、最後にそのコードのまま呼び出し元へ返すのがポイントです。0で返すと、配布ツール側が再起動の必要を知る手段を失います(「PowerShellのエラー処理と再実行設計」)
  • インストーラーの引数の引用符に注意する。Start-Process -ArgumentList は配列を空白で連結するだけなので、引数の区切りは保持されません。空白を含むパスは設定ファイル側で引用符付きにするか、ProcessStartInfo.ArgumentList(PowerShell 7)を使ってください(「PowerShellから外部exeを正しく呼ぶ」)

6. PowerShellモジュールからwingetを使う

コマンドラインの出力を文字列で解析するより、PowerShellモジュールを使うほうが堅牢です。Microsoft.WinGet.Client には Find-WinGetPackage / Install-WinGetPackage / Get-WinGetPackage / Update-WinGetPackage などのコマンドレットがあります。4

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

# 導入済みかを確認してから入れる(冪等)
foreach ($id in 'Google.Chrome', 'Microsoft.VisualStudioCode', '7zip.7zip') {
    if (Get-WinGetPackage -Id $id -ErrorAction SilentlyContinue) {
        Write-Verbose "導入済み: $id"
        continue
    }
    Install-WinGetPackage -Id $id -Mode Silent -Scope System
}

オブジェクトとして結果が返るため、成否判定や一覧の突合がそのまま書けるのが利点です。

7. 無人実行・システムコンテキストの注意

キッティングを完全自動化しようとすると、必ず「どのアカウントで実行するか」の問題にぶつかります。

  • wingetはユーザーコンテキストでの実行を前提とした部分があります。システムコンテキストでの実行は、Microsoftが今後の機能として挙げている段階です3
  • 昇格が必要な処理と、ユーザー固有の処理は分けます。マシン全体の設定は管理者権限で、ユーザープロファイル配下の設定は初回ログオン時に実行する構成が扱いやすくなります
  • 必ず実際の実行アカウントで検証してください。「手元の管理者アカウントでは動いたのに、配布したら動かない」は、この分野で最も多い失敗です(「タスクスケジューラのタスクが実行されない」)

8. 実務の定石(判断表)

やること 手段 補足
市販・OSS製品の導入 winget install / configure --silent --accept-* は必須1
既存標準機の構成の吸い出し winget export 設定は含まれない。出発点として使う2
アプリ + Windows設定を宣言的に winget configure(YAML) Win10 1809以降 + winget 1.6以降3
レジストリ・Windows機能・社内アプリ PowerShell(管理者) 状態確認してから変更(冪等)
共有ドライブ・共有プリンター・ユーザー固有の設定 PowerShell(ログオン時・非昇格) 昇格・SYSTEMで作ると利用者から見えない
社内独自アプリ PowerShell + サイレントインストーラー 専用リポジトリ構築は小規模には過剰
PowerShellから制御したい Microsoft.WinGet.Client 出力の文字列解析が不要になる4
無人実行 実行アカウントで検証 システムコンテキストは制約あり3
実施記録 Start-Transcript + 終了コード 「この端末に何をしたか」を残す
再起動が必要な場合 終了コード3010 / 1641を返す どちらも成功。0で返すと配布ツールが再起動を認識できない5

9. まとめ

  • キッティング手順書は、実行可能なファイルに置き換えられます。アプリ導入はwinget、それ以外の設定はPowerShellという分担が現実的です。
  • winget install では --silent--accept-package-agreements --accept-source-agreements を必ず付けます。付け忘れると無人実行が止まります。
  • winget export は既存の標準機から構成を吸い出せますが、設定やwinget管理外のアプリは含まれません。
  • WinGet Configuration(winget configure)を使えば、アプリと設定を宣言的な1ファイルにまとめられ、再実行に強い構成になります。
  • PowerShell側の処理は、必ず「現在の状態を確認してから変更する」書き方にして冪等性を確保してください。
  • マシン全体の設定は管理者権限で、ネットワークドライブや共有プリンターなどユーザー固有の設定はログオン時に非昇格で、と実行フェーズを分けてください。昇格セッションやSYSTEMで作った接続は利用者に見えません。
  • 無人実行では実行アカウントの検証が最重要です。システムコンテキストでのwinget実行には制約があることを前提に設計してください。

サンプルコードのダウンロード

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。管理者フェーズ・ユーザーフェーズ・設定ファイルの完全な例が入っています。

サンプルコードをダウンロード(zip)

この記事のサンプルは、Windowsやテナントに依存するため実行検証はしていません。構文解析とPSScriptAnalyzerによる静的解析までは全ファイルに対して実施していますが、動作は必ずご自身の検証機で確認してください。

# 構文解析 + 静的解析(Windows以外でも実行できる)
./Invoke-SampleTests.ps1

設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、PCキッティングや社内標準環境の自動化、手順書として属人化している運用の実行可能化、配布スクリプトの設計支援を扱っています。

参考リンク

  1. Microsoft Learn, install コマンド (winget). –id / -e による対象の指定、–silentによる無人インストール、–accept-package-agreements / –accept-source-agreements による使用条件への同意、–scopeによるインストール範囲(user / machine)の指定について。あわせてUse WinGet to install and manage applicationsのコマンド一覧について。  2 3

  2. Microsoft Learn, export コマンド (winget). インストール済みパッケージの一覧をJSONへ書き出せること、–include-versionsによるバージョンの記録、import コマンドによる復元と–ignore-unavailableの動作、エクスポートの対象がwinget管理下のパッケージに限られることについて。  2 3

  3. Microsoft Learn, WinGet Configuration. WinGet ConfigurationがYAMLで望ましい状態を宣言しPowerShell DSCを用いて適用する仕組みであること、無人セットアップに利用できること、Windows 10 バージョン1809(ビルド17763)以降またはWindows 11とWinGet v1.6.2631以降が必要であること、管理者シェルから実行した場合のUACの扱い、システムコンテキストでの実行が今後の開発項目として挙げられていることについて。あわせてconfigure コマンドのshow / –accept-configuration-agreementsについて。  2 3 4 5 6 7

  4. GitHub, microsoft/winget-cli ─ Microsoft.WinGet.Client PowerShell モジュール. PowerShell GalleryからMicrosoft.WinGet.Clientモジュールを導入できること、Find-WinGetPackage / Get-WinGetPackage / Install-WinGetPackage / Update-WinGetPackage / Uninstall-WinGetPackage などのコマンドレットが提供され、結果をオブジェクトとして扱えることについて。  2 3

  5. Microsoft Learn, Windows インストーラーのエラー コード. ERROR_SUCCESS_REBOOT_REQUIRED(3010)が「変更を有効にするには再起動が必要。インストール自体は成功」であること、ERROR_SUCCESS_REBOOT_INITIATED(1641)が「インストーラーが再起動を開始した。成功を示すコード」であることについて。  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

winget importで、キッティングは全部自動化できますか?
アプリのインストールまでは自動化できますが、それだけでは足りません。winget importが再現するのはパッケージの一覧であり、アプリ内の設定、プリンターの追加、ネットワークドライブの割り当て、電源設定、レジストリによる社内標準設定などは対象外です。また、winget以外の方法で入れたアプリや、社内独自の業務アプリはエクスポートに含まれません。実務では、アプリ導入をwingetに任せ、残りの設定をPowerShellスクリプトで補う二段構えが現実的です。
wingetとWinGet Configuration(winget configure)はどう違いますか?
wingetのinstall/importは「この順番でこれを入れる」という手続き的な指示ですが、WinGet Configurationは「最終的にこの状態であってほしい」という宣言をYAMLファイルに書く方式です。内部でPowerShell DSCを使い、アプリの導入だけでなくWindowsの設定やアプリの構成まで1つのファイルで表現できます。すでに望ましい状態なら何もしないため、途中で失敗しても同じファイルを再実行すればよく、キッティングのやり直しに強いのが利点です。Windows 10 1809以降とwinget 1.6以降が必要です。
タスクスケジューラやIntuneからSYSTEM権限でwingetを実行しても大丈夫ですか?
注意が必要です。wingetはユーザーコンテキストでの実行を前提とした部分があり、システムコンテキストでの実行はMicrosoftが今後の開発項目として挙げている段階です。実務では、全ユーザー向けにインストールする--scope machineを使う、PowerShellモジュール(Microsoft.WinGet.Client)経由で実行する、あるいは初回ログオン時にユーザーコンテキストで走らせるといった回避策を取ります。いずれにしても、実際に使う実行アカウントで必ず検証してください。
キッティングスクリプトは何度実行しても安全であるべきですか?
はい。冪等性(何度実行しても同じ結果になること)は必須と考えてください。キッティングは途中で失敗することが日常的にあり、そのたびに最初からやり直せる必要があります。フォルダー作成はTest-Pathで存在確認してから、レジストリ設定は現在値を確認してから、アプリ導入は導入済みかを確認してから実行する、という書き方にしておけば、失敗した箇所から再開できます。WinGet Configurationはこの考え方が最初から組み込まれています。
社内独自の業務アプリはwingetで配れますか?
社内向けのプライベートリポジトリ(REST APIのソース)を用意すれば可能ですが、そのためのサーバーを構築・維持する手間があります。数本のアプリのためであれば、PowerShellスクリプトから共有フォルダー上のインストーラーをサイレント実行するほうが簡単です。市販・OSS製品はwingetに任せ、社内アプリはPowerShellで入れる、という切り分けが小規模組織では最も現実的です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る