Ansible vs. Chef for Managing Windows

ConfigureRemotingForAnsible.ps1Summary: Ansible is a simple and powerful application/DevOps framework for managing Windows configuration and provisioning. Getting off the ground with Ansible was also fairly straightforward and doesn’t require a large time or infrastructure investment.

These are some initial thoughts about using Ansible to manage the Windows platform (specifically compared to Chef). This analysis does not consider the ways in which Chef might be a better choice than Ansible for Windows management (they do exist and might be the subject of another blog post in the future).

The Ansible pilot was setup to be able to mimic some existing Chef recipes in order to determine time investment and infrastructure requirements as well as Ansible capabilities. The pilot consisted of an Ansible server (CentOS 6 – 64bit) and a Windows server (Windows Server 2012 Standard Edition) to be managed. Findings were as follows (in no particular order):

Note: Ansible Module  = Chef Resource, Ansible Playbook = Chef Recipe

  • Agentless: Ansible does not use an agent to manage Windows, but merely uses Windows’ built in Windows Remote Management (WinRM) protocol and framework.
  • WinRM Configuration: The PowerShell script ConfigureRemotingForAnsible.ps1 needs to be run on the managed node in order to enable communication with the Ansible server. The script basically configures a custom HTTPS listener with a special certificate.
  • PowerShell 3.0: PowerShell 3.0 or above is required by Ansible. With PowerShell 3.0, the following Hotfix KB2842230 may also need to be installed. On the other hand, Chef works well with PowerShell 2.0. PowerShell 3.0 can be easily updated on versions of Windows Server (pre 2012 came with PowerShell 2.0) by installing the Windows Management Framework (WMF) 3.0.
  • Predictable Execution: Ansible playbooks have a single execution phase, rather than Chef’s compile then execute phases, which, in some cases, can make Chef recipes less predictable. For example in Chef, modifying an environmental variable several times on a managed node within a single run_list will produce unexpected results.
  • Fewer Facts: Far fewer Ansible Facts are discovered at runtime than Ohai Attributes for a Windows host (this is not so with Linux). Chef’s Ohai discovers the same mountain of properties on both Windows and Linux.
  • Parameters: Parameters can be passed into the Ansible Playbook from the command line which is useful for changing Playbook behavior when it’s executed.
  • PowerShell: Ansible runs pure PowerShell scripts “as is”. Chef requires PowerShell scripts to be slightly modified by escaping certain characters. Ansible also simply manages the transfer of the script to the managed node, script execution, and script removal.
  • External File Transfers: The Ansible URL module nicely transfers big files via a URL. This is convenient when using Artifactory or Pydio to pull down large binaries for installation or processing.
  • Unzipping Files: The Ansible ZIP module is simple to use and nicely expands compressed files on the managed node.
  • Variable Passing: Variables can be easily passed out of a PowerShell script to the Playbook (or other modules) during runtime. This is useful when PowerShell is used to dynamically gather or process needed data at runtime. Although Chef easily allows for the passing of Attributes into PowerShell scripts, pulling data back out of those scripts is tricky.
  • Windows Update Works! The Windows Update module works (no permissions issues, I don’t know how Ansible is accomplishing this because this is a known issue with Chef). The problem lies in Windows not granting access to certain internal methods when accessed remotely, even with Administrator credentials. To see how this can be overcome with Chef, go to this blog post on the topic.
  • Reboot Management: Ansible playbooks can easily manage reboots since the Playbook is being run from the Ansible server and not the Node.
  • User Input: An Ansible Playbook can take user input during runtime. For example getting runtime credentials when joining a Windows node to a domain.

Windows Update Playbook Example:

---
# This playbook installs Windows updates
# Run with the following command:
#   ansible-playbook update-win.yml --ask-pass --u Administrator

- name: Configure Server
  hosts: windows
  gather_facts: true
  tasks:
    - name: Install Windows updates
      win_updates:
        category_names: ['SecurityUpdates','CriticalUpdates','UpdateRollups','Updates']

    - name: Restart machine
      raw: shutdown /r /f /c "Ansible updates triggered"
      async: 0
      poll: 0
      ignore_errors: true

    - name: Waiting for server to come back
      local_action: wait_for
                    host={{ inventory_hostname }}
                    state=started
                    timeout=60
      sudo: false

Windows Domain Join Playbook Example:

---
# This playbook joins Windows to a domain
# Run with the following command:
#   ansible-playbook joindomain-win.yml --ask-pass --u Administrator


- name: Join domain
  hosts: windows
  gather_facts: true
  vars_prompt:
    - name: "user"
      prompt: "Domain Join username"
      private: no
    - name: "password"
      prompt: "Domain Join password"
      private: yes
  tasks:
    - name: Join domain script
      script: "files/join-domain.ps1 -u '{{ user }}' -p '{{ password }}'"
      ignore_errors: true

    - name: Waiting for server to come back
      local_action: wait_for
                    host={{ inventory_hostname }}
                    state=started
                    timeout=60
      sudo: false

Windows Domain Join PowerShell Script Example:

#
# Script:: join-domain.ps1
# Joins a sesrver to a domain
#

param($u,$p)

$securePassword = ConvertTo-SecureString -String $p -AsPlainText -Force
$psCreds = new-object -typename System.Management.Automation.PSCredential -argumentlist $u, $securePassword


$domainCheck = (Get-WmiObject -Class win32_computersystem).Domain
if (!($domainCheck -eq "contoso.com")) {
  Add-Computer -DomainName "contoso.com" -Credential $psCreds -Force -Restart

  eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Ansible-Playbook" /D "joindomain-win: Added to the domain 'contoso.com'."
}

Enjoy!

Apply Windows Updates Via a Chef Recipe

Summary

Using Chef (or any other remote management tool for that matter – like Windows Remote Management or PowerShell Remoting) to apply Windows updates to a remote system is difficult because some of the Windows Update methods will not work when executed from a remote connection, even if you’re using Administrator level credentials (this is apparently a feature, not a bug). To get around this, the Chef recipe must launch the update commands via a task in Task Scheduler. This can be done by configuring the Task Scheduler task to call the Chef recipe via the local ‘chef-client’ utility.

In this example I’m creating a task to ‘run once’, but since it’s in the past, this task will never get executed on its own. Then I’m manually launching the newly created task, which just calls the Chef Client to run my InstallWindowsUpdates cookbook (recipe: default.rb).

Create/Execute Task via PowerShell Remoting Example:

Invoke-Command -ComputerName <server name> -Credential <admin credentials> -ScriptBlock { cmd /c "schtasks /Create /RU System /RL HIGHEST /F /TR ""c:\opscode\chef\bin\chef-client.bat -o InstallWindowsUpdates"" /TN ChefInstallUpdates /SC Once /ST 00:00 2>&1" }
Invoke-Command -ComputerName <server name> -Credential <admin credentials> -ScriptBlock { cmd /c "schtasks /run /tn ChefInstallUpdates 2>&1" }

Windows Update Recipe Example (default.rb):

#
# Cookbook Name:: InstallWindowsUpdates
# Recipe:: default
# Author(s):: Otto Helweg
#

# Configures Windows Update automatic updates
powershell_script "install-windows-updates" do
  guard_interpreter :powershell_script
  # Set a 2 hour timeout
  timeout 7200
  code <<-EOH
    Write-Host -ForegroundColor Green "Searching for updates (this may take up to 30 minutes or more)..."

    $updateSession = New-Object -com Microsoft.Update.Session
    $updateSearcher = $updateSession.CreateupdateSearcher()
    try
    {
      $searchResult =  $updateSearcher.Search("Type='Software' and IsHidden=0 and IsInstalled=0").Updates
    }
    catch
    {
      eventcreate /t ERROR /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Update attempt failed."
      $updateFailed = $true
    }

    if(!($updateFailed)) {
      foreach ($updateItem in $searchResult) {
        $UpdatesToDownload = New-Object -com Microsoft.Update.UpdateColl
        if (!($updateItem.EulaAccepted)) {
          $updateItem.AcceptEula()
        }
        $UpdatesToDownload.Add($updateItem)
        $Downloader = $UpdateSession.CreateUpdateDownloader()
        $Downloader.Updates = $UpdatesToDownload
        $Downloader.Download()
        $UpdatesToInstall = New-Object -com Microsoft.Update.UpdateColl
        $UpdatesToInstall.Add($updateItem)
        $Title = $updateItem.Title
        Write-host -ForegroundColor Green "  Installing Update: $Title"
        $Installer = $UpdateSession.CreateUpdateInstaller()
        $Installer.Updates = $UpdatesToInstall
        $InstallationResult = $Installer.Install()
        eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Installed update $Title."
      }

      if (!($searchResult.Count)) {
        eventcreate /t INFORMATION /ID 999 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: No updates available."
      }
      eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Done Installing Updates."
    }
  EOH
  action :run
end

Enjoy!

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!

Configuring Windows with Chef and the PowerShell Resource

The PowerShell Resource in Chef is very powerful and versatile for configuring Windows. It essentially exposes all of the power of PowerShell to Chef. And since in PowerShell v.4, most Server Manager functionality is exposed via PowerShell cmdlets, there isn’t a lot you can’t configure in Windows with PowerShell.

The following recipe demonstrates some of the server configuration functionality via Chef’s PowerShell resource by performing 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.

Chef Recipe:

# Leaves a timestamp indicating the last time this recipe was run on the node (this resource in intentionally not idempotent)
powershell_script "leave-timestamp" do
  code <<-EOH
    $currentTime = Get-Date
    $currentTimeString = $currentTime.ToUniversalTime().ToString()
    [Environment]::SetEnvironmentVariable("ChefClientRun","Last Chef-Client run (UTC): $currentTimeString","Machine")
    eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Client" /D "Last Chef-Client run (UTC): $currentTimeString"
  EOH
end
 
# 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
 
# Disables checking for updates
powershell_script "disable-update-checking" do
  guard_interpreter :powershell_script
  code <<-EOH
    $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
    $WUSettings.NotificationLevel = 1
    $WUSettings.save()
    eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "Chef-Client" /D "Disabled Checking for Updates"
  EOH
  only_if <<-EOH
    $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
    if ($WUSettings.NotificationLevel -ne "1") {
      $true
    } else {
      $false
    }
  EOH
end
 
# Verifies Windows Remote Management is Configured or Configures it
powershell_script "check-winrm" do
  guard_interpreter :powershell_script
  code <<-EOH
    Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
    eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "Chef-Client" /D "Enabled Windows Remote Management"
  EOH
  not_if <<-EOH
    # $wsmanOutput isn't used here, just a placeholder for possible use in the future
    try {
      # Use to remove a listener for testing
      # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"
      $wsmanOutput = Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
      $true
    } catch {
      $wsmanOutput = "WinRM doesn't seem to be configured or enabled."
      $false
    }
  EOH
end
 
# Installs the Application-Server Role
powershell_script "install-application-server" do
  guard_interpreter :powershell_script
  code <<-EOH
    $installResult = Add-WindowsFeature Application-Server,AS-Web-Support
    foreach ($result in $installResult) {
      if ($result.RestartNeeded -eq "Yes") {
        eventcreate /t INFORMATION /ID 5 /L APPLICATION /SO "Chef-Client" /D "Restart is required."
      }
    }
    eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "Chef-Client" /D "Installed Role: Applicaiton-Server"
  EOH
  not_if <<-EOH
    if ((Get-WindowsFeature -Name Application-Server).Installed) {
      $true
    } else {
      $false
    }
  EOH
 end
Your output should look something like this (although I ran this with Test-Kitchen rather than ‘chef-client’ directly):

Note: The check-winrm script did not execute because it was already configured on the node (server).

Chef-PowerShell-1

New Windows Events should be logged to the configured node (server):

Chef-PowerShell-2

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

Chef-PowerShell-3

The following practices are leveraged in this recipe:

  • A time stamp is logged on the server in order to track every time the recipe is executed. 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 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 recipes.

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 nearly all the resources implement PowerShell script logic in order to make their resources follow idempotent best practices. The logic implemented 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. Both conditions need to be addressed by the Guard. Also notice where ‘not_if’ and ‘only_if’ are used.

Enjoy!