How adapted resource manifests bridge PowerShell resources in Microsoft Desired State Configuration (DSC)

Learn how to speed up discovery for PowerShell DSC resources

How adapted resource manifests bridge PowerShell resources in Microsoft Desired State Configuration (DSC)

If you ever waited for dsc resource list --adapter Microsoft.Adapter/PowerShell to finish, you might think that the problem is DSC's engine itself. But that's not completely true.

DSC's engine (dsc.exe) bridges PowerShell resources using an adapter. That adapter just knows about PowerShell's subsystem and does the translation to the engine. But when the adapter does discovery of PowerShell resources, it uses either the old PowerShell DSC module (PSDesiredStateConfiguratiron), or scans through all PowerShell module paths using the $env:PSModulePath environment variable.

Even though the DSC team implemented some caching mechanisms to speed up this process, each discovery that happens whenever you try to invoke a PowerShell DSC resource just takes time.

That's changing.

Two recent pull requests have been officially merged and released in v3.2.0-preview.13. Those pull requests introduce adapted resource manifests for the PowerShell 7 adapter (Microsoft.Adapter/PowerShell). An adapted resource manifest is a small JSON file that tells DSC's engine exactly what it needs to know. No scanning, no cache warming, no more waiting. Let me walk you through what's different, how it works, and what you still need to be aware of.

The current behavior

I've already mentioned it a bit in the introduction, but DSC discovers resources by scanning PATH (or DSC_RESOURCE_PATH) environment variable for *.dsc.resource.json files (or .yaml/.yml). If a resource manifest is found on the PATH, DSC finds it instantly. Just simple and fast.

But PowerShell modules with their DSC resources don't live in PATH. They live in $env:PSModulePath. Think of places like ~/Documents/PowerShell/Modules, /usr/local/share/powershell/Modules or C:\Program Files\PowerShell\Modules. And those paths don't contain *.dsc.resource.json files either, so DSC can't see them. At least not without the help of the adapter.

So, that's where the adapter acts as this kind of bridge. When you run dsc resource list --adapter Microsoft.Adapter/PowerShell, the adapter executes a PowerShell function called Invoke-DscCacheRefresh. This function does a lot of heavy lifting like:

  1. It splits all the paths in $env:PSModulePath and walks through every directory.
  2. It loads the module manifest for each module, looking for DscResourcesToExport.
  3. It parses the AST of .psm1 files to find [DscResource()] attributes.
  4. It builds a JSON cache file (PSAdapterCache.json)

Even with caching, it can take time to find the right resource. That, of course, depends on some factors if the adapter isn't specified in a configuration document or if a system has many PowerShell modules installed.

We can see with those points that the tension is clear. While DSC's model is PATH-based and snappy, so is the opposite of PowerShell's module system. They don't fit together naturally.

To top it all off, you can't perform a dsc resource schema on a PowerShell DSC resource. The adapter always just returned true, and you had to guess the properties that you specified when invoking a resource.

Now that we know the current behavior, let's look at how it changes with adapted resource manifests.

With adapted resource manifests

The actual fix is actually surprisingly straightforward. Instead of making dsc.exe discover PowerShell resources at runtime, you declare them upfront.

Sounds vague, I know.

An adapted resource manifest in the Microsoft DSC landscape is a totally new file type: *.dsc.adaptedResource.json. It's a very lightweight JSON file that tells the engine everything about a PowerShell resource without launching PowerShell at all. Here's what one looks like:

{
    "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json",
    "type": "MyModule/MyDscResource",
    "kind": "resource",
    "version": "1.0.0",
    "capabilities": ["get", "set", "test"],
    "description": "Manages widget configuration.",
    "author": "Gijs Reijn",
    "requireAdapter": "Microsoft.Adapter/PowerShell",
    "path": "MyModule.psd1",
    "schema": {
        "embedded": {
            "$schema": "http://json-schema.org/draft-07/schema#",
            "title": "MyDscResource",
            "type": "object",
            "properties": {
                "name": { "type": "string" },
                "enabled": { "type": "boolean" }
            },
            "required": ["name"],
            "additionalProperties": false
        }
    }
}

For users who already know the resource manifest, this should look very similar. But for the ones not, let's break it down:

  • The typeproperty is the fully-qualified resource type name; in this example, it represents the <ModuleName/ResourceName.
  • The requireAdapter property tells DSC's engine which adapter handles this resource.
  • The path is the relative path to the PowerShell module manifest. This is the key to the magic.
  • The schema property is an embedded JSON schema that DSC can validate input on.
  • The capabilities property says what capabilities the DSC resource supports.

In this case, when I'm talking about a DSC resource, I'm talking about a class-based one.

When DSC finds this file on the PATH, it reads it and registers the resource. This happens with dsc resource list and not dsc resource list --adapter Microsoft.Adapter/PowerShell. So that means it doesn't spawn a PowerShell process, nor does it scan through $env:PSModulePath.

Okay, we have the theory, what about the performance and improvements?

Benefits using adapted resource manifests

To look at the benefits, we can do a short exercise by creating a class-based DSC resource, running it through the adapter, and then through the adapted resource manifest approach:

# MyModule.psd1
@{
    ModuleVersion        = '1.0.0'
    GUID                 = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
    Author               = 'Gijs Reijn'
    Description          = 'Manages widget configuration.'
    PowerShellVersion    = '7.0'
    RootModule           = 'MyModule.psm1'
    DscResourcesToExport = @('MyDscResource')
}

# MyModule.psm1
[DscResource()]
class MyDscResource {
    [DscProperty(Key)]
    [string] $Name

    [DscProperty()]
    [bool] $Enabled

    [MyDscResource] Get() {
        $currentState = [MyDscResource]::new()
        $currentState.Name = $this.Name
        $currentState.Enabled = $false

        return $currentState
    }

    [void] Set() {
        if ($this.Enabled) {
            Write-Verbose "Enabling $($this.Name)"
        } else {
            Write-Verbose "Disabling $($this.Name)"
        }
    }

    [bool] Test() {
        $currentState = $this.Get()
        return $currentState.Enabled -eq $this.Enabled
    }
}

Honestly, the above example doesn't represent a production-ready module, but it illustrates the purpose. Let's measure the first and second times when running through the dsc resource list command:

Figure 1: Measuring through dsc resource list

You can see that there's a small difference in there, but still not significant in speed. Now, time to use the adapted resource manifest:

{
    "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json",
    "type": "MyModule/MyDscResource",
    "kind": "resource",
    "version": "1.0.0",
    "capabilities": ["get", "set", "test"],
    "description": "Manages widget configuration.",
    "author": "Gijs Reijn",
    "requireAdapter": "Microsoft.Adapter/PowerShell",
    "path": "MyModule/1.0.0/MyModule.psd1",
    "schema": {
        "embedded": {
            "$schema": "http://json-schema.org/draft-07/schema#",
            "title": "MyDscResource",
            "type": "object",
            "properties": {
                "name": { "type": "string" },
                "enabled": { "type": "boolean" }
            },
            "required": ["name"],
            "additionalProperties": false
        }
    }
}
Figure 2: Running dsc resource list to discover PowerShell resource

That's pretty fast! And what it even brings more is that you can see the schema of the resource because we added the JSON embedded schema in the resource manifest:

Figure 3: Get resource schema

Clearly, you can see the differences between using an adapted resource manifest versus running it directly through the adapter for discovery (the same will be for other operations like get or set).

The one "limitation"

Here's the part that I left behind and you need to pay attention to.

In figure 2, you probably only noticed that one resource was returned. That was because I explicitly defined the path where the PowerShell module lived using DSC_RESOURCE_PATH. But that instantly brings me to the start of the story.

Adapted resource manifests are discovered the same way as any other DSC manifest file. It knows about it when it is either in the PATH or DSC_RESOURCE_PATH. But PowerShell modules typically aren't located in these environment variables.

The Invoke-DscCacheRefresh doesn't resolve the path field in the adapted resource manifest, either. It's just not built for it. So, even if you ship an adapted resource manifest for your DSC resources, DSC won't find them when you either use Install-Module or Install-PSResource.

Is there a way to solve this?

Well, luckily, yes. DSC has an extensible model allowing you to discover resources outside its boundary of PATH or DSC_RESOURCE_PATH. This can be done through a discovery extension. Discovery extensions have a file extension of *.dsc.extension.json with a discover command that knows where PowerShell modules live. And if it knows where they live, it can load the relevant manifest files.

Now, I can paste up the code, but I've taken the initiative to have this extension built in when you install DSC. The pull request is merged and can be found at GitHub (and will become available in the next versions of Microsoft DSC). It's worth noting that this is a feature only for PowerShell 7. The WindowsPowerShell adapter isn't supported.

Conclusion

You've just learned that an adapted resource manifest brings a change in how PowerShell DSC resources show up in DSC. It definitely speeds up the process of discovering, but also gives you the capability to list out the schema. For other operations, it's good to know that it skips the full cache refresh, even making it faster and more predictable.

But...

It still needs a discovery extension. Hopefully, the pull request linked will be included in the next releases.

If you maintain PowerShell DSC resources, especially class-based ones, you can start creating adapted resource manifests for them and then ship them alongside your module packaging. Your users are going to notice the