Summary: Microsoft MVP, Will Anderson, modifies a Desired State Configuration template.
Good morning. Ed Wilson, Microsoft Scripting Guy, is here. Today we have part three of Will Anderson’s awesome Desired State Configuration (DSC) series. To catch up, be sure you read:
Here’s Will…
We've pasted our template into our ISE instance, and now we're going to start modifying it.
Importing DSC resources
Any DSC resource that is not part of the built-in DSC resources need to be imported into the configuration to tell the target system what tools it needs to perform the necessary tasks. Although it's not required for the built-in resources, it can be a good idea to also call them out. This not only establishes good habit patterns, but if you start downloading and using updated versions of those built-in resources, they may not necessarily have the same capabilities and attributes as the version that came with the software. Also, if you don't import the DSC resource into the configuration, you'll get an annoying warning message when you go to generate the .mof file:
To import a DSC resource, you need to know the module that contains it and the version. You can find this with Get-DscResource:
PS C:\Windows\system32> Get-DscResource -Name WindowsFeature
ImplementedAs Name ModuleName Version Properties
------------- ---- ---------- ------- ----------
PowerShell WindowsFeature PSDesiredStateConfiguration 1.1 {Name, Credential, DependsOn, Ensure...}
If you want to know all of the resources contained in a specific module, you can use the Module parameter instead of the Name parameter:
PS C:\Windows\system32> Get-DscResource -Module PSDesiredStateConfiguration
ImplementedAs Name ModuleName Version Properties
------------- ---- ---------- ------- ----------
Binary File {DestinationPath, Attributes, Checksum, Content...
PowerShell Archive PSDesiredStateConfiguration 1.1 {Destination, Path, Checksum, Credential...}
PowerShell Environment PSDesiredStateConfiguration 1.1 {Name, DependsOn, Ensure, Path...}
PowerShell Group PSDesiredStateConfiguration 1.1 {GroupName, Credential, DependsOn, Description...}
Binary Log PSDesiredStateConfiguration 1.1 {Message, DependsOn, PsDscRunAsCredential}
PowerShell Package PSDesiredStateConfiguration 1.1 {Name, Path, ProductId, Arguments...}
PowerShell Registry PSDesiredStateConfiguration 1.1 {Key, ValueName, DependsOn, Ensure...}
PowerShell Script PSDesiredStateConfiguration 1.1 {GetScript, SetScript, TestScript, Credential...}
PowerShell Service PSDesiredStateConfiguration 1.1 {Name, BuiltInAccount, Credential, Dependencies...
PowerShell User PSDesiredStateConfiguration 1.1 {UserName, DependsOn, Description, Disabled...}
PowerShell WaitForAll PSDesiredStateConfiguration 1.1 {NodeName, ResourceName, DependsOn, PsDscRunAsC...
PowerShell WaitForAny PSDesiredStateConfiguration 1.1 {NodeName, ResourceName, DependsOn, PsDscRunAsC...
PowerShell WaitForSome PSDesiredStateConfiguration 1.1 {NodeCount, NodeName, ResourceName, DependsOn...}
PowerShell WindowsFeature PSDesiredStateConfiguration 1.1 {Name, Credential, DependsOn, Ensure...}
PowerShell WindowsOptionalFeature PSDesiredStateConfiguration 1.1 {Name, DependsOn, Ensure, LogLevel...}
PowerShell WindowsProcess PSDesiredStateConfiguration 1.1 {Arguments, Path, Credential, DependsOn...}
Now that we know the module and version, we can call it in our configuration file. This call is placed before the Node block:
configuration CMDPConfig
{
Import-DscResource -ModuleName PSDesiredStateConfiguration -ModuleVersion 1.1
# One can evaluate expressions to get the node list
# E.g: $AllNodes.Where("Role -eq Web").NodeName
node ("lwincm02")
{
You could also write it with the properties as a hash table, which works much better if you intend to use these same configurations in Azure because (as of this writing), Azure does not support the former method.
Import-DscResource -ModuleName @{ModuleName= 'PSDesiredStateConfiguration'; ModuleVersion = '1.1'}
Configuring the resource
When we call a DSC resource, we need to give it a friendly name. This should be something concise, but easily recognizable so that whoever is viewing the configuration script or configuration log files can easily identify what's being configured.
For my distribution points, I want to put my systems into a Server Core state (no GUI because we're PowerShell users and we don't need a GUI). We'll call this first configuration, RemoveUI. You can also add comments in the usual PowerShell fashion, but I save those for any time I need to make a more verbose annotation in the configuration script.
I'm also going to insert the name of the server feature we want to change on the mandatory Name parameter. If you're not sure of the name of the server feature you want to add or remove, you can always run the Get-WindowsFeature command and find it under the name field:
PS C:\Windows\system32> Get-WindowsFeature *gui*
Display Name Name Install State
------------ ---- -------------
[X] Graphical Management Tools and Infrastructure Server-Gui-Mgmt-Infra Installed
[X] Server Graphical Shell Server-Gui-Shell Installed
Now that we have the name of our feature, we'll start modifying our DSC resource:
Before we go further, it should be noted that there are common values among most of the DSC resources. They are
- DependsOn
- Ensure
- PsDscRunAsCredential (introduced in DSC v2 with WMF 5.0).
DependsOn and Ensure will probably be the values that you'll use the most, but I'll quickly discuss all three of them. Let's start with Ensure:
WindowsFeature RemoveUI
{
Name = 'Server-Gui-Shell'
[Credential = [PSCredential]]
[DependsOn = [string[]]]
[Ensure = [string]{ Absent | Present }]
[IncludeAllSubFeature = [bool]]
[LogPath = [string]]
[PsDscRunAsCredential = [PSCredential]]
[Source = [string]]
}
Ensure has two values available to it: Present and Absent. This is essentially your on/off switch. We want to remove the UI, so we'll be setting this to Absent. This is the only thing I want to do for this feature, so we'll remove the unused parameters from when we pasted our template.
WindowsFeature RemoveUI
{
Name = 'Server-Gui-Shell'
Ensure = 'Absent'
}
DependsOn establishes dependencies in your DSC configuration. For example, in my DP configuration script, I have a requirement to make sure that Remote Differential Compression is installed on the server because it's a required feature for the distribution point. By default, this feature is already installed on the server, but I want to make sure it's there at all times.
I also want to always make sure that this check is completed after the RemoveUI configuration. This way, I'm always maintaining a list of logical steps in my configuration. If my configuration ever goes out of compliance and I want to re-enforce it, the Local Configuration Manager will follow the logical order that I build in the configuration.
PsDscRunAsCredential, as you might have guessed, allows you to pass credentials to your configuration. For example, if you want to use a DSC resource to join a system to the domain, you can use this parameter to pass the administrative credentials that are required.
You don't have to have each step in you configuration necessarily dependent on the previous, but you definitely want to use it in situations where you might need to verify that any configurations that your change is dependent on are correctly set up.
For instance, my next configurations deal with IIS. I need to install the IIS 6 WMI Compatibility component, and for that I obviously need IIS. So it's logical to ensure that IIS exists on the system before attempting to install its dependent features.
DependsOn uses the DSC resource name inside of a pair of angle brackets, in addition to the friendly name we give the resource afterwards, as you can see in the following example. Let's paste our WindowsFeature DSC Resource template again, and configure Remote Differential Compression:
WindowsFeature EnableRemoteDifferentialCompression
{
Ensure = 'Present'
Name = 'RDC'
DependsOn = '[WindowsFeature]RemoveUI'
}
And now we'll continue down the line with our IIS features:
WindowsFeature IIS
{
Ensure = "Present"
Name = "Web-Server"
DependsOn = "[WindowsFeature]EnableRemoteDifferentialCompression"
}
WindowsFeature IIS6WMICompatibility
{
Ensure = "Present"
Name = "Web-WMI"
DependsOn = "[WindowsFeature]IIS"
}
At the end of the configuration block, we'll call the configuration to generate the .mof file. So your configuration should look a little something like this:
configuration CMDPConfig
{
Import-DscResource -ModuleName @{ModuleName= 'PSDesiredStateConfiguration'; ModuleVersion = '1.1'}
# One can evaluate expressions to get the node list
# E.g: $AllNodes.Where("Role -eq Web").NodeName
node ("lwincm02")
{
WindowsFeature RemoveUI
{
Name = 'Server-Gui-Shell'
Ensure = 'Absent'
}
WindowsFeature EnableRemoteDifferentialCompression
{
Ensure = 'Present'
Name = 'RDC'
DependsOn = '[WindowsFeature]RemoveUI'
}
WindowsFeature EnableIIS
{
Ensure = "Present"
Name = "Web-Server"
DependsOn = "[WindowsFeature]EnableRemoteDifferentialCompression"
}
WindowsFeature EnableIIS6WMICompatibility
{
Ensure = "Present"
Name = "Web-WMI"
DependsOn = "[WindowsFeature]EnableIIS"
}
}
}CMDPConfig
Now we have a very basic configuration that we can start testing. The first test is to see if we can generate a configuration .mof file. Make sure that your present working directory is set to a directory where you want to generate the .mof files. For my example, I created a Configs directory in C:\scripts, and I set my location to that path. By default, ISE puts you into the system32 directory, and we probably don't want our configurations to reside there.
So let's execute the script we created:
PS C:\Windows\system32> C:\scripts\Configs\CMDPConfig.ps1
Directory: C:\Windows\system32\CMDPConfig
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/7/2016 3:52 PM 20208 LWINCM02.mof
As you can see, when the script executes, it creates a directory with the name of the configuration you created. Inside, you'll find a .mof file with the host name of the system. Let's open the .mof file and examine the contents:
/*
@TargetNode='lwincm02'
@GeneratedBy=William
@GenerationDate=12/30/2015 18:55:10
@GenerationHost=LWINERD
*/
instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]RemoveUI";
Ensure = "Absent";
SourceInfo = "C:\\scripts\\Configs\\CMDPConfig.ps1::10::9::WindowsFeature";
Name = "Server-Gui-Shell";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.1";
ConfigurationName = "CMDPConfig";
};
instance of MSFT_RoleResource as $MSFT_RoleResource2ref
{
ResourceID = "[WindowsFeature]EnableRemoteDifferentialCompression";
Ensure = "Present";
SourceInfo = "C:\\scripts\\Configs\\CMDPConfig.ps1::16::9::WindowsFeature";
Name = "RDC";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.1";
DependsOn = {
"[WindowsFeature]RemoveUI"};
ConfigurationName = "CMDPConfig";
};
instance of MSFT_RoleResource as $MSFT_RoleResource3ref
{
ResourceID = "[WindowsFeature]EnableIIS";
Ensure = "Present";
SourceInfo = "C:\\scripts\\Configs\\CMDPConfig.ps1::23::9::WindowsFeature";
Name = "Web-Server";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.1";
DependsOn = {
"[WindowsFeature]EnableRemoteDifferentialCompression"};
ConfigurationName = "CMDPConfig";
};
instance of MSFT_RoleResource as $MSFT_RoleResource4ref
{
ResourceID = "[WindowsFeature]EnableIIS6WMICompatibility";
Ensure = "Present";
SourceInfo = "C:\\scripts\\Configs\\CMDPConfig.ps1::30::9::WindowsFeature";
Name = "Web-WMI";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.1";
DependsOn = {
"[WindowsFeature]EnableIIS"};
ConfigurationName = "CMDPConfig";
};
instance of OMI_ConfigurationDocument
{
Version="2.0.0";
MinimumCompatibleVersion = "1.0.0";
CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};
Author="William";
GenerationDate="12/30/2015 18:55:10";
GenerationHost="LWINERD";
Name="CMDPConfig";
};
Success! Now that we've created our first .mof file, we'll be able to push the configuration to a target machine. In our next exciting episode, we'll do a test push to add the rest of our Windows features to the configuration and update the .mof file!
~Will
Thank you, Will, for another awesome post. He will be back tomorrow with Part 4 of this series.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. Also check out my Microsoft Operations Management Suite Blog. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy