Stop wrestling with PowerShell module installation: The DSC resource that makes Module Management actually work

How to declaratively define PowerShell module management with DSC resources

Stop wrestling with PowerShell module installation: The DSC resource that makes Module Management actually work

I used to dread onboarding new developers.

"Just run this script," I'd say, handing over a 200-line PowerShell script mess that installed modules. Sometimes it worked. Usually, it didn't. Wrong versions installed. Dependencies conflicted. Four hours later, we'd still be troubleshooting why their environment didn't match the way mine did.

The worst part? I knew it was my fault. I'd tried everything. Documenting it in wikis, using external tools, and providing instructions on Teams. Nothing lasted, and environments kept drifting. I spent more time firefighting module issues than actually building things.

Then, during the DSC work group meeting I'm part of, it clicked.

While the problem wasn't in the tools, it was the dependency on them or the way I was writing it.

Enter the Microsoft.PowerShell.PSResourceGet DSC resource. Declarative module management that actually works. No more "works on my machine." No more version drift. No more setup scripts that break every month.

Adityha developed this unreleased resource with functionality planned for integration into Microsoft.PowerShell.PSResourceGet v1.2.0. And it's about to change how you manage PowerShell modules.

Here's what I learned down the road.

The module version game

Here's the scenario: Your deployment scripts need SqlServer module version 21.1.18256. Not 22.x, as it broke authentication behaviour. Pester needs to be 5.x because your tests don't work with 4.x. And PSScriptAnalyzer? Any recent version works, but it needs to be installed.

Now multiply this across:

  • Your local machine where you develop
  • Three Windows servers running the deployment script
  • Four build agents running separate jobs to test your script
  • Any other developer's laptop on your team

How do you guarantee everyone has the correct versions?

Most teams write a setup script. Something like this:

if (-not (Get-Module -ListAvailable -Name Az.Compute -RequiredVersion 5.7.0)) {
    Install-PSResource -Name Az.Compute -Version 5.7.0
}
# ...repeat 20 more times

Others might fall back on the PSDepend module. But that module still needs to be bootstrapped first.

So, it still works until it doesn't. Someone runs it twice and gets different results. A build agent has an old version cached. A developer manually installs something, breaking the pattern.

You return to the earlier point: document it, store it in a Wiki, and post it on Teams. That doesn't really stick.

There's a better way: Stop writing installation instructions. Start declaring requirements.

How PSResourceGet DSC resource work

First, if you want to try this out, you've to grab the resource from GitHub and store all three files in your dsc.exe file location.

Once you have the resource available, instead of imperative installation scripts, you declare your desired state:

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
  - name: Required PowerShell Modules
    type: Microsoft.PowerShell.PSResourceGet/PSResourceList
    properties:
      repositoryName: PSGallery
      resources: 
        - name: Pester
          version: '[[5.0.0,6.0.0)'     # Exactly this version
        - name: PSScriptAnalyzer
          version: '[[>=1.21.0]'         # 1.21.0 or newer
        - name: SqlServer
          version: '21.1.18256'          # Exactly this version
ℹ️
If you're getting an error that the schema doesn't match the version, update the version pattern in the resource manifest with:
"pattern": "^((\\[|\\()[ \\t]*(>=|>|<=|<)?[ \\t]*\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?[ \\t]*(,[ \\t]*(>=|>|<=|<)?[ \\t]*(\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)?[ \\t]*)?(\\]|\\))|\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)$"

Save this as module.dsc.yaml and run:

dsc config get --file module.dsc.yaml

DSC will fetch the current state, and if none are present, it returns:

results:
- metadata:
    Microsoft.DSC:
      duration: PT9.5483312S
  name: Required PowerShell Modules
  type: Microsoft.PowerShell.PSResourceGet/PSResourceList
  result:
    actualState:
      repositoryName: PSGallery
      resources:
      - name: Pester
        version: null
        scope: 0
        repositoryName: null
        preRelease: false
        _exist: false
      - name: PSScriptAnalyzer
        version: null
        scope: 0
        repositoryName: null
        preRelease: false
        _exist: false
      - name: SqlServer
        version: null
        scope: 0
        repositoryName: null
        preRelease: false
        _exist: false
messages: []
hadErrors: false

Run it with the dsc config set, and it installs only what's needed. Run it again tomorrow, next week, or on a different machine, and you get the same results (unless a new version is released for the version range).

Version ranges: flexibility where you need it

Notice the version syntax in the example above? That's NuGet version range notation, and it gives you precise control:

  • '21.1.18256' locks to an exact version
  • '[[5.0.0,6.0.0)' accepts any 5.x release but excludes 6.0 and above
  • '[[>=1.21.0]' sets a minimum version

You can also specify ranges with both bounds: '[2.0.0,2.5.0]' (both inclusive) when you've tested a specific version window.

This isn't just syntax flexibility—it's policy-as-code. Your config file captures exactly which versions are acceptable and which will break your automation.

ℹ️
Do note that the module isn't officially released, and this behavior can change over time.

Managing package sources

You can also declare where modules come from:

resources:
  - name: Internal Module Repository
    type: Microsoft.PowerShell.PSResourceGet/Repository
    properties:
      Name: CompanyInternal
      Uri: https://nuget.company.com/powershell
      Trusted: true
      Priority: 10

The key benefits of declaring such a configuration are:

  • Priority control: Lower numbers are checked first
  • Consistency: Everyone uses the same sources
  • Trusted repositories: No more confirmation prompts in automation (it doesn't mean you still have to take care of the authentication flow for these repositories)

Once you've declared a repository, PSResourceList can automatically pull from it without additional configuration.

Real scenarios where this saves your day

Here are two scenarios where declaring your repositories and modules can save the day.

Scenario 1: New build agent setup

Your DevOps pipeline needs specific tool versions installed:

resources:
  - name: Build Tools
    type: Microsoft.PowerShell.PSResourceGet/PSResourceList
    properties:
      resources:
        - name: Pester
          version: '5.7.1'
        - name: PSScriptAnalyzer
          version: '1.22.0'
        - name: Microsoft.PowerShell.PlatyPS
          version: '1.0.1'

New build agent? Apply this config. Agent acting weird? Reapply the config. Testing a new tool version? Change the config and reapply. Version control your infrastructure the same way you version control your code.

Scenario 2: Developer environment consistency

Every developer needs the same internal modules:

resources:
  - name: Company Module Repository
    type: Repository
    properties:
      Name: Microsoft.PowerShell.PSResourceGet/Repository
      Uri: https://artifacts.company.com/powershell
      Trusted: true
      Priority: 5
  
  - name: Internal Modules
    type: Microsoft.PowerShell.PSResourceGet/PSResourceList
    properties:
      resources:
        - name: CompanyDSCResources
          version: '3.2.0'
          repository: CompanyPowerShell
        - name: CompanyAzureTools
          version: '1.5.0'
          repository: CompanyPowerShell

New developer joins? They run one command and have a production-matched environment—no wiki pages listing manual steps. Just give them the dsc config set command, and they're ready to go.

Getting started

Ready to try it? Here's how to get up and running in five minutes. Since this is currently unreleased, as mentioned earlier, you'll need to grab it from GitHub. However, once this functionality is integrated into PSResourceGet, you won't need these installation steps. Just install dsc.exe, upgrade PowerShell (PSResourceGet ships with newer versions of PowerShell), or update it manually.

For now, here's the setup:

  1. Install dsc.exe:
Install-PSResource -Name PSDSC -Scope CurrentUser

Install-DscExe
  1. Download the Microsoft.PowerShell.PSResourceGet DSC resource:
$repoOwner = "PowerShell"
$repoName = "PSResourceGet"
$prNumber = 1852

$apiUrl = "https://api.github.com/repos/$repoOwner/$repoName/pulls/$prNumber/files"
$pr = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "PowerShell" }

$dscLocation = Get-Command dsc | Select-Object -ExpandProperty Source | Split-Path -Parent

foreach ($file in $pr) {
    if ($file.filename.Contains("/")) {
        $fileName = $file.filename.Split("/")[-1]
    } else {
        $fileName = $file.filename
    }
    Write-Output "Downloading $($file.filename) to $dscLocation"
    $outputFile = Join-Path -Path $dscLocation -ChildPath $fileName
    Invoke-RestMethod -Uri $file.raw_url -OutFile $outputFile
}
  1. Create a config file (modules.dsc.yaml):
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
  - name: Essential Modules
    type: PSResourceList
    properties:
      resources:
        - name: Pester
          version: '5.7.1'
  1. Apply it:
dsc config set --file modules.dsc.yaml
  1. Test it later:
dsc config test --file modules.dsc.yaml

The test command checks if the resource is in its desired state.

Why this matters

This isn't just about easier installation. It's about:

  • Drift detection: Know when someone manually installed a different version (when there's an orchestrator)
  • Compliance: Prove your environments match your actual document
  • Reproducibility: Rebuild environments from code, not from memory

Traditional imperative scripts can't give you these capabilities. DSC's declarative model makes them natural.

Try it, break it, share your experience

The Microsoft.PowerShell.PSResourceGet DSC resource isn't officially released yet, but you've now learned how to get started.

  • What scenarios work great for you?
  • What scenarios break it?
  • What features would make this indispensable?

Configuration-as-code isn't just for servers and cloud resources anymore. Your automation toolchain deserves the same treatment.