Adding ConfigurationData dynamically from a DSC configuration
When writing a DSC configuration, separating the environmental data from the configuration logic is a best practice : it allows to reuse the same logic for different environments, for example the Dev, QA and Prod environments.
This generally means that the environment data is stored in separate .psd1
files. This is explained in this documentation page.
However, these configuration data files are relatively static, so if the environment changes frequently these files might end up containing outdated information. A solution is to keep the static environment data in the configuration data files and then adding the dynamic data on the fly.
A good example of this use case is a web application, where the configuration is identical for all web servers but these servers are treated not as pets but as cattle : we create and kill them on a daily basis.
Because they are cattle, we don’t call them by their name, in fact we don’t even know their name. So the configuration data file doesn’t contain any node names :
@{
# Node specific data
AllNodes = @(
# All the Web Servers have following information
@{
NodeName = '*'
WebsiteName = 'ClickFire'
SourcePath = '\\DevBox\SiteContents\'
DestinationPath = 'C:\inetpub\wwwroot\ClickFire_Content'
DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
}
);
NonNodeData = ''
}
By the way, the web application used in this example is an internal HR app, codenamed “Project ClickFire”.
Let’s assume the above configuration data is all the information we need to configure our nodes. That’s great, but we still need some node names, otherwise there will be no MOF file generated when we run the configuration.
So we’ll need the query some kind of database to get the names of the web servers for this application, Active Directory for example. This is easy to do, especially if these servers are all in the same OU and/or there is a naming convention for them :
C:\> $OUPath = 'OU=Project ClickFire,OU=Servers,DC=Mat,DC=lab'
C:\> $DynamicNodeNames = Get-ADComputer -SearchBase $OUPath -Filter {Name -Like 'Web*'} |
>> Select-Object -ExpandProperty Name
C:\> $DynamicNodeNames
Web083
Web084
Web086
Now that we have the node names, we need to add a hashtable for each node into the AllNodes
section of our configuration data.
To do that, we first need to import the data from the configuration data file and we store it into a variable for further manipulation. There is a new cmdlet introduced in PowerShell 5.0 which makes this very simple : Import-PowerShellDataFile
:
C:\> $EnvironmentData = Import-PowerShellDataFile -Path 'C:\Lab\EnvironmentData\Project_ClickFire.psd1'
C:\> $EnvironmentData
Name Value
---- -----
AllNodes {System.Collections.Hashtable}
NonNodeData
C:\> $EnvironmentData.AllNodes
Name Value
---- -----
DefaultWebSitePath C:\inetpub\wwwroot\ClickFire_Content
NodeName *
WebsiteName ClickFire
DestinationPath C:\inetpub\wwwroot\ClickFire_Content
SourcePath \\DevBox\SiteContents\
Now, we have our configuration available to us as a [hashtable]
and the AllNodes
section inside of it is also a hashtable.
More accurately, the AllNodes
section is an array of hashtables because each node entry within AllNodes
is a [hashtable]
:
C:\> $EnvironmentData.AllNodes.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
C:\> $EnvironmentData.AllNodes | Get-Member | Select-Object TypeName -Unique
TypeName
--------
System.Collections.Hashtable
So now, what we need to do is to inject a new node entry for each node returned by our Active Directory query into the AllNodes
section :
C:\> Foreach ( $DynamicNodeName in $DynamicNodeNames ) {
$EnvironmentData.AllNodes += @{NodeName = $DynamicNodeName; Role = 'WebServer'}
}
For each node name, we add a new [hashtable]
into AllNodes
.
These hashtables are pretty simple in this case, this is just to give our nodes a name and a role (in case we need to differentiate with other server types, like database servers for example).
The result of this updated configuration data is equivalent to :
@{
# Node specific data
AllNodes = @(
# All the Web Servers have following information
@{
NodeName = '*'
WebsiteName = 'ClickFire'
SourcePath = '\\DevBox\SiteContents\'
DestinationPath = 'C:\inetpub\wwwroot\ClickFire_Content'
DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
}
@{
NodeName = 'Web083'
Role = 'WebServer'
}
@{
NodeName = 'Web084'
Role = 'WebServer'
}
@{
NodeName = 'Web086'
Role = 'WebServer'
}
);
NonNodeData = ''
}
So that’s it for the node data, but what if we need to add non-node data ?
It is very similar to the node data because the NonNodeData
section of the configuration data is also a [hashtable]
.
Let’s say we want to add a piece of XML data that may be used for the web.config
file of our web servers to the NonNodeData
section of the configuration data. We could do that in the configuration data file :
@{
# Node specific data
AllNodes = @(
# All the Web Servers have following information
@{
NodeName = '*'
WebsiteName = 'ClickFire'
SourcePath = '\\DevBox\SiteContents\'
DestinationPath = 'C:\inetpub\wwwroot\ClickFire_Content'
DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
}
);
NonNodeData =
@{
DynamicConfig = [Xml](Get-Content -Path C:\Lab\SiteContents\web.config)
}
}
Nope, we get an ugly error :
Cannot generate a Windows PowerShell object for a ScriptBlock evaluating dynamic expressions.
This is because to safely import data from a file, the cmdlet Import-PowerShellDataFile
works in RestrictedLanguage mode.
This means that executing cmdlets, or functions, or any type of command is not allowed in a data file. Even the [Xml]
type and a bunch of other things are not allowed in this mode. For more information, please refer to about_Language_Modes.
It does make sense : data files should contain data, not code.
OK, so we’ll do that from the DSC configuration script, then :
C:\> $DynamicConfig = [Xml](Get-Content -Path '\\DevBox\SiteContents\web.config')
C:\> $DynamicConfig
xml configuration
--- -------------
version='1.0' encoding='UTF-8' configuration
C:\> $EnvironmentData.NonNodeData = @{DynamicConfig = $DynamicConfig}
C:\> $EnvironmentData.NonNodeData.DynamicConfig.configuration
configSections : configSections
managementOdata : managementOdata
appSettings : appSettings
system.web : system.web
system.serviceModel : system.serviceModel
system.webServer : system.webServer
runtime : runtime
With this technique, we can put whatever we want in NonNodeData
, even XML data, as long as it is wrapped in a [hashtable]
. The last command shows that we can easily access this dynamic config data because it is stored as a tidy [Xml]
PowerShell object.
Please note that the Active Directory query, the import of the configuration data and the manipulation of this data are all done in the same script as the DSC configuration but outside of the DSC configuration itself.
That way, this modified configuration data can be passed to the DSC config via its ConfigurationData
parameter.
Putting it all together, here is what the whole DSC configuration script looks like :
Configuration Project_ClickFire
{
Import-DscResource -Module 'PSDesiredStateConfiguration'
Import-DscResource -Module 'xWebAdministration'
Node $AllNodes.Where{$_.Role -eq 'WebServer'}.NodeName
{
WindowsFeature IIS
{
Ensure = 'Present'
Name = 'Web-Server'
}
File SiteContent
{
Ensure = 'Present'
SourcePath = $Node.SourcePath
DestinationPath = $Node.DestinationPath
Recurse = $True
Type = 'Directory'
DependsOn = '[WindowsFeature]IIS'
}
xWebsite Project_ClickFire_WebSite
{
Ensure = 'Present'
Name = $Node.WebsiteName
State = 'Started'
PhysicalPath = $Node.DestinationPath
DependsOn = '[File]SiteContent'
}
}
}
# Adding dynamic Node data
$EnvironmentData = Import-PowerShellDataFile -Path "$PSScriptRoot\..\EnvironmentData\Project_ClickFire.psd1"
$OUPath = 'OU=Project ClickFire,OU=Servers,DC=Mat,DC=lab'
$DynamicNodeNames = (Get-ADComputer -SearchBase $OUPath -Filter {Name -Like 'Web*'}).Name
Foreach ( $DynamicNodeName in $DynamicNodeNames ) {
$EnvironmentData.AllNodes += @{NodeName = $DynamicNodeName; Role = 'WebServer'}
}
# Adding dynamic non-Node data
$DynamicConfig = [Xml](Get-Content -Path '\\DevBox\SiteContents\web.config')
$EnvironmentData.NonNodeData = @{DynamicConfig = $DynamicConfig}
Project_ClickFire -ConfigurationData $EnvironmentData -OutputPath 'C:\Lab\Conf\Project_ClickFire'
Running this script indeed generates a MOF file for each node, containing the same settings :
C:\> & 'C:\Lab\Conf\Project_ClickFire_Config.ps1'
Directory: C:\Lab\Conf\Project_ClickFire
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 6/6/2016 1:37 PM 3986 Web083.mof
-a---- 6/6/2016 1:37 PM 3986 Web084.mof
-a---- 6/6/2016 1:37 PM 3986 Web086.mof
Hopefully, this helps treating web servers really as cattle and give its full meaning to the expression “server farm”.
Comments
ekw
is there anything similar to ‘Export-PowershellDataFile’ ? In my situation I’m trying to provide a way for engineers to add nodes to the psd1 file without editing the psd1 file directly.
Import-PowershellDataFile seems like a great start, but I don’t see an Export-PowershellDataFile.
Mathieu Buisson
Hi ekw,
Like you, I was recently looking for
Export-PowershellDataFile
to write data from a script to a file, and then, re-hydrate this data from the file to another script.But
Export-PowershellDataFile
does not exist, which is, in my opinion, a unfortunate omission.In this specific case, I failed back to using JSON instead of a native PowerShell data file, to store and re-hydrate data.
Leave a Comment
Your email address will not be published. Required fields are marked *