Chef and PowerShell DSC Overview

Summary

PowerShell DSC is a very valuable extension to Chef Cookbooks for configuring and provisioning Windows Nodes for the following reasons:

  • It can be assumed that DSC Resources will be updated sooner, more stable, and more powerful than the equivalent Windows specific Chef Resources. Primarily because many of the DSC Resources are developed by Microsoft, for Microsoft platforms and applications.
  • Base DSC Resources (e.g. ‘file’, ‘registry’, ‘environment’, ‘script’, ‘feature’) are available by default to Chef Recipes for Nodes that have the Windows Management Framework v.4 (WMF 4) installed.
  • DSC includes a wealth of extension Resources that can simplify and stabilize a Chef recipe when installing or configuring Windows platform components.
  • Chef makes it easier to leverage DSC Resources than a traditional PowerShell DSC infrastructure for the following reasons:
    • No DSC server is required
    • DSC extension Resources don’t require implementation on the server (packaging and checksums need to be manually created for each extension Resource)
    • The Node doesn’t need to be configured for as a DSC Node (WMF v.4+ is still required on the Node)
    • DSC Resources can be executed on the fly by Chef-Client on the Node (rather than waiting for the COM objects to get exercised by the DSC Scheduled Tasks which can lead to inconsistent results when invoked manually)

Considerations:

  • Chef-Client v.12+ is required on the Node
  • WMF v.4+ is required on the Node
  • Using the Chef Resource ‘dsc_resource’ requires WMF v.5+ (this analysis is focusing on the Chef Resource ‘dsc_script’). WMF 5 won’t RTM until Windows 10, but will be available downlevel to Windows Server 2008 and beta versions are currently available down to Windows Server 2008 R2
  • Using DSC extension Resources requires DSC extension Resource management within the cookbook (e.g. by using the Chef Resource ‘remote_diretory’ in order to make sure DSC extension Resources are available on the Node when called by the Chef Recipe) where this is handled automatically be DSC Server infrastructure.

Chef Resource and DSC Resource Comparison

The following comparison is not meant to argue the benefit of using DSC Resources over Chef Resources, rather these examples are meant to demonstrate the difference in code complexity when performing Windows platform actions from Chef vs. the PowerShell DSC server. Considering whether or not to use DSC Resources might also include weighing the decision to develop recipes that are platform dependent since there are no plans for DSC Resources to include platforms other than Windows. In addition, these examples don’t include any Chef Resources other than the ‘powershell_script’ Resource. There are many Windows specific Chef resources that can also simplify Chef Recipes.

The following examples demonstrate enabling remote access via RDP to the Node.

Example: Leveraging the Chef ‘powershell_script’ Resource
# Enables remote desktop access to the server
powershell_script "enable-remote-desktop" do
  guard_interpreter :powershell_script
  code <<-EOH
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -name "fDenyTSConnections" -Value 0
    Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -name "UserAuthentication" -Value 1
    eventcreate /t INFORMATION /ID 2 /L APPLICATION /SO "Chef-Client" /D "Enabled Remote Desktop access"
  EOH
  only_if <<-EOH
    if ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server').fDenyTSConnections -ne "0") {
      $true
    } elseif ((Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP").Enabled -ne "True") {
      $true
    } elseif ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp').UserAuthentication -ne "1") {
      $true
    } else {
      $false
    }
  EOH
end
Example: PowerShell DSC Script (Recipe) Using a DSC Resource
# Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
xRemoteDesktopAdmin EnableRDP
{
  Ensure = "Present"
  UserAuthentication = "Secure"
}
 
xFirewall AllowRDP
{
    Name = 'DSC - Remote Desktop Admin Connections'
    DisplayGroup = "Remote Desktop"
    Ensure = 'Present'
    State = 'Enabled'
    Access = 'Allow'
    Profile = ("Domain", "Private", "Public")
}
Example: Chef Recipe Using a DSC Resource from ‘dsc_script’
dsc_script "Configure Server" do
  imports 'xRemoteDesktopAdmin'
  imports 'xNetworking'
  code <<-SITESDSC
    # Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
    xRemoteDesktopAdmin EnableRemoteDesktop
    {
      Ensure = "Present"
      UserAuthentication = "Secure"
    }
 
    xFirewall AllowRDP
    {
        Name = 'DSC - Remote Desktop Admin Connections'
        DisplayGroup = "Remote Desktop"
        Ensure = 'Present'
        State = 'Enabled'
        Access = 'Allow'
        Profile = ("Domain", "Private", "Public")
    }
  SITESDSC
end
Example: Adding DSC Resource Extension Management in Chef Recipes

The following Chef Recipe snipit demonstrates how 2 DSC extension Resources are managed on the Node. The ‘remote_directory’ merely insures that the ‘xRemoteDesktopAdmin’ and ‘xNetworking’ resources are installed on the Node so they can be leveraged by the Chef Recipe.

remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xRemoteDesktopAdmin" do
  source "xRemoteDesktopAdmin"
  action :create
end
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xNetworking" do
  source "xNetworking"
  action :create
end
Appendix: Full Chef Recipe ‘dsc_script.rb’

The following recipe performs actions on a Windows Server that fall under the topic of general server configuration.

# Requires Chef-Client v.12+
# Author: Otto Helweg
 
include_recipe 'ws2012r2::lcm_setup_dsc_script'
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xRemoteDesktopAdmin" do
  source "xRemoteDesktopAdmin"
  action :create
end
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xNetworking" do
  source "xNetworking"
  action :create
end
 
dsc_script "Configure Server" do
  imports 'xRemoteDesktopAdmin'
  imports 'xNetworking'
  code <<-SITESDSC
    # Leaves a timestamp indicating the last time this recipe was run on the node (this resource in intentionally not idempotent)
    Environment LeaveTimestamp
    {
      Ensure = "Present"
      Name = "DSCClientRun"
      Value = "Last PowerShell DSC run (UTC): " + (Get-Date).ToUniversalTime().ToString()
    }
 
    # Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
    xRemoteDesktopAdmin EnableRemoteDesktop
    {
      Ensure = "Present"
      UserAuthentication = "Secure"
    }
 
    xFirewall AllowRDP
    {
        Name = 'DSC - Remote Desktop Admin Connections'
        DisplayGroup = "Remote Desktop"
        Ensure = 'Present'
        State = 'Enabled'
        Access = 'Allow'
        Profile = ("Domain", "Private", "Public")
    }
 
    # Disables checking for updates
    Script DisableUpdates
    {
      SetScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        $WUSettings.NotificationLevel = 1
        $WUSettings.save()
        eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "DSC-Client" /D "Disabled Checking for Updates"
      }
      TestScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        if ($WUSettings.NotificationLevel -ne "1") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Verifies Windows Remote Management is Configured or Configures it
    Script EnableWinrm
    {
      SetScript = {
        Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
        eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "DSC-Client" /D "Enabled Windows Remote Management"
      }
      TestScript = {
        try{
          # Use to remove a listener for testing
          # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"}
          Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
          return $true
        } catch {
          #$wsmanOutput = "WinRM doesn't seem to be configured or enabled."
          return $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Installs the Applicaiton-Server Role
    Script InstallAppServer-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "DSC-Client" /D "Installed Role: Applicaiton-Server"
      }
      TestScript = {
        if ((Get-WindowsFeature -Name Application-Server).Installed) {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    WindowsFeature InstallAppServer-Step1
    {
      Name = "Application-Server"
      Ensure = "Present"
      IncludeAllSubFeature = $true
    }
 
    WindowsFeature InstallAppServer-Step2
    {
      Name = "AS-Web-Support"
      Ensure = "Present"
      IncludeAllSubFeature = $true
      DependsOn = "[WindowsFeature]InstallAppServer-Step1"
    }
 
    # Disables Shutdown tracking (asking for a reason for shutting down the server)
    Script DisableShutdownTracking-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 7 /L APPLICATION /SO "DSC-Client" /D "Disabled Shutdown Tracking"
      }
      TestScript = {
        if ((Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Reliability').ShutdownReasonOn -ne "0") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    Registry DisableShutdownTracking
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Reliability"
      ValueName = "ShutdownReasonOn"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
 
    # Disables automatic maintenance
    Script DisableAutomaticMaintenance
    {
      SetScript = {
        $taskList = @("Idle Maintenance","Maintenance Configurator","Manual Maintenance","Regular Maintenance")
        $schTasks = Get-ScheduledTask
        foreach ($task in $schTasks) {
          if (($task.TaskPath -eq "\\Microsoft\\Windows\\TaskScheduler\\") -and ($taskList.Contains($task.TaskName))) {
            Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath "\\Microsoft\\Windows\\TaskScheduler\\" -Confirm:$false
          }
        }
        eventcreate /t INFORMATION /ID 8 /L APPLICATION /SO "DSC-Client" /D "Disables automatic maintenance"
      }
      TestScript = {
        $taskList = @("Idle Maintenance","Maintenance Configurator","Manual Maintenance","Regular Maintenance")
        $schTasks = Get-ScheduledTask
        foreach ($task in $taskList) {
          if ($schTasks.TaskName.Contains($task)) {
            $found = $true
          }
        }
        if ($found = $true) {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
  SITESDSC
end

Enjoy!

PowerShell DSC Script Example – Node Configuration

The following PowerShell DSC script was converted from a Chef recipe for Windows located here. It’s easy to see that there’s a lot of similarity to the logic that PowerShell DSC and Chef use regarding their resources and provisioning or configuring as node (server). PowerShell DSC adds an additional feature in their Script Resource called ‘GetScript’ which handles gathering information from the Node (not used in this example) for use elsewhere in the Script Resource (must be passed back as a hash). In addition, idempotency is managed by ‘TestScipt’ passing back either $true or $false, where $false will cause ‘SetScript’ to execute. Lastly, all variables must be defined within a Script Resource (most commonly a script block) and not outside of the resource, otherwise they will be evaluated and stored at the time of the MOF file generation.

The following script demonstrates some of the server configuration functionality via PowerShell’s new DSC service, and performs the following actions:

  1. Timestamp: A Windows Event is created and an environmental variable is updated to show the last time this recipe was executed on the server.
  2. Enable Remote Desktop: The Remote Desktop (RDP) functionality is enabled.
  3. Disable Windows Updates: Windows automatic updates are disabled (use with caution – unless you have another update process, don’t do this).
  4. Enable Windows Remote Management: The Windows Remote Management functionality is enabled.
  5. Install IIS: The Application Server and Web Server roles are installed and enabled.
  6. Disable Server Reboot Tracking: A minor registry key is added/updated to stop the default practice of asking for a reboot reason on Windows Server.

DSC Publishing Steps:

The following steps are taken to publish a MOF file that’s ready to be consumed by nodes:

  1. PowerShell DSC script is written (below) and executed.
  2. In the current working directory (important!) a MOF file is created, representing the resources called out in the DSC script.
  3. The MOF file is named after the ‘Node’, in a sub-directory named after the ‘Configuration’ call (e.g. in this case it would be named ‘.\ServerConfig\Anybody.mof’)
    The MOF file is copied to the PowerShell DSC server’s hosting directory.
  4. A sister CheckSum file is generated for the newly created MOF file.
  5. Nodes will the download the MOF file via a REST call, specifying a specific MOF file by GUID (a node can only be configured to download a single MOF file). This will be covered in another blog.

DSC Script:

Configuration ServerConfig
{
  Node Anybody
  {
    # Leaves a timestamp indicating the last time this script was run on the node (this resource is intentionally not idempotent)
    Script LeaveTimestamp
    {
      SetScript = {
        $currentTime = Get-Date
        $currentTimeString = $currentTime.ToUniversalTime().ToString()
        [Environment]::SetEnvironmentVariable("DSCClientRun","Last DSC-Client run (UTC): $currentTimeString","Machine")
        eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "DSC-Client" /D "Last DSC-Client run (UTC): $currentTimeString"
      }
      TestScript = {
        $false
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Enables remote desktop access to the server
    Registry EnableRDP-Step1
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server"
      ValueName = "fDenyTSConnections"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
 
    Registry EnableRDP-Step2
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
      ValueName = "UserAuthentication"
      ValueData = "1"
      ValueType = "Dword"
      Force = $true
    }
 
    Script EnableRDP
    {
      SetScript = {
        Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
        eventcreate /t INFORMATION /ID 2 /L APPLICATION /SO "DSC-Client" /D "Enabled Remote Desktop access"
      }
      TestScript = {
        if ((Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP").Enabled -ne "True") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Disables checking for updates
    Script DisableUpdates
    {
      SetScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        $WUSettings.NotificationLevel = 1
        $WUSettings.save()
        eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "DSC-Client" /D "Disabled Checking for Updates"
      }
      TestScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        if ($WUSettings.NotificationLevel -ne "1") {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Verifies Windows Remote Management is Configured or Configures it
    Script EnableWinrm
    {
      SetScript = {
        Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
        eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "DSC-Client" /D "Enabled Windows Remote Management"
      }
      TestScript = {
        try{
          # Use to remove a listener for testing
          # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"}
          Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
          return $true
        } catch {
          #$wsmanOutput = "WinRM doesn't seem to be configured or enabled."
          return $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Installs the Applicaiton-Server Role
    Script InstallAppServer-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "DSC-Client" /D "Installed Role: Applicaiton-Server"
      }
      TestScript = {
        if ((Get-WindowsFeature -Name Application-Server).Installed) {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    WindowsFeature InstallAppServer-Step1
    {
      Name = "Application-Server"
      Ensure = "Present"
      IncludeAllSubFeature = $true
    }
 
    WindowsFeature InstallAppServer-Step2
    {
      Name = "AS-Web-Support"
      Ensure = "Present"
      IncludeAllSubFeature = $true
      DependsOn = "[WindowsFeature]InstallAppServer-Step1"
    }
 
    # Disables Shutdown tracking (asking for a reason for shutting down the server)
    Script DisableShutdownTracking-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 7 /L APPLICATION /SO "DSC-Client" /D "Disabled Shutdown Tracking"
      }
      TestScript = {
        if ((Get-ItemProperty -Path 'HKLM:\\SOFTWARE\Policies\Microsoft\Windows NT\Reliability').ShutdownReasonOn -ne "0") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    Registry DisableShutdownTracking
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Reliability"
      ValueName = "ShutdownReasonOn"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
  }
}
 
# The MOF filename must be a GUID. It can be any unique GUID and can be generated by the following PowerShell command ([guid]::NewGuid()).
$guid = "45b51dc8-132c-4052-8e3b-479c73d4c9cc"
 
# Create the MOF file from the above PowerShell DSC script
ServerConfig
 
# Used to copy the newly generated MOF file in the Pull Server's publishing location
$mofFile = "c:\dscscripts\ServerConfig\Anybody.mof"
$mofPath = "C:\Program Files\WindowsPowerShell\DscService\Configuration"
$DSCMofFile = $mofPath + "\" + $guid + ".mof"
 
Copy-Item $mofFile -Destination $DSCMofFile
# Generate a CheckSum sister file for the MOF file

 New-DSCCheckSum $DSCMofFile -Force
After the DSC script is executed on the node, New Windows Events should be logged to the configured node (server):

DSC-Example-1

And the following web page should be viewable on the node (server):

DSC-Example-2

The following practices are leveraged in this recipe:
  • A time stamp is logged on the server in order to track every time the script is executed via both a Windows Event and a Machine Environmental Variable (the environmental variable doesn’t refresh until you log out and log back in – is CMD getting cached somewhere?). This might be helpful for troubleshooting issues by correlating with the recipe execution timestamp.
  • A Windows Event is logged every time a resource is executed on the node (server). Event IDs are unique to the resource function.
  • All resources (except the timestamp resource) are idempotent. In other words, logic is in place to insure the resource is not executed if it doesn’t need to be. For example, if IIS has already been installed, then there should be no attempt to install it again. This is a ‘best practice’ for writing DSC scripts.
The following PowerShell functionality is exercised by this recipe:
  • An Environmental Variable is created/updated
  • Events are created with the ‘eventcreate’ command. The PowerShell ‘New-Event’ is not used because for Windows Server (not client) you need to register your custom event source with the server, so ‘eventcreate’ is simpler.
  • A Registry Key is added/updated
  • The Enable-FirewallRule cmdlet is used
  • A COM object is accessed and/or updated
  • The Set-WsmanQuickConfig cmdlet is used
  • The Get-WsmanInstance cmdlet is used
  • The Add-WindowsFeature cmdlet is used

Idempotency:

Notice that the Script Resource implements PowerShell script logic (via ‘TestScript’) in order to make the resource follow idempotent best practices (most of the other resources manage this for you). The logic implemented in the Script Resource is a reasonable attempt to determine if action is necessary, while not covering every possible permutation that might exist for a misconfigured server. Also notice how either $true or $false is passed back to the resource from the test logic. And finally, the ‘SetScript’ block is not executed unless ‘$false’ is returned by the ‘TestScript’ logic.

Enjoy!