Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2015/development-environments-with-powershell-dsc-part-2

Development environments with PowerShell DSC – Windows Features

Published 19 October 2015
Updated 25 August 2016
This is post 2 of 7 in a series titled Development environments with PowerShell DSC

I kicked off this series last week with a look at what PowerShell DSC is. This week I'm going to look at scripts you can use for basic configuration of Windows itself for a hypothetical development environment. I'll cover basic Windows features, a few issues you might encounter and helpful tweaks like being able to disable IE's "enhanced security" mode if you're working on a server.

Before I get going, couple of notes about the examples I'm including in this series of posts: They all have a call to `Start-DSCConfiguration` using the "-Force" flag to make DSC run this configuration immediately in "push" mode. However, the `Configuration` blocks declared should all work in pull configurations. Writing the scripts this way just makes them easier for people to try out without any other setup effort. Also the examples are all pretty much self-contained, so the overall configuration of a server using them would rely on many separate scripts. You don't necessarily need to have as many as I'm showing here. It's more to keep the examples clear and usable than to represent the "right" architecture. You're free to merge things together, or even break them apart further if that suits you.

Operating systems

The beginning of the setup for any machine involves getting the basic O/S installed. I'm going to leave this step out of this post, as it's so dependent on your particular flavour of virtualisation technology or hardware. You can install Windows on physical machines from a CD, from an ISO image, or from a USB drive. If you're virtualising your machine you can use most VM software to clone an existing base VM image, use tools like Packer to build images for you, or just install from a CD/ISO/USB as well.

Whichever way you choose to go, the rest of my write-up assumes you have a base install of Windows Server 2012. Many of these techniques will work on other versions of Windows (client and server) – you just need to ensure that your base install includes PowerShell 4 and WMI 4.

Getting your OS Features sorted out

When you set up a copy of Windows Server, the default behaviour these days is to install the basics, but to leave the setup of any specific features you need to the Server Manager UI after your first boot.

So in order to sort this out for things like IIS and .Net frameworks, we need to have a core setup script. As we saw with last week's post, asking DSC to install a Windows Feature for us is pretty easy with the WindowsFeature resource. You just need to know Windows' internal code name for the feature you want to add. So how do we work those out?

The answer is the Get-WindowsFeature CommandLet in PowerShell. This returns a helpful list of all the features available, along with their current state and their internal name. After running that you might see:

Get-WindowsFeature

Armed with that data, you can go through the requirements you have for features your dev environment will need, and sort out a list to transform into a set of DSC resource requests. Most of these are simple. You just end up with a pretty long script, as you often have to install quite a few features and sub-features.

Depending on what your install critera are, you may be able to simplify this with the IncludeAllSubFeature property of the WindowsFeature resource. This allows you to specify the top-level feature and have DSC install every one of its child features. However if you explicitly need to install just some features, you do seem to need to list them out one by one.

You might end up with something like:

Configuration WindowsFeatureInstall
{
    Node $AllNodes.where{ $_.Role.Contains("WindowsFeatures") }.NodeName
    {
        WindowsFeature WebServer
        {
            Name = "Web-Server"
            Ensure = "Present"
        }
        
        WindowsFeature WebWebServer
        {
            Name = "Web-WebServer"
            Ensure = "Present"
        }
        
        WindowsFeature WebCommonHttp
        {
            Name = "Web-Common-Http"
            Ensure = "Present"
        }

        WindowsFeature WebDefaultDoc
        {
            Name = "Web-Default-Doc"
            Ensure = "Present"
        }

        WindowsFeature WebDirBrowsing
        {
            Name = "Web-Dir-Browsing"
            Ensure = "Present"
        }

        WindowsFeature WebHttpErrors
        {
            Name = "Web-Http-Errors"
            Ensure = "Present"
        }

        WindowsFeature WebStaticContent
        {
            Name = "Web-Static-Content"
            Ensure = "Present"
        }

        WindowsFeature WebHttpRedirect
        {
            Name = "Web-Http-Redirect"
            Ensure = "Present"
        }

        WindowsFeature WebDavPublishing
        {
            Name = "Web-DAV-Publishing"
            Ensure = "Present"
        }

        WindowsFeature WebHttpLogging
        {
            Name = "Web-Http-Logging"
            Ensure = "Present"
        }

        WindowsFeature WebLogLibraries
        {
            Name = "Web-Log-Libraries"
            Ensure = "Present"
        }

        WindowsFeature WebRequestMonitor
        {
            Name = "Web-Request-Monitor"
            Ensure = "Present"
        }

        WindowsFeature WebHttpTracing
        {
            Name = "Web-Http-Tracing"
            Ensure = "Present"
        }

        WindowsFeature WebPerformance
        {
            Name = "Web-Performance"
            Ensure = "Present"
        }

        WindowsFeature WebStaticCompression
        {
            Name = "Web-Stat-Compression"
            Ensure = "Present"
        }

        WindowsFeature WebDynamicCompression
        {
            Name = "Web-Dyn-Compression"
            Ensure = "Present"
        }

        WindowsFeature WebSecurity
        {
            Name = "Web-Security"
            Ensure = "Present"
        }

        WindowsFeature WebFiltering
        {
            Name = "Web-Filtering"
            Ensure = "Present"
        }

        WindowsFeature WebBasicAuth
        {
            Name = "Web-Basic-Auth"
            Ensure = "Present"
        }

        WindowsFeature WebCertProvider
        {
            Name = "Web-CertProvider"
            Ensure = "Present"
        }

        WindowsFeature WebClientAuth
        {
            Name = "Web-Client-Auth"
            Ensure = "Present"
        }

        WindowsFeature WebDigestAuth
        {
            Name = "Web-Digest-Auth"
            Ensure = "Present"
        }

        WindowsFeature WebUrlAuth
        {
            Name = "Web-Url-Auth"
            Ensure = "Present"
        }

        WindowsFeature WebWindowsAuth
        {
            Name = "Web-Windows-Auth"
            Ensure = "Present"
        }

        WindowsFeature WebAppDev
        {
            Name = "Web-App-Dev"
            Ensure = "Present"
        }

        WindowsFeature WebNetExt
        {
            Name = "Web-Net-Ext"
            Ensure = "Present"
        }

        WindowsFeature WebNetExt45
        {
            Name = "Web-Net-Ext45"
            Ensure = "Present"
        }

        WindowsFeature WebAspNet
        {
            Name = "Web-Asp-Net"
            Ensure = "Present"
        }

        WindowsFeature WebAspNet45
        {
            Name = "Web-Asp-Net45"
            Ensure = "Present"
        }

        WindowsFeature WebIsapiExt
        {
            Name = "Web-ISAPI-Ext"
            Ensure = "Present"
        }

        WindowsFeature WebIsapiFilter
        {
            Name = "Web-ISAPI-Filter"
            Ensure = "Present"
        }

        WindowsFeature WebMgmtConsole
        {
            Name = "Web-Mgmt-Console"
            Ensure = "Present"
        }

        WindowsFeature NetFrameworkFeatures
        {
            Name = "NET-Framework-Features"
            Ensure = "Present"
        }

        WindowsFeature NETFrameworkCore
        {
            Name = "NET-Framework-Core"
            Ensure = "Present"
        }

        WindowsFeature NETFramework45Features
        {
            Name = "NET-Framework-45-Features"
            Ensure = "Present"
        }

        WindowsFeature NETFramework45Core
        {
            Name = "NET-Framework-45-Core"
            Ensure = "Present"
        }

        WindowsFeature NETFramework45ASPNET
        {
            Name = "NET-Framework-45-ASPNET"
            Ensure = "Present"
        }
    }
}

WindowsFeatureInstall -ConfigurationData "configData.psd1"
Start-DscConfiguration -Path .\WindowsFeatureInstall -Verbose -Wait -Force

					

This (big) example is ensuring you have key IIS features and the ASP.Net v4 runtime features ready for running websites.

It's making reference to a config data file. I talked a bit about this last week, but for the purposes of the examples in this post, all it needs to define is the server / role data I talked about before. For example:

@{
    AllNodes = @(
        @{
            NodeName = "WIN-AQEKG7L9SE8"
            Role = "Setup, DesktopExperience, WindowsFeatures, IE, SqlServer, MongoDB, Sitecore"
         }
    );
}

					

Future posts will extend this with more data. But for the moment, that can be saved as configData.psd1 and the examples here will consume it.

Windows Feature edge cases

Some features are a little bit more tricky however. One specific one that's an issue for Sitecore developers is the "Desktop Experience" feature. This seems a bit oddly named, but if you're running Sitecore on a Windows Server OS it is required to make the WebDAV-based drag-and drop uploading of Media Library resources work. As the linked post suggests, you can just ignore it. But if you need WebDAV to work, beware that this feature seems to always require a reboot.

In the short time I've spent with DSC so far, I have not managed to work out a "nice" DSC-based approach to handling this reboot work. There are a variety of tricks you can use, mostly based around breaking out these reboot-requiring features to a separate script. It only requires one feature, so it's a trivial script:

Configuration DesktopExperienceInstall
{
    Node $AllNodes.where{ $_.Role.Contains("DesktopExperience") }.NodeName
    {
        WindowsFeature WebDavDesktopExperience
        {
            Name = "Desktop-Experience"
            Ensure="Present"
        }
    }
}

DesktopExperienceInstall -ConfigurationData "configData.psd1"
Start-DscConfiguration -Path .\DesktopExperienceInstall -Verbose -Wait -Force

					

You can execute this, trigger a reboot (either manually or via the Restart-Computer CommandLet) and then run your other scripts to finish the process. A colleague of mine has put together some interesting script that makes use of workflows to enable this without user-interaction. However I plan to come back to this issue to discover the "proper DSC" approach when I've had some more time to work on it.

Adding 3rd party extensions to DSC

Sometimes DSC can't do all the things you need it to by default.

The default install of Windows Server has Internet Explorer configured in a strongly locked-down state. (Referred to as IE's "Enhanced Security Configuration") This makes sense if the machine is going to run as a proper server, as it helps to reduce the surface area for attacks. But it's a bit of a pain if you're using a server for development tasks, as it makes IE very difficult to use to test your websites. Hence turning this feature off can be helpful for setting up a development machine.

Now, out of the box DSC doesn't have a resource for making this config change. But an extension exists to enable it. So this is a good example for showing how you can add 3rd party resources into your DSC-based processes. These extensions are just PowerShell Modules that implement a DSC Resource, so they can be installed simply via DSC. For example, the extension that contains the resource for disabling IE's security config is called xSystemSecurity. Generally DSC extensions prefixed with "x" are "experimental" bits of code that Microsoft are considering adding into the core of DSC in future versions. The module can be downloaded from the Technet Gallery.

So to make this configuration change we need to do two things on our target server. First we need to install the xSystemSecurity module and then we need to make use of it.

This is the first point where we've come across a DSC script having a dependency on an extrnal file, so it's a good point to introduce an approach to dealing with these. Generally, you'll want to have a central location for any files your scripts depend on. That way we can pass in this location, and have the scripts download their dependencies onto whatever machine they're executing on. Achieving this needs us to make a one important change to how we're working.

A DSC Configuration can use exactly the same syntax for passing in command-line parameters as an ordinary PowerShell script function. So we can declare a -PackagePath parameter. This can then be used to locate the xSystemSecurity package (which happens to be supplied as a zip file) and uncompress it to the right location.

A DSC script to achieve this might look like:

Configuration SetupInstall
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PackagePath
    )

    Node $AllNodes.where{ $_.Role.Contains("Setup") }.NodeName
    {
        Archive AddxSystemSecurity
        {
            Path = "$PackagePath\xSystemSecurity_1.0.zip"
            Destination = "$Env:ProgramFiles\WindowsPowerShell\Modules"
        }
    }
}

SetupInstall -ConfigurationData "configData.psd1" -PackagePath "\\VBoxSvr\dsc\Setup"
Start-DscConfiguration -Path .\SetupInstall -Verbose -Wait -Force

					

The param() declares a required paramater of type string called $PackagePath which we can refer to elsewhere in our script. The Archive resource type knows how to unzip an archive file for us. We specify the source path using our $PackagePath variable, and the target path using the special PowerShell syntax for accessing Environment Variables. This allows us to find the machines's "Program Files" folder without worrying about it's actual path.

Note that the call to invoke SetupInstall now takes this extra parameter specifying where our central repository of files is. That means the parameter's data is available when the .MOF file is being generated by DSC.

It's worth noting that you have to be a bit careful about disk permissions with your shares here. The underlying engine which runs the DSC Scripts is always running as the "System" account. Hence if you have a remote share that implements security you will need to pass -Credential parameter that the Configuration accepts when you invoke it. The value of the parameter needs to be a System.Management.Automation.PSCredential object for a user account which does have permissions to access the share.

Back to IE Enhanced Security Configuration

So now we've added the extra DSC feature required to modify the security settings in IE, how do we actually change them? Simple. You just need to tell DSC you're importing an external Resource and then use it:

Configuration EnableIEEsc
{
    Import-DSCResource -Module xSystemSecurity -Name xIEEsc

    Node $AllNodes.where{ $_.Role.Contains("IE") }.NodeName
    {
        xIEEsc EnableIEEscAdmin
        {
            IsEnabled = $false
            UserRole  = "Administrators"
        }

        xIEEsc EnableIEEscUser
        {
            IsEnabled = $false
            UserRole  = "Users"
        }
    }

}

EnableIEEsc -ConfigurationData "configData.psd1"
Start-DscConfiguration -Path .\EnableIEEsc -Verbose -Wait -Force

					

The Import-DSCResource CommandLet tells DSC where to find the external code we're calling. And from then on the new xIEEsc resource is available to use. Here we're calling it twice to turn off enhanced security for both admin and ordinary users on the server.

With that done we can start to think about the other pre-requisites of Sitecore. So next week's post will look at adapting my previous work on automating a MongoDB install using DSC.

↑ Back to top