Search
Generic filters
Exact matches only
Filter by Custom Post Type

Building a Desired State Configuration Configuration - Part 2

Ok, let's get back to creating a DSC configuration.  If you haven't read the last post in this series, go back and do that now, I'll wait.  Now with that out of the way, let's get back to it...

The High Points

Picking Back UP

Now that we have some of the basics down, we can start to look deeper at how composable these configurations are. A DSC configuration defined in PowerShell offers several advantages, not the least of which is that a configuration can be parameterized.

Parameterization

configuration MyFirstServerConfig
{
	param ([string[]]$NodeName)
	node $NodeName
	{
		WindowsFeature snmp
        {
            Name = 'SNMP-Service'
        }
	}
}

With this simple tweak, I've taken a configuration that was hard-coded to one server name to one that can take an array of server names. The PowerShell savvy are probably going, "Big deal.. functions could do that since Monad". If you remember back in the last post, I showed how ConfigurationData could be used to pass data into a configuration. Then my main configuration did some stuff based on metadata about the node. My configuration was starting to look a bit complicated. The ability to parameterize configurations really helps us when we are ready for the next step, nesting configurations.

Nesting Configurations

Let's start with an example...

$ConfigurationData = @{
  AllNodes = @(
    @{NodeName = 'Server1';Role='Web'},
    @{NodeName = 'Server2';Role='FileShare'}
    @{NodeName = 'Server3';Role=@('FileShare','Web')}
  )
}

configuration RoleConfiguration
{
	param ($Roles)

	switch ($Roles)
    {
        'FileShare' {
                        WindowsFeature FileSharing
                        {
                            Name = 'FS-FileServer'
                        } 
                    }
        'Web'       {
                        WindowsFeature Web
                        {
                            Name = 'web-Server'
                        } 
                    }        
    }
}

configuration MyFirstServerConfig 
{
    node $allnodes.NodeName
    {
        WindowsFeature snmp
        {
            Name = 'SNMP-Service'
        }       

        RoleConfiguration MyServerRoles
        {
        	Roles = $Node.Role
    	}
    }
}

So, what did we just see? I defined a parameterized configuration and then used it like a DSC Resource in my main configuration. Parameters are passed to the nested configuration in the exact same way as to a DSC Resource. This syntax also means that we can use DependsOn to create dependency chains between groups of functionality more easily.

configuration MyFirstServerConfig 
{
    node $allnodes.NodeName
    {
        WindowsFeature snmp
        {
            Name = 'SNMP-Service'
        }       

        RoleConfiguration MyServerRoles
        {
        	Roles = $Node.Role
        	DependsOn = '[WindowsFeature]snmp'
    	}
    }
}

We can leverage this technique of creating nested configurations to simplify our configuration scripts, minimize dependency chains, and provide an easy way to reuse configuration sections for multiple configurations, all using the same semantics of any DSC resource.

Applying Configurations

Once we have our configurations generated, we have a couple of ways to distribute and apply the configurations. We'll start assuming that we have generated our configurations for the servers we would like to target.

Start-DscConfiguration

Our first option is Start-DscConfiguration. We can point Start-DscConfiguration to the configuration files that we've generated (just point to the directory with the configuration files in them).

Start-DscConfiguration -Path ./MyFirstServerConfig

Doing this will attempt to run the configurations generated against any nodes specified. You can target specific servers by using the -computername or -cimsession parameters.

One downside to using Start-DscConfiguration is that any custom resources (not nested configurations) need to be present on the remote node BEFORE applying the configuration.

You CANNOT create a configuration that uses the file resource (or any other resource) to create the resource on disk during the DSC run. While this would be a cool trick, the resources contain a schema.mof file that defines the interface that DSC can use and the DSC engine will error if it cannot find the resource interface when the configuration is validated before it applies. One option is having two-phased configurations, one to distribute resources and the second to apply it.

Pulling a Configuration

The next alternative is to distribute configurations and resources using a pull Server. In box, DSC supports two types of pull server, an REST based pull server (like described in my previous post) and an SMB based pull server (described here). The pull server requires nodes to be labeled with a GUID (the configuration ID, which we'll talk about in an upcoming post), instead of server name. The pull server also requires that each config be accompanied by a checksum file with the file hash of the configuration file (example 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof and 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof.checksum).  One word off caution.. there can be no extra whitespace after the hash in the checksum file or the hash check will fail on the client node.  This means you cannot use

Get-FileHash 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof | out-file 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof.checksum

or

Get-FileHash 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof | set-content 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof.checksum

as those leave extra whitespace at the end of the file. I've been using

[System.IO.File]::AppendAllText('72ed4117-fc49-4f81-822c-5bc59db64dd3.mof.checksum', (Get-FileHash 72ed4117-fc49-4f81-822c-5bc59db64dd3.mof).Hash)

In my next post, I'll be talking about we can configure our clients to talk to a pull server, then we can see stuff really start to happen.

Building a Desired State Configuration Configuration

Now that's a title!  We've worked through my reasoning as to why I want Desired State Configuration (DSC) and how to build a pull server.  Today and in the next post we are going to look at how to create configurations which describe how our target systems are supposed to work.

The High Points

Building Configurations

Configurations are the driving force for DSC.  A configuration is a Managed Object Format (MOF) document that describes the how a specified server (or servers) should look.

What You See

A basic configuration may look like

/*
@TargetNode='8c7bfb10-8540-4a89-904c-5e6759de6d80'
@GeneratedBy=svc_build
@GenerationDate=10/07/2013 19:43:24
@GenerationHost=OR-WEB01
*/

instance of Pagefile as $Pagefile1ref
{
ResourceID = "[Pagefile]Default::[BaseServer]JustTheBasics::[VirtualServer]VMWare";
 InitialSize = 4294967296;
 SourceInfo = "C:\\windows\\system32\\WindowsPowerShell\\v1.0\\Modules\\SELocalConfiguration\\StackExchangeConfiguration\\StackExchangeConfiguration.psm1::14::5::Pagefile";
 ModuleName = "Pagefile";
 MaximumSize = 4294967296;
 ModuleVersion = "1.0";
};

instance of PowerPlan as $PowerPlan1ref
{
ResourceID = "[PowerPlan]Default::[BaseServer]JustTheBasics::[VirtualServer]VMWare";
 SourceInfo = "C:\\windows\\system32\\WindowsPowerShell\\v1.0\\Modules\\SELocalConfiguration\\StackExchangeConfiguration\\StackExchangeConfiguration.psm1::20::5::PowerPlan";
 Name = "High performance";
 ModuleName = "PowerPlan";
 ModuleVersion = "1.0";
};

instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]snmp::[BaseServer]JustTheBasics::[VirtualServer]VMWare";
 SourceInfo = "C:\\windows\\system32\\WindowsPowerShell\\v1.0\\Modules\\SELocalConfiguration\\StackExchangeConfiguration\\StackExchangeConfiguration.psm1::25::5::WindowsFeature";
 Name = "SNMP-Service";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of OMI_ConfigurationDocument
{
 Version="1.0.0";
 Author="build_service";
 GenerationDate="10/07/2013 19:43:24";
 GenerationHost="OR-WEB01";
};

Each instance of a MOF class (except for the OMI_ConfigurationDocument) refer to a DSC Resource and provides the parameters that resource will be called with when the configuration engine runs.  There are a couple of properties that are not passed to the resource module.  The ResourceID is a unique identifier that indicates the resource and the configuration inheritance tree where it is defined (we'll dig deeper into that shortly).  The ModuleVersion is the version number of the PowerShell module (from the psd1) of the DSC Resource.

Getting From Here To There

We don't want to write straight MOF files to define configuration, mainly because they are kind of verbose, with a some boilerplate  stuff for each resource.  Fortunately, we've got a Domain Specific Language (DSL) in PowerShell v4 to generate them.

The Configuration Keyword

PowerShell v4 contains the keyword "configuration", which allows us to provide a name for the configuration (like a function name).

configuration MyFirstServerConfig 
{
}

It looks just like how you would define a function or workflow. Now let's put something useful inside of it.

configuration MyFirstServerConfig 
{
    WindowsFeature snmp
    {
        Name = 'SNMP-Service'
    }
}

In this most simple of examples, we've defined a particular feature to be installed on a Windows Server. When we run this snippet, a wrapper function will be generated (kind of like how a workflow wrapper is generated). At this point, no MOF file has been created or applied, this simply creates a function that can generate a configuration based on the resources specified within. If we execute this configuration

PS> MyFirstServerConfig

we'll get a file named localhost.mof in a folder at $pwd/MyFirstServerConfig.

Configuration Default Parameters - OutputPath

If we want to specify the server the configuration applies to, we can wrap the resources in a Node block.

configuration MyFirstServerConfig 
{
    Node Server1
    {
      WindowsFeature snmp
      {
          Name = 'SNMP-Service'
      }
    }
}

This will create a configuration named Server1. Node names will be important as we move on to talking about targeting via Start-DscConfiguration and using the pull server.

We do have some options as to how the configuration gets generated. We can use the OutputPath to control where the configuration files are deposited.

PS> MyFirstServerConfig -OutputPath c:\Configurations
Configuration Default Parameters - ConfigurationData

Our other major parameter is ConfigurationData. ConfigurationData is a way to separate out your environmental concerns from the configuration documents. We'll come back to this one after we explore a few more concepts. ConfigurationData is a hashtable that expects a certain structure. The hashtable should contain an key named AllNodes, which is an array of hashtables that describe the nodes whose data you want to inject. For example

$ConfigurationData = @{
  AllNodes = @(
    @{NodeName = 'Server1';Role='Web'},
    @{NodeName = 'Server2';Role='FileShare'}
  )
}

NodeName is a common convention for specifying the node name.  We don't want to use Node, as there are some automatic variables populated in a configuration, one of which is $Node.  All the other keys in the hashtable representing a node are completely up to you.

Just a quick aside.. the node name does not necessarily equate to the server name.  When we get in to targeting (a bit in this post and more in an upcoming one), we'll see how this is true.

After we have some data in our ConfigurationData hashtable (and the variable doesn't need to be called ConfigurationData, I just did for convenience sake), we can use that to help drive our configuration. We'll tweak our configuration function a bit, so that it can take advantage of the extra data being supplied.

configuration MyFirstServerConfig 
{
    node $allnodes.NodeName
    {
        WindowsFeature snmp
        {
            Name = 'SNMP-Service'
        }       
        switch ($Node.Role)
        {
            'FileShare' {
                            WindowsFeature FileSharing
                            {
                                Name = 'FS-FileServer'
                            } 
                        }
            'Web'       {
                            WindowsFeature Web
                            {
                                Name = 'web-Server'
                            } 
                        }
        }
    }
}

Since this is a PowerShell DSL, I can use PowerShell functions, operators, and flow control to manipulate the configuration details. In this case, I'm using a switch statement to add roles to my server based on role definitions I'm supplying in my ConfigurationData.

PS> MyFirstServerConfig -ConfigurationData $ConfigurationData

    Directory: C:\scripts\MyFirstServerConfig

Mode                LastWriteTime     Length Name                                                                              
----                -------------     ------ ----                                                                              
-a---         10/8/2013   4:03 PM       1494 Server1.mof                                                                       
-a---         10/8/2013   4:03 PM       1516 Server2.mof

If we look at the MOF files generated by this, we'll see that Server1 does not have the FS-FileServer role, but does have the Web-Server role.

/*
@TargetNode='Server1'
@GeneratedBy=smurawski
@GenerationDate=10/08/2013 16:03:51
@GenerationHost=OR-UTIL02
*/

instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]snmp";
 SourceInfo = "::12::9::WindowsFeature";
 Name = "SNMP-Service";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of MSFT_RoleResource as $MSFT_RoleResource2ref
{
ResourceID = "[WindowsFeature]Web";
 SourceInfo = "::25::29::WindowsFeature";
 Name = "web-Server";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of OMI_ConfigurationDocument
{
 Version="1.0.0";
 Author="smurawski";
 GenerationDate="10/08/2013 16:03:51";
 GenerationHost="OR-UTIL02";
};

And Server2 has the reverse.

/*
@TargetNode='Server2'
@GeneratedBy=smurawski
@GenerationDate=10/08/2013 16:06:31
@GenerationHost=OR-UTIL02
*/

instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]snmp";
 SourceInfo = "::13::9::WindowsFeature";
 Name = "SNMP-Service";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of MSFT_RoleResource as $MSFT_RoleResource2ref
{
ResourceID = "[WindowsFeature]FileSharing";
 SourceInfo = "::20::29::WindowsFeature";
 Name = "FS-FileServer";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of OMI_ConfigurationDocument
{
 Version="1.0.0";
 Author="smurawski";
 GenerationDate="10/08/2013 16:06:31";
 GenerationHost="OR-UTIL02";
};

To highlight a neat trick since we are using a switch statement and switch can process collections, we can specify more than one role in our hashtable and our configuration should be able to add all the required resources.

$ConfigurationData = @{
  AllNodes = @(
    @{NodeName = 'Server1';Role='Web'},
    @{NodeName = 'Server2';Role='FileShare'}
    @{NodeName = 'Server3';Role=@('FileShare','Web')}
  )
}

configuration MyFirstServerConfig 
{
    node $allnodes.NodeName
    {
        WindowsFeature snmp
        {
            Name = 'SNMP-Service'
        }       
        switch ($Node.Role)
        {
            'FileShare' {
                            WindowsFeature FileSharing
                            {
                                Name = 'FS-FileServer'
                            } 
                        }
            'Web'       {
                            WindowsFeature Web
                            {
                                Name = 'web-Server'
                            } 
                        }
        }
    }
}

MyFirstServerConfig -ConfigurationData $ConfigurationData

If we look at the configuration generated for Server3, we'll find both Web-Server and FS-FileServer roles described.

/*
@TargetNode='Server3'
@GeneratedBy=smurawski
@GenerationDate=10/08/2013 16:06:31
@GenerationHost=OR-UTIL02
*/

instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]snmp";
 SourceInfo = "::13::9::WindowsFeature";
 Name = "SNMP-Service";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of MSFT_RoleResource as $MSFT_RoleResource2ref
{
ResourceID = "[WindowsFeature]FileSharing";
 SourceInfo = "::20::29::WindowsFeature";
 Name = "FS-FileServer";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of MSFT_RoleResource as $MSFT_RoleResource3ref
{
ResourceID = "[WindowsFeature]Web";
 SourceInfo = "::26::29::WindowsFeature";
 Name = "web-Server";
 ModuleName = "MSFT_RoleResource";
 ModuleVersion = "1.0";
};

instance of OMI_ConfigurationDocument
{
 Version="1.0.0";
 Author="smurawski";
 GenerationDate="10/08/2013 16:06:31";
 GenerationHost="OR-UTIL02";
};

Next Up

In the next post, we'll continue this topic and look at other ways we can parameterize configurations as well as nesting configurations.  We'll also touch on how to apply these configurations from Start-DscConfiguration and via a Pull Server.  Stay tuned!

Building a Desired State Configuration Pull Server

Quick recap, I'm working through a series of posts about the Desired State Configuration infrastructure that I'm building at Stack Exchange, including some how-to's.

The High Points

I started with an overview of what and why.  Today, I'm going to start the how.

Building a Pull Server

I'm going to describe how to do this with Server 2012 R2 RTM (NOTE: this is not the General Availability  release, so there may be changes at GA), since that's the environment I'm working most in.  If there is enough demand, I may follow up with how to do this using the Windows Management Framework on downlevel operating systems after the GA version of WMF 4 is released.

The first step is adding the required roles and features, including the DSC Service.

Add-WindowsFeature Dsc-Service

Fortunately, the Dsc-Service feature has the right dependencies configured so IIS, the correct modules, and the Management OData Extension are all enabled.

Next we need to set up the IIS web site:

  • Create an directory to serve the web application from (I'll use c:\inetpub\wwwroot\PSDSCPullServer)
  • Copy several files from $pshome/modules/psdesiredstateconfiguration/pullserver (Global.asax, PSDSCPullServer.mof, PSDSCPullServer.svc, PSDSCPullServer.xml) to this directory.
  • Copy PSDSCPullServer.config and rename it to web.config
  • Create a subdirectory named "bin".
  • Copy one file from $pshome/modules/psdesiredstateconfiguration/pullserver (Microsoft.Powershell.DesiredStateConfiguration.Service.dll) to the "bin" directory.
  • In IIS, create an application pool that runs under the "Local System" account.
  • In, IIS, create a new site (or application in an existing site or just use the existing default site)
  • Point the site or application root to the directory you designated as the root of the site.
  • Unlock the sections of the web config as below
$appcmd = "$env:windir\system32\inetsrv\appcmd.exe" 
& $appCmd unlock config -section:access
& $appCmd unlock config -section:anonymousAuthentication
& $appCmd unlock config -section:basicAuthentication
& $appCmd unlock config -section:windowsAuthentication

 

Now we need to set up the location where the pull server content will be served from.  Installing the DSC Service feature creates a default location ( $env:programfiles\WindowsPowerShell\DscService ).  There'll you find sub-directories for configuration and modules.  We can use these folders or we can create another location.  I'm going to stick with the defaults for now.  We've got a few steps left.

First, we need to copy the Devices.mdb from $pshome/modules/psdesiredstateconfiguration/pullserver to the root of our pull server data location (in this case, $env:programfiles\WindowsPowerShell\DscService )

Update the web.config app settings with the following settings:

<add key="dbprovider" value="System.Data.OleDb" />
<add key="dbconnectionstr" value="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Program Files\WindowsPowerShell\DscService\Devices.mdb;" />
<add key="ConfigurationPath" value="C:\Program Files\WindowsPowerShell\DscService\Configuration" />
<add key="ModulePath" value="C:\Program Files\WindowsPowerShell\DscService\Modules" />

After that your pull server should be up and running.  You should see something like this if you navigate to http://yourpullserver/psdscpullserver.svc

PullServerDefaultUrl

 

 

Skip to toolbar