Every PowerShell Desired State Configuration resource must have at least one Key property that’s used to uniquely identify it within a single configuration. For the DSC Script Resource the keys are the GetScript, TestScript, and SetScript properties. Basically this means that each Script resource can’t contain the same content. Makes sense on the surface, but when you consider variable substitution, and looping through collections in $ConfigurationData, it’s easy to come up with a configuration that ends up with this error:

Add-NodeKeys : The key properties combination ‘your script here’ is duplicated for keys ‘GetScript,SetScript,TestScript’ of resource ‘Script’ in node ‘nodename’. Please make sure key properties are unique for each resource in a node.

This is quite annoying, but there are ways around it so you don’t have to resort to manually unrolling your loop.

Error Demonstration

Consider the following contrived sample configuration:

Configuration ServerSetup
{ 
    Node $AllNodes.NodeName
    {
        foreach ($value in $Node.values) {
            Script "$value"
            {
                GetScript = {
                    $thisValue = $Using:value
                    $current = Get-SomeData -About $thisValue
                    @{
                        $thisValue = [bool]$current
                    }
                }

                TestScript = {
                    $thisValue = $Using:value
                    $current = Get-SomeData -About $thisValue
                    $dataGood = [bool]$current
                    Write-Verbose -Message "Status of Data about '$thisValue' is: $dataGood"
                    $dataGood
                }

                SetScript = {
                    $thisValue = $Using:value
                    Write-Verbose -Message "Enabling Data about '$thisValue'"
                    Set-SomeData -About $thisValue
                }
            }
        }
    }
}

Assuming the configuration data for the node contains more than one entry in its values key, this will cause the error above.

What’s going wrong?

The problem is that your loop is generating multiple Script resources, and each of them appear to have the same values for the key properties, but the point of embedding the current iteration’s $value in the script with $Using:value is specifically to generate a unique set of [ScriptBlock]’s for each iteration.

The problem is that the script blocks are being checked before the $Using variables are replaced. If the check were not being done at all, the generated MOF would end up being correct.

[ScriptBlock] vs [String] and variable replacement

It has been pointed out that the *Script properties on this resource are actually [String] types. It’s useful to assign a [ScriptBlock] because it makes it much easier to write the code in an editor: you get proper syntax highlighting and tab completion and intellisense. But if you wanted to you could just use a string.

That means you could use variable substitution and the values you want to be unique would be embedded. So you could do something like this:

GetScript = "
    `$thisValue = `"$value`" # Hope that was really a string
    `$current = Get-SomeData -About `$thisValue
    @{
        `$thisValue = [bool]`$current
    }
"

You can see that this kind of sucks.

  • You get no source editing features

  • You have to be careful to escape special characters. If you miss one, you’ll be embedding a variable you didn’t intend to (which will probably end up being $null and it will be hard to find. The bigger the code, the worse this becomes (though arguably at that point you shouldn’t be using the Script resource anymore). And it’s not just $, you’ll also have to replace the backtick itself, double quotes, etc.

    You could use single quotes with the -format operator and use {0},{1}, etc., but then you have to escape single quotes and curly braces.

  • You have to be aware of the variable type

  • You can’t easily embed complex objects

This is not ideal.

Using the Power of $Using

If you take a look at a MOF file that’s generated from the use of a Script resource with $Using variables, you’ll be able to see the actual [String]s that are generated by the process.

As an even smaller example let’s look at this GetScript property:

$v = @{ Hello = 'My Name Is' }
Script MyScript
{
    GetScript = { $Using:v }
}

In the MOF, you’ll end up with a script resource that looks something like this:

Instance of MSFT_ScriptResource as $MSFT_ScriptResource1ref
{
ResourceID = "[Script]MyScript";
GetScript = "$v = [System.Management.Automation.PSSerializer]::Deserialize('<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas.microsoft.com/powershell/2004/04\">\n  <Obj RefId=\"0\">\n    <TN RefId=\"0\">\n      <T>System.Collections.Hashtable</T>\n      <T>System.Object</T>\n    </TN>\n    <DCT>\n      <En>\n        <S N=\"Key\">Hello</S>\n        <S N=\"Value\">My Name Is</S>\n      </En>\n    </DCT>\n  </Obj>\n</Objs>')\n $v ";
 ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.0";
};

So essentially what’s happened here is that PowerShell serialized the object into CliXML (the same way it would if you had used Export-CliXml) then embedded the deserialization call with the hardcoded XML string into the script block that gets put in the MOF. Pretty clever actually.

$Using this to our advantage

If DSC’s validation of uniqueness is happening before the substitution, then we can do the substitution ourselves by serializing the object and doing the same substitution before we pass it to the Script resource’s property.

I wrote a function that does just that. Here’s a gist so you can use git to clone it or just copy it, whichever is easiest:

Function Usage

$v = @{ Hello = 'My Name Is' }
Script MyScript
{
    GetScript = Replace-Using -Code { 
        $Using:v 
    }
}

# or

Script MyScript
{
    GetScript = { 
        $Using:v 
    } | Replace-Using
}

That’s all there is to it! Write your script block like you normally would (with $Using), but pass it to the Replace-Using function either by parameter or by pipeline. All of the $Using variables will be replaced by serialized representations (with deserialization code embedded).

Function Breakdown

This function takes a single [String] parameter that contains the code (passing in a [ScriptBlock] works because it gets cast).

I use a specific overload of [RegEx]::Replace() that takes the string the search, the pattern to search for, and critically, a callback function to process the replace.

I create a script block within the function that acts as the callback function. The RegEx pattern I’m using looks for $Using: and then captures the part after the colon, which is the variable name. Within the callback I retrieve the value of that variable with Get-Variable and serialize it with the built-in serializer.

Then we just return the string that we want to use to replace the match (which is the entire $Using:variable part of the code block). For the replacement, I make sure to embed the Deserialize call, and I wrap the whole thing in $() because that will help it work better in more situations (like if you embedded your variable in a string: "A powershell string containing $Using:myvalue is useful.").

Caveats

I can’t be certain that what I’m doing will embed cleanly into every single place where you could shove a $Using variable so you might want to just do something simple with it like assign your $Using variables to local script variables and then use the local from then on. It’s easier to type and to debug that way anyway:

{
    $localVar = $Using:myVar
    
    Get-SomeThing -Data "A string $localVar is: $($localVar | Verify-Data)"
}

For now using this in a DSC Script Resource is the only time I can think of where it makes sense to do this manually, but I’d be interested to hear of other uses.