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!