13 minutes
Windows Desired State Configuration - A Primer
I’ve recently completed my first DSC deployment. Whilst I was testing DSC in a lab, I wrote a primer for my colleages. Here is that primer, with some edits. It’s a rough guide but it features links to a number of useful resources.
Note: This is an old article which may contain some out of date content.
This article concentrates on using DSC in a push configuration. That means that the configuration is applied when a system administrator manually starts DSC from their own workstation. In the coming weeks I’ll be implementing a pull configuration for a development environments. This relies on a pull server from which the DSC clients are configured to fetch and apply DSC configuration.
What is DSC?
Desired State Configuration is a feature that first shipped with Windows Management Framework 4, which is in-turn closely coupled to Powershell 4.0. DSC provides an idempotent mechanism for deploying configuration changes to a target host or a group of target hosts.
Which version of Windows Work with DSC
- Windows 7 (PowerShellGet functionality doesn’t seem to work)
- Windows 8.1 (Professional Upwards)
- Server 2008 R2
- Server 2012
- Server 2012 R2
What do you need for DSC Administration?
Prerequisites
A pc for pushing your DSC configuration which must be running any version for Windows post Windows 7/Server 2008 r2 At least one host which you can provision with DSC.
Prepare your Environment
- If you are using Windows 7, then you will need to install Windows Management Framework 4.0 or higher. Windows 8.1 Professional/Server 2012r2 ship with WMF 4.0 in the tin. PowerShell WMF 4.0 (includes Powershell 4.0).
- Although WMF ships with a small set of resources, you might want to add some more. If you’re not using WMF5.0 I would recommend installing the DSC Resource Kit “Wave 10”.
Getting Started
The Powershell ISE can be used to author scripts and compile them into MOFs (Managed Object Files). Be aware that the MOFs will be generated in the ISE’s current folder, so before starting work use the console pane to change directory (e.g. Set-Location E:\DSC)
Writing a simple DSC Script is quite a trivial business. For example, the following PowerShell script will create a DSC MOF to provision an IIS Server with the base IIS role:
Configuration IisWebServerSample1
{
Import-DscResource –ModuleName ’PSDesiredStateConfiguration’
Node "MyWebServer"
{
WindowsFeature IIS
{
Ensure="Present"
Name="Web-Server"
}
}
}
Set-Location $env:UserProfile\Documents\ManagedObjectFiles
IisWebServerSample1
The DSC Script defines what to all intents and purposes, is a PowerShell function. The penultimate statement in the script sets our location so that executing the “function” (final step) produces a MOF in ‘‘‘C:\Users<UserName>\Dcouments\ManagedObjectFiles\MyWebServer’'’.
We can now apply our compiled MOF to a target host called “MyWebServer” with the following command:
Start-DscConfiguration -Path .\IisWebServerSample1 -Wait -Verbose
You can be a bit more flexible around applying DSC configuration to different hosts. Use a parameter for your target host (node) name and then leave compiling the MOF until it’s actually used, as per the following example:
Configuration IisWebServerSample2
{
# Parameters are optional
param($TargetNodeName)
Import-DscResource –ModuleName ’PSDesiredStateConfiguration’
Node $TargetNodeName
{
WindowsFeature IIS
{
Ensure="Present"
Name="Web-Server"
}
}
}
To apply this configuration to Server001, use the following statements:
Set-Location -Path $env:UserProfile\Documents\ManagedObjectFiles
. $env:UserProfile\Documents\ManagedObjectFiles\DscScripts\IisWebServerSample2.ps1 -TargetNodeName Server001
Start-DscConfiguration -Path $env:UserProfile\Documents\ManagedObjectFiles\IisWebServerSample2 -Wait
Resources
DSC is all about resources. In the examples above, the resource “WindowsFeature” is used to install IIS. In short, resources enable you to “do stuff” using DSC. With WMF 4.0, Microsoft started releasing resources in waves, the latest being Wave 10. With the advent of WMF 5.0, Microsoft has introduced a NuGet Style repository type system.
You can read more about resources here.
Resource Kits and Repositories
- https://technet.microsoft.com/en-us/library/dn249921.aspx - List of DSC resources included “in the box”
- https://gallery.technet.microsoft.com/scriptcenter/DSC-Resource-Kit-All-c449312d - Latest wave of the resource kit for use with WMF 4.0
- http://www.powershellgallery.com/ - PowerShell Gallery - requires at least WMF 5.0 Preview
- https://github.com/powershell/ - GitHub
Installing and using a Module
DSC resources are incorporated into modules. These very much resemble traditional PowerShell modules - it’s only their content and use that differs slightly.
If using WMF 5.0 (not on Windows 7), installing a new module is as simple as: Install-Module -Name xSmbShare
If using WMF 4.0 you may need to find the module on GitHub, download it, and extract the contents to C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DSCResources. Once it is installed, it should be immediately available to use in DSC scripts. You can check by running Get-DscResource and searching for the resource/module name, e.g.
> Get-DscResource
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...}
PowerShell xSmbShare xSmbShare 1.1.0.0 {Name, Path, ChangeAccess, ConcurrentUserLimit...}
If you experience problems installing resources, have a read of the MSDN Powershell Blog on How to Deploy and Discover Windows Powershell Desired State Configuration Resources.
Finding out about a Resource
List Installed DSC Resources
Get-DscResource
Get the properties for a resource
(Get-DscResource -Name File).Properties
Creating Resources
Sometimes it’s necessary to create your own resources.
Please note that I couldn’t get PowerShellGet working under Windows 7, which is used in the following example to install the xDSCResourceDesigner. It is possible to download it from GitHub and ensure the files are extracted to the appropriate path, e.g. *C:\Program Files\WindowsPowerShell\Modules\xDSCResourceDesigner\1.5.0.0*.
Set-Up
Before you can begin creating your own resources, you will need to install the xDSCResourceDesigner module.
Installing xDSCResourceDesigner with WMF 4.0
- Download xDSCResourceDesigner from GitHub
- Extract the contents of the folder contained within the zip file to C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DSCResources\xDSCResourceDesigner.
Once the module is installed, you can confirm it using Get-ChildItem:
> Get-ChildItem C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DSCResources\xDSCResourceDesigner
Directory: C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DSCResources\xDSCResourceDesigner
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 17/12/2014 18:09 17204 TechNetDocumentation-xDscResourceDesigner.html
-ar--- 11/12/2014 14:13 1275 xDSCResourceDesigner.psd1
-ar--- 11/12/2014 14:13 103520 xDSCResourceDesigner.psm1
-ar--- 27/03/2014 09:33 8022 xDSCResourceDesigner.strings.psd1
And also check using Get-Module:
> Get-Module -listavailable | Select-String "xDSCResourceDesigner"
Installing xDSCResourceDesigner with WMF 5.0
Get-PackageProvider -Name NuGet -ForceBootstrap
Install-Module -Name xDSCResourceDesigner
Building Your New Resource
-
Create a new module, if we don’t already have one for our DSC Resources
New-Item -Path "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\" -Name myDSCResources -ItemType Directory
-
Create the manifest for our new module
New-ModuleManifest -Path "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\myDSCResources\myDSCResources.psd1" -Guid (([guid]::NewGuid()).Guid) -Author "Codeweavers Ltd." -CompanyName "Codeweavers Ltd." -ModuleVersion 1.0 -Description "Codeweavers DSC Resources" -PowerShellVersion 4.0 -FunctionsToExport '*.TargetResource'
-
Define properties, e.g.
$ServiceName = New-xDscResourceProperty -Name ServiceName -Type String -Attribute key $ServicePath = New-xDscResourceProperty -Name ServicePath -Type String -Attribute Write $ServiceAdditionalArgs = New-xDscResourceProperty -Name ServiceAdditionalArgs -Type String -Attribute Write $ServiceStartCondition = New-xDscResourceProperty -Name ServiceStartCondition -Type String -Attribute Write $ServiceStopAction = New-xDscResourceProperty -Name ServiceStopAction -Type String -Attribute Write $NssmPath = New-xDscResourceProperty -Name NssmPath -Type String -Attribute Write $Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent"
-
Create the resource:
New-xDscResource -Name bnNssmService -Property $ServiceName,$ServicePath,$ServiceAdditionalArgs,$ServiceStartCondition,$ServiceStopAction,$NssmPath,$Ensure ` -Path $env:windir\System32\WindowsPowerShell\v1.0\Modules\myDSCResources
-
Add logic to the resultant PowerShell Script Module, e.g. “C:\Windows\System32\WindowsPowerShell\v1.0\Modules\bnDCSResources\DSCResources\bnNssmService\bnNssmService.psm1”. This is what the module looks like before any additional logic is included:
function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." <# $returnValue = @{ ServiceName = [System.String] ServicePath = [System.String] ServiceAdditionalArgs = [System.String] ServiceStartCondition = [System.String] ServiceStopAction = [System.String] NssmPath = [System.String] Ensure = [System.String] } $returnValue #> } function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName, [System.String] $ServicePath, [System.String] $ServiceAdditionalArgs, [System.String] $ServiceStartCondition, [System.String] $ServiceStopAction, [System.String] $NssmPath, [ValidateSet("Present","Absent")] [System.String] $Ensure ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." #Include this line if the resource requires a system reboot. #$global:DSCMachineStatus = 1 } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName, [System.String] $ServicePath, [System.String] $ServiceAdditionalArgs, [System.String] $ServiceStartCondition, [System.String] $ServiceStopAction, [System.String] $NssmPath, [ValidateSet("Present","Absent")] [System.String] $Ensure ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." <# $result = [System.Boolean] $result #> } Export-ModuleMember -Function *-TargetResource
-
This is what the module looks like after we have added some logic:
function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName, [parameter(Mandatory = $true)] [System.String] $NssmPath ) Write-Verbose "Verbose: Checking $NssmPath" Write-Debug "Debug: Test-Path -Path $NssmPath" $nssmInstalled = Test-Path -Path $NssmPath if($nssmInstalled){ Write-Verbose "Verbose: $NssmPath is installed - retrieving details" Write-Debug "Debug: GetPath: & $NssmPath get $ServiceName Application" $ServicePath =& $NssmPath get $ServiceName Application Write-Debug "Debug: GetArgs: & $NssmPath get $ServiceName AppParameters" $ServiceAdditionalArgs = & $NssmPath get $ServiceName AppParameters Write-Debug "Debug: GetStartCondition: & $NssmPath get `"$ServiceName`" Start" $ServiceStartCondition = & $NssmPath get "$ServiceName" Start Write-Debug "Debug: GetStopAction: & $NssmPath set `"$ServiceName`" Appexit Default" $ServiceStopAction = & $NssmPath set "$ServiceName" Appexit Default } else { Write-Verbose "Verbose: $NssmPath is not installed" } Write-Debug "Debug: Set return value" $returnValue = @{ ServiceName = $ServiceName ServicePath = $ServicePath ServiceAdditionalArgs = $ServiceAdditionalArgs ServiceStartCondition = $ServiceStartCondition ServiceStopAction = $ServiceStopAction NssmPath = $NssmPath Ensure = $ensureResult } $returnValue } function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName, [System.String] $ServicePath, [System.String] $ServiceAdditionalArgs, [System.String] $ServiceStartCondition, [System.String] $ServiceStopAction, [parameter(Mandatory = $true)] [System.String] $NssmPath, [ValidateSet("Present","Absent")] [System.String] $Ensure ) Write-Verbose "Verbose: Checking $NssmPath" Write-Debug "Debug: Test-Path -Path $NssmPath" $nssmInstalled = Test-Path -Path $NssmPath if(!$nssmInstalled) { Write-Verbose "Verbose: $NssmPath is not installed" Write-Debug "Debug: Throwing Exception" throw [System.IO.FileNotFoundException]"$NssmPath not found. Hint: Use File resource to copy NSSM to appropriate location on target host" } Write-Verbose "Verbose: Remove $ServiceName Service - ignore any errors" if($PSCmdlet.ShouldProcess("Removing existing installation of $ServiceName")){ Write-Debug "Debug: ErrorActionPreference = `"SilentlyContinue`"" $_ErrorActionPreference = $ErrorActionPreference ; $ErrorActionPreference = "SilentlyContinue" Write-Debug "Debug: & $NssmPath remove `"$ServiceName`" confirm" & $NssmPath remove "$ServiceName" confirm Write-Debug "Debug: ErrorActionPreference = $_ErrorActionPreference" $ErrorActionPreference = $_ErrorActionPreference } Write-Verbose "Verbose: Testing `"Ensure`" property: $Ensure" if($Ensure -eq "Present") { Write-Verbose "Verbose: Property `"Ensure`" equals `"Present`"; Proceed with installation/configuration" if($PSCmdlet.ShouldProcess("Installing $ServiceName")){ Write-Debug "Debug: & $NssmPath install `"$ServiceName`" `"$ServicePath`" $ServiceAdditionalArgs" & $NssmPath install "$ServiceName" "$ServicePath" $ServiceAdditionalArgs if($LastExitCode -ne 0){ Write-Debug "Debug: LastExitCode not 0: $LastExitCode`nDebug: Throwing Exception" throw [System.Exception]"Return Code: $LastExitCode `n Failed Nssm Service Install" } } if($PSCmdlet.ShouldProcess("Applying the Start Condition `"$ServiceStartCondition`" to $ServiceName")){ Write-Debug "Debug: & $NssmPath set `"$ServiceName`" Start $ServiceStartCondition" & $NssmPath set "$ServiceName" Start $ServiceStartCondition if($LastExitCode -ne 0){ Write-Debug "Debug: LastExitCode not 0: $LastExitCode`nDebug: Throwing Exception" throw [System.Exception]"Return Code: $LastExitCode `n Failed Nssm Set Parameter" } } if($PSCmdlet.ShouldProcess("Applying the Service Stop Action `"$ServiceStopAction`" to $ServiceName")){ Write-Debug "Debug: & $NssmPath set `"$ServiceName`" Appexit Default $ServiceStopAction" & $NssmPath set "$ServiceName" Appexit Default $ServiceStopAction if($LastExitCode -ne 0){ Write-Debug "Debug: LastExitCode not 0: $LastExitCode`nDebug: Throwing Exception" throw [System.Exception]"Return Code: $LastExitCode `n Failed Nssm Set Parameter" } } } else { Write-Verbose "Verbose: Property `"Ensure`" equals `"Absent`"; No further work to do" } #Include this line if the resource requires a system reboot. #$global:DSCMachineStatus = 1 } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [System.String] $ServiceName, [System.String] $ServicePath, [System.String] $ServiceAdditionalArgs, [System.String] $ServiceStartCondition, [System.String] $ServiceStopAction, [parameter(Mandatory = $true)] [System.String] $NssmPath, [ValidateSet("Present","Absent")] [System.String] $Ensure ) Write-Verbose "Verbose: Checking path $NssmPath" Write-Debug "Debug: Test-Path -Path $NssmPath" $nssmInstalled = Test-Path -Path $NssmPath if($nssmInstalled) { Write-Verbose "Verbose: $NssmPath is installed - proceed to next check" } else { Write-Verbose "Verbose: $NssmPath is not installed. Terminate here" $returnValue = ($false -eq ($Ensure -eq "Present")) return $returnValue } Write-Verbose "Verbose: Checking whether $ServiceName is installed and configured with correct start condition/stop action" Write-Debug "Debug: ErrorActionPreference = `"SilentlyContinue`"" $_ErrorActionPreference = $ErrorActionPreference ; $ErrorActionPreference = "SilentlyContinue" Write-Debug "Debug: if(((& $NssmPath get `"$ServiceName`" Start) -eq $ServiceStartCondition) -and`n`t((& $NssmPath get `"$ServiceName`" Appexit Default) -eq $ServiceStopAction))" $serviceInstalledConfigged = ((& $NssmPath get "$ServiceName" Start) -eq $ServiceStartCondition) -and` ((& $NssmPath get "$ServiceName" Appexit Default) -eq $ServiceStopAction) Write-Debug "Debug: ErrorActionPreference = $_ErrorActionPreference" $ErrorActionPreference = $_ErrorActionPreference if($serviceInstalledConfigged) { Write-Verbose "Verbose: $ServiceName is installed and correctly configured" $returnValue = ($true -eq ($Ensure -eq "Present")) } else { Write-Verbose "Verbose: $ServiceName is either not installed or incorrectly configured" $returnValue = ($false -eq ($Ensure -eq "Present")) } Write-Verbose "Verbose: Returning $returnValue" return $returnValue } Export-ModuleMember -Function *-TargetResource
-
You can test your new resource is recognised as a DSC resource
> Get-Module -name myDSCResources ModuleType Version Name ExportedCommands ---------- ------- ---- ---------------- Manifest 1.0 myDSCResources > (Get-DscResource -Name bnNssmService).Properties Name PropertyType IsMandatory Values ---- ------------ ----------- ------ NssmPath [string] True {} ServiceName [string] True {} DependsOn [string[]] False {} Ensure [string] False {Absent, Present} PsDscRunAsCredential [PSCredential] False {} ServiceAdditionalArgs [string] False {} ServicePath [string] False {} ServiceStartCondition [string] False {} ServiceStopAction [string] False {}
Using Your New Resource
Import the module containing your new resource into a DSC script and then simply use the new resource within the “Node” section.
Import-DscResource -ModuleName 'myDSCResources'
bnNssmService Unison
{
NssmPath="C:\Unison\nssm-2.23\win64\nssm.exe"
ServiceName="Unison"
Ensure="Present"
ServiceAdditionalArgs="-socket 874"
ServicePath="C:\Unison\unison.exe"
ServiceStartCondition="SERVICE_DEMAND_START"
ServiceStopAction="Exit"
}
Further Info
Some of the references used in building my first resource:
- https://www.powershellgallery.com/packages/xDSCResourceDesigner/
- http://blogs.msdn.com/b/powershell/archive/2013/11/19/resource-designer-tool-a-walkthrough-writing-a-dsc-resource.aspx
- http://www.powershellmagazine.com/2015/07/02/creating-a-simple-dsc-resource/
Gotchas
Resources Not On Target Node
A DSC resource needs to be copied to a target host before it can be used. If you are using WMF 5.0 (Windows 8.1/Server 2012 r2 have WMF 4.0 pre-installed by default) and you use a library available from the PowerShell Gallery, then PowerShellGet should retrieve any PowerShell Gallery modules that you use almost automagically. Why ‘almost’? That’s because you need to install ‘‘‘nuget-anycpu.exe’’’ on the target node first. That can be accomplished by
Get-PackageProvider -Name NuGet -ForceBootstrap
If you are using WMF 4.0 or a custom resource, then you will need to copy the module to the target host manually. We are using Group Policy to pre-stage everything needed for our DSC configuration to the ‘‘‘C:\DSC’’’ folder on each target node. But you could, for example use a DSC script to pre-stage a target host, for example: Configuration PreStageIisWebServer { Import-DscResource –ModuleName ’PSDesiredStateConfiguration’
Node "MyWebServer"
{
File bnDSCModuleReplace
{
SourcePath = "\\win2k12r2-dc1\dsc$\myDSCResources"
DestinationPath = "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\myDSCResources"
Recurse = $true
Type = "Directory"
Force= $true
Ensure="Present"
}
}
}
Set-Location -Path "D:\Code\infrastructure\Scripts\DesiredStateConfiguration\ManagedObjectFiles"
PreStageIisWebServer
Note that the source path needs to be a network share that the machine account on the target host has access too. See the “DSC Runs Under Local System Account” gotcha.
DSC Runs Under Local System Account
When DSC is applied to a target host, it runs on the target host under the local system account. This can make things like accessing network shares problematic without some pre-configuration. In order for DSC running on a target node to access a network share, you will need need to give the target hosts machine account the relevant permissions on that share. You can accomplish this from the command line on the server (or remotely via winrs or PsExec) as follows:
net share MyFolder=d:\dsc\staging /GRANT:>domain name<\>machine name<$,READ
Where >domain name< and >machine name< are subsituted for your Domain and your DSC target computer name.
The PSDesiredStateConfiguration File Resource
Does not copy files from source to destination when the source files already exist at the target location. Even if the source files have changed.
Exception Handling
In my experience so far, non-terminating powershell errors can turn into fugly uncaught exceptions. Especially when using Test-TargetResource
within a custom resource and the test involves an unavoidable exception when the test fails. Either use $ErrorActionPreference="Stop"
and handle the logic within a try-catch or use $ErrorActionPreference="SilentlyContinue"
(N.B. once you have executed the potentially failing statements, you may want to set $ErrorActionPreference
back to it’s original value.
References
- The DSC Book courtesy of Powershell.org
- The Technet DSC Blog Series, Part 1: Learning about DSC