How to configure your Windows Server 2025 with WinGet and Microsoft DSC: Azure Edition

How to configure your Windows Server 2025 with WinGet and Microsoft DSC: Azure Edition

Windows Server 2025 comes preinstalled with WinGet, reducing another prerequisite in the automation steps.

Or does it?

After trying everything under my local account, things looked pretty smooth. The process got fully automated for what I wanted to achieve:

  • Upgrade to the latest WinGet version.
  • Install Microsoft DSC.
  • Perform post-configuration, such as setting the time zone, and install PowerShell 7

But here's the kicker: when you want to automate the process on Azure fully, you should simply be able to use the "RunPowerShellScript" task, crank out a couple of lines of PowerShell code, and it should be good to go.

Unfortunately, this was not the case.

Reviewing my approach, I realized that a couple of workarounds were needed. And in this blog post, I'll share everything about it.

Prerequisites

If you feel strong about it and you want to follow along, you need:

  • An active Azure subscription
  • The Az PowerShell module installed (v15.0.0 or above)
  • Bicep installed (v0.38.4 or above)
  • Git installed

Creating the Azure VM

Before you can actually configure anything, you'll need an Azure VM. I didn't want to spend much time developing my own Bicep code, and that's why we'll be using the avm-bicep-labs repository to easily spawn up the required resources.

Follow the steps below to clone the repository, update the VM image to use Windows Server 2025, and deploy it using Azure PowerShell:

  1. Open a PowerShell terminal prompt and clone the repo:
git clone https://github.com/Azure-Samples/avm-bicep-labs.git
  1. Find the Windows Server 2025 marketplace SKU for your region (this example uses westeurope)
Get-AzVMImageSku -Location westeurope `
  -PublisherName MicrosoftWindowsServer `
  -Offer WindowsServer
  1. Edit the Bicep module lab02\childModules\workload.bicep and update the imageReference property under the VM resource:
imageReference: {
	publisher: 'MicrosoftWindowsServer'
	offer:    'WindowsServer'
	sku:      '<YOUR-SKU-HERE>'
	version:  'latest'
}
  1. Authenticate with Azure and deploy using Azure PowerShell:
Connect-AzAccount
New-AzDeployment -Location <location> `
  -TemplateFile lab02\main.bicep `
  -DeploymentName dscdemo
You will be prompted to fill in a password for the Azure VM. Store this for later.

The deployment takes roughly 10 minutes because of Azure Bastion. After the deployment finishes, go to the Azure Portal and locate your newly created VM. From there, you can connect using Azure Bastion.

Debunking WinGet installation

As mentioned in the introduction, WinGet comes preinstalled with Windows Server 2025. This only applies to local user accounts. Whenever you try to run WinGet under the System context, it cannot find it.

That begs the question, why do you want to run under the System context?

If you want to automate the process fully, you can leverage the Invoke-AzVMRunCommand command. But this command runs under the NT AUTHORITY\SYSTEM account, and there are a couple of issues with this:

  • WinGet needs to be reinstalled under this context
  • It requires dsc.exe installed and discoverable
  • PowerShell modules require preinstallation

Luckily, a couple of folks already figured out how to get WinGet working properly under this context. With some slight modification, it can be executed after the VM has been created:

  1. Create a new file named bootstrap.ps1
  2. Add the following code snippet, replace the placeholder values, and save the file:
$scriptBlock = {
    # Bootstrap package provider for NuGet
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

    # Install Visual C++ Redistributable and WinGet
    $downloadurl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe'
    $WebClient = New-Object System.Net.WebClient
    $WebClient.DownloadFile($downloadurl, "$env:Temp\vc_redist.x64.exe")
    $WebClient.Dispose()
    $Install = start-process "$env:temp\vc_redist.x64.exe" -argumentlist "/q /norestart" -Wait -PassThru
    $7zipFolder = "${env:WinDir}\Temp\7zip"
    New-Item -ItemType Directory -Path "${env:WinDir}\Temp\WinGet-Stage" -Force
    Invoke-WebRequest -UseBasicParsing -Uri https://aka.ms/getwinget -OutFile "${env:WinDir}\Temp\WinGet-Stage\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
    New-Item -ItemType Directory -Path $7zipFolder -Force
    Invoke-WebRequest -UseBasicParsing -Uri https://www.7-zip.org/a/7zr.exe -OutFile "$7zipFolder\7zr.exe"
    Invoke-WebRequest -UseBasicParsing -Uri https://www.7-zip.org/a/7z2408-extra.7z -OutFile "$7zipFolder\7zr-extra.7z"
    & "$7zipFolder\7zr.exe" x "$7zipFolder\7zr-extra.7z" -o"$7zipFolder" -y
    New-Item -ItemType Directory -Path "${env:ProgramData}\Microsoft.DesktopAppInstaller" -Force
    & "$7zipFolder\7za.exe" x "${env:WinDir}\Temp\WinGet-Stage\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -o"${env:WinDir}\Temp\WinGet-Stage" -y
    & "$7zipFolder\7za.exe" x "${env:WinDir}\Temp\WinGet-Stage\AppInstaller_x64.msix" -o"${env:ProgramData}\Microsoft.DesktopAppInstaller" -y

    # Add to current session PATH
    $env:Path += [System.IO.Path]::PathSeparator + "${env:ProgramData}\Microsoft.DesktopAppInstaller"

    # Install Microsoft DSC
    $uri = 'https://github.com/PowerShell/DSC/releases/download/v3.2.0-preview.10/DSC-3.2.0-preview.10-x86_64-pc-windows-msvc.zip'
    $tmpDir = [System.IO.Path]::GetTempPath()
    $zipPath = Join-Path $tmpDir 'DSC-3.2.0-preview.10-x86_64-pc-windows-msvc.zip'
    $installPath = Join-Path $env:LOCALAPPDATA 'dsc'
    (New-Object Net.WebClient).DownloadFile($uri, $zipPath)
    Expand-Archive -LiteralPath $zipPath -DestinationPath $installPath -Force
    # Add to current session PATH
    $env:PATH += [System.IO.Path]::PathSeparator + $installPath
    # Unblock extracted files
    Get-ChildItem -Path $installPath -Recurse | Unblock-File

    # Create a WinGet DSC configuration file
    $string = @'
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
metadata:
  winget:
    processor: dscv3
resources:
  - name: Install PowerShell
    type: Microsoft.WinGet/Package
    properties:
      id: Microsoft.PowerShell
      source: winget
  - name: Time zone
    type: ComputerManagementDSC/TimeZone
    properties:
      IsSingleInstance: "Yes"
      TimeZone: "W. Europe Standard Time"
'@

    $configPath = Join-Path -Path $env:TEMP -ChildPath 'configuration.winget'
    Set-Content -Value $string -Path $configPath

    # Install PowerShell modules
    Install-Module -Name ComputerManagementDSC -Repository PSGallery -Scope AllUsers -Force

    # Execute WinGet
    winget configure --file $configPath --processor-path (Join-Path $installPath 'dsc.exe') --accept-configuration-agreements --suppress-initial-details
}

$script = [scriptblock]::create($scriptBlock)
Invoke-AzVMRunCommand -ResourceGroupName '<resourceGroupName>' -VMName '<vmName>' -CommandId 'RunPowerShellScript' -ScriptString $script

  1. Open your PowerShell terminal and run the script: .\bootstrap.ps1

What this script does:

  • It bootstraps the NuGet package provider for PowerShell
  • Downloads and installs Visual C++ Redistributable and WinGet
  • Installs Microsoft DSC
  • Installs required PowerShell modules
  • Uses the --processor-path to detect dsc.exe
  • Execute winget configure to appy the configuration document

The script takes a little while, but after it is completed, notice the time zone changed:

Figure 1: Time zone set to West Europe

If you look at the installed software, you notice PowerShell is successfully installed:

Figure 2: PowerShell installed on Azure VM

Summary

Whilst WinGet comes preinstalled with Windows Server 2025, you just learned the nitty-gritty things to get it working under a different context. This becomes useful when you want to fully automate the process after the VM is created.

More post-configuration steps can be added in the bootstrap script. But don't forget to install the required PowerShell modules if you're using different ones.

After you are done, don't forget to clean up the resources:

Remove-AzResourceGroup -Name "<workloadResourceGroup>" -ForceDeletionType "Microsoft.Compute/virtualMachines"
Remove-AzResourceGroup -Name "<resourceGroup>