Introduction

Configuration Data for DSC is somewhat analogous to configuration files in traditional applications; different environments can have their own Configuration Data file. The 2 main topics I will go over in this blog post is the use of Nodes in Configuration Data, and using multiple Configuration Data files.

Nodes in Configuration Data

A typical Configuration Data file looks something like this:

@{
    AllNodes =
    @(
        @{
            NodeName = "VM-1"
            Role     = "WebServer"
        },
        @{
            NodeName = "VM-2"
            Role     = "AppServer"
        },
        @{
            NodeName = "VM-3"
            Role     = "SQLServer"
        }
    );

    NonNodeData = @{
        LogFolder = "C:\Logs"
        InputFolder = "C:\Input"
        OutputFolder = "C:\Output"
        ApplicationUrl = "https://myapp.com"
        ServiceUrl = "https://myservice.com"
    }
}

AllNodes is the only required property; it is an array of Node objects. Each Node object must have a NodeName property. We can add as many additional properties as we would like to Node objects as well as the top top-level Configuration Data object itself. NonNodeData is generally used as a container to put all properties that do not apply to individual Nodes; however, we could rename it or split it into multiple objects. Taking the previous sample above, we can rearrange things to make it a bit easier to read:

@{
    AllNodes =
    @(
        @{
            NodeName = "VM-1"
            Role     = "WebServer"
        },
        @{
            NodeName = "VM-2"
            Role     = "AppServer"
        },
        @{
            NodeName = "VM-3"
            Role     = "SQLServer"
        }
    )
    Folders = @{
        LogFolder = "C:\Logs"
        InputFolder = "C:\Input"
        OutputFolder = "C:\Output"
    }
    Urls = @{
        ApplicationUrl = "https://myapp.com"
        ServiceUrl = "https://myservice.com"
    }
}

Regarding the NodeName property – you will often see this as the same as the VM Name. However, this isn’t required – NodeName can be anything. In fact, there is a good reason to not treat it as a VM Name, and instead use NodeName more like the Role of the VM. In the original example, what happens if we want to add a second WebServer? Since Configuration Data is set at compile time, we would need to recompile our DSC. Rewriting that Configuration Data file, we can eliminate the need to recompile if we want to add a new server:

@{
    AllNodes =
    @(
        @{
            NodeName = "WebServer"
        },
        @{
            NodeName = "AppServer"
        },
        @{
            NodeName = "SQLServer"
        }
    )
    Folders = @{
        LogFolder = "C:\Logs"
        InputFolder = "C:\Input"
        OutputFolder = "C:\Output"
    }
    Urls = @{
        ApplicationUrl = "https://myapp.com"
        ServiceUrl = "https://myservice.com"
    }
}

There is unfortunately one use case where the above does not work – if you have a DSC resource that needs to reference the actual VM name (such as the ComputerManagementDsc’s Computer resource which is used to join a computer to a domain). You would either have to revert back to specifying each VM name in your Configuration Data file, or rewrite the DSC resource to not need the actual VM name.

Dealing with multiple Configuration Data files

Let’s say we’ve properly split out our Configuration Data files so that we have one for each environment:

MySampleConfiguration.AzureCloud.psd1:

@{
    AllNodes =
    @(
        @{
            NodeName = "WebServer"
        },
        @{
            NodeName = "AppServer"
        },
        @{
            NodeName = "SQLServer"
        }
    )
    Environment = 
    @{
        Name = "AzureCloud"
    }
}
MySampleConfiguration.AzureUSGovernment.psd1:

@{
    AllNodes =
    @(
        @{
            NodeName = "WebServer"
        },
        @{
            NodeName = "AppServer"
        },
        @{
            NodeName = "SQLServer"
        }
    )
    Environment = 
    @{
        Name = "AzureUSGovernment"
    }
}

There is a good bit of duplicate content in both files. Since Configuration Data is just a hashtable object, and its value is set at compile time, we can split out our files, then merge them with powershell

MySampleConfiguration.shared.psd1:

@{
    AllNodes =
    @(
        @{
            NodeName = "WebServer"
        },
        @{
            NodeName = "AppServer"
        },
        @{
            NodeName = "SQLServer"
        }
    )
}
MySampleConfiguration.AzureCloud.psd1:

@{
    Environment = 
    @{
        Name = "AzureCloud"
    }
}
MySampleConfiguration.AzureUSGovernment.psd1:

@{
    Environment = 
    @{
        Name = "AzureUSGovernment"
    }
}

We need to write a function to merge the hashtable objects. In this case, any new properties are copied from the second object into a clone of the first, and any properties in the first that also exist in the second are overwritten by the second:

function Merge-Hashtables
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [Hashtable]
        $First,

        [Parameter(Mandatory=$true)]
        [Hashtable]
        $Second
    )

    function ProcessKeys($first, $second)
    {
        foreach ($key in $second.Keys) {
            $firstValue = $first[$key]
            $secondValue = $second[$key]

            if ($firstValue -is [hashtable] -and $secondValue -is [hashtable])
            {
                ProcessKey($firstValue, $secondValue)
            }
            else
            {
                $first[$key] = $secondValue
            }
        }
    }

    $firstClone = $First.Clone()
    $secondClone = $Second.Clone()

    ProcessKeys -first $firstClone -second $secondClone

    return $firstClone
}

Then we load the shared and appropriate environment-specific Configuration Data file, merge them, and pass it in at compile time:

$sharedConfigData = Import-PowerShellDataFile MySampleConfiguration.shared.psd1
$azureCloudConfigData = Import-PowerShellDataFile MySampleConfiguration.AzureCloud.psd1
$mergedConfigurationData = Merge-Hashtables -First $sharedConfigData -Second $azureConfigData

$automationAccountName = "Your Azure Automation Account Name"
$resourceGroupName = "The resource group containing your automation account"
$configurationName = "Name of your configuration"
$pathToConfiguration = "Path to your DSC file"

Import-AzAutomationDscConfiguration -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -SourcePath $pathToConfiguration -Force -Published

Start-AzAutomationDscCompilationJob -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -ConfigurationName $configurationName -ConfigurationData $mergedConfigurationData