Creating reusable DSC configurations

So far, we have been working with singular DSC configuration script files and DSC Configuration blocks. We have taken an approach of one DSC Configuration block per script file, and one script file per environment. We covered why this was best practice in the earlier sections, and at this point you've probably tried out a couple on your own using what we have done so far. You are likely realizing that these script files can grow very large, containing hundreds of lines of code with large sections of just DSC Resource declaration statements.

This is a common problem with CM. The world is complex and messy, and declaring it in text files sounds easy at first but becomes difficult to maintain if all you have is one big ball of lines without some organization. If you have developed scripts or code in any language, you are familiar with a common solution to the "big ball of mud" problem: encapsulation. Encapsulation involves extracting the common or related parts to reusable components and then referencing those components in all the different places they are needed. This reduces the complexity of maintaining the code while increasing the flexibility of its use across your code base. We can apply this technique to DSC in a few different ways, with varying degrees of success.

Other CM products, like Puppet and Chef, provide several features that address this need with things like file inheritance or function includes. They allow you to bundle a set of steps to bring a target node into compliance in to a reusable package that can be referenced in many other configuration scripts. The actual implementation and feature set vary with which vendor you choose.

Since DSC is still a young product, it is missing some of the features of other products that have been out there longer. This isn't a criticism; just a statement of what features are provided out of the box with DSC. There are some ways we can get the encapsulation we desire using the features DSC has now, using either Nested DSC configurations or DSC composite resources. We'll cover these now.

Nested DSC configurations

The easiest way to start organizing your DSC configuration script is to use nested DSC configurations. Nested DSC configurations use the encapsulation technique by organizing related code inside one DSC Configuration block and, then referencing that DSC Configuration block in another DSC Configuration block, which is a set of DSC Resource statements and code grouped together into a single DSC Configuration block. You can then reference this DSC Configuration block in another DSC Configuration block.

Nested DSC configuration syntax

Think of using nested DSC configurations as writing PowerShell functions. Each PowerShell function encompasses a certain purpose, just like a DSC Configuration block encompasses a certain set of configuration instructions. If we look at the example in code_example_05.ps1, we can see that setting the environment variable and creating the config file are related and tied together. If we encapsulate them together, we can just reference a single block of code instead of a list of DSC Resources. Let's use code_example_5.ps1 and apply the nested DSC configuration method to it:

[CmdletBinding()]
param(
  [Parameter()]$OutputPath = [IO.Path]::Combine($PSScriptRoot, 'InstallExampleSoftware'),
  [Parameter()]$ConfigData
)

Configuration ImportantSoftwareConfig
{
  param($ConfigFile, $ConfigFileContents)

  Environment AppEnvVariable
  {
    Ensure = 'Present'
    Name   = 'ConfigFileLocation'
    Value  = $ConfigFile
  }

  File ConfigFile
  {
    DestinationPath = $ConfigFile
    Contents        = $ConfigFileContents
  }
}

Configuration InstallExampleSoftware
{
  Node $AllNodes.NodeName
  {

    WindowsFeature DotNet
    {
      Ensure = 'Present'
      Name   = 'NET-Framework-45-Core'
    }
  }

  Node $AllNodes.Where({$_.Roles -contains 'FooBar'}).NodeName
  {
    ImportantSoftwareConfig SetImportantValues
    {
      ConfigFile         = $Node.ExampleSoftware.ConfigFile
      ConfigFileContents = $ConfigurationData.NonNodeData.ConfigFileContents
    }

    Package  InstallExampleSoftware
    {
      Ensure    = 'Present'
      Name      = $Node.ExampleSoftware.Name
      ProductId = $Node.ExampleSoftware.ProductId
      Path      = $Node.ExampleSoftware.SourcePath 
      DependsOn = @('[WindowsFeature]DotNet')
    }
  }
}

InstallExampleSoftware  -OutputPath $OutputPath -ConfigurationData $ConfigData

We immediately see the benefit of encapsulating a set of configuration instructions inside separate DSC configurations when we examine InstallExampleSoftware. All the work needed to get the "important" environment variable set is encapsulated inside the ImportantSoftwareConfig nested DSC configuration, and the main InstallExampleSoftware is able to deal with the rest of the configuration. We now clearly see that setting the environment variable and creating the config file are related and dependent on each other, and we can reference this block wherever we need to. It is now a reusable piece of code.

Nested DSC configuration limitations

There are some limitations to nested DSC configurations. As you can see in the previous example, we have created a reusable piece of code, but it's still in the same file as the original DSC Configuration block. We haven't reduced the lines of code needed in the file, just moved them around. We have increased the modularity of the code (the code can be used in several places by just referencing the method signature), but we still have a file with lots of lines of code. This may become hard to read as the file gets larger. Frequently, you will have to understand the entire file to know what will be executed, which gets harder and harder as the line count increases.

Your first instinct is most likely to cut the ImportantSoftwareConfig DSC Configuration block and put it in a separate file. Once you do that, you will notice more limitations. There aren't any import statements for nested DSC configurations, so you will have to dot-source the separate file in your main configuration file like the following snippet:

./importantsofwareconfig.ps1

Configuration InstallExampleSoftware
{
  Node $AllNodes.Nodename
<#...#>
}

This introduces more work for you. You have to locate the dependent configuration file and ensure it exists before you continue. If you make a lot of them, it will become harder for you to keep track of all the files.

Even if you get the dot-sourcing approach to work, there are some functional problems with it. There have been isolated bug reports that some parameters aren't parsed when a nested DSC configuration exists outside the main configuration file. These reports seem to have been cleared up by the latest WMF 4 release that occurred on May 28, 2015, so make sure you are on at least that release if you intend to use them.

Using DependsOn does not work for nested configurations at all in WMF 4, as reported here in the PowerShell Connect bug tracker: https://connect.microsoft.com/PowerShell/feedback/details/812943/dsc-cannot-set-dependson-to-a-nested-configuration. This has been corrected in WMF 5.0, and to some extent, although there are still some reported bugs, in the patched version of WMF 4.0 on WS2012R2.

Thankfully, these limitations are addressed by the feature we introduce in the next section, called DSC composite resources.

DSC composite resources

Why are we mentioning resources in the DSC configuration files chapter when we are covering DSC Resources in the next chapter? This is a good question that is awkward to answer because of the confusing name choices that were made to describe this feature. Once we get the definitions out of the way, things will become much clearer.

A DSC composite resource is a DSC Configuration block that is bundled up in a DSC Resource PowerShell module folder structure that can be reused in other DSC configuration script files. While the authoring and syntax is slightly different, the usage of a DSC composite resource is exactly like that of a DSC Resource. I assume the naming stemmed from this similarity in implementation.

Why use DSC composite resources?

When you convert a set of DSC Configuration blocks into a reusable DSC composite resource, you abstract the complexity of configuration that targets nodes or pieces of software and improve manageability of your configuration scripts.

If this sounds confusing, it's because it is confusing to explain without an example to look at. If you look at the DSC configuration script in The Script file example of this chapter, you will see several DSC Resources being used to declare the desired state of a target node. We split them into two node blocks because only some of the target nodes should have ExampleSoftware installed on them and all of them should have .NET installed, a specific environment variable set, and a specific file created. Let's imagine that we need to install another piece of software called TheRealSoftwareStuff that has its own list of dependencies that are different than those of ExampleSoftware. We would have a long list of statements with a complicated DependsOn dependency list.

Note

We've mentioned DependsOn several times without specifically calling it out. DependsOn is a parameter all DSC Resources implement. This allows DSC to execute the DSC Resources in an expected order. If DependsOn is not specified, then DSC does not guarantee the order in which it will execute the DSC Resources defined.

Adding to the middle of this list would require reordering the entire dependency tree and copy and pasting entries until it looked right, which we could get wrong. Wouldn't it be simpler and less error-prone if we were able to group the dependencies and steps into separate blocks? If we could do that, our dependency tree would suddenly become a lot smaller, and only the parts that matter to the software we are installing are present.

We are going to take the example in The script file example and add TheRealSoftwareStuff to it, then split it up into DSC composite resources. If you're thinking that I didn't explain anything about that remark earlier about how DSC composite resources have to be packaged specially and it's hard to get that right, don't worry; I will get to that. It's easier to understand if we look first at why you would want to do this. I won't copy the original here, as you can look at The script file example for that. Here is the modified script before we start to encapsulate the code. This example can be used with the following:

# beginning of script
[CmdletBinding()]
param(
  [Parameter()]$OutputPath = [IO.Path]::Combine($PSScriptRoot, 'InstallExampleSoftware'),
  [Parameter()]$ConfigData
)

Configuration InstallExampleSoftware
{
  Node $AllNodes.NodeName
  {
    WindowsFeature DotNet
    {
      Ensure = 'Present'
      Name   = 'NET-Framework-45-Core'
    }
  }

  Node $AllNodes.Where({$_.Roles -contains 'FooBar'}).NodeName
  {
    Environment AppEnvVariable
    {
      Ensure = 'Present'
      Name   = 'ConfigFileLocation'
      Value  = $Node.ExampleSoftware.ConfigFile
    }

    File ConfigFile
    {
      DestinationPath = $Node.ExampleSoftware.ConfigFile
      Contents = $ConfigurationData.NonNodeData.ConfigFileContents
    }
    
    Package  InstallExampleSoftware
    {
      Ensure    = 'Present'
      Name      = $Node.ExampleSoftware.Name
      ProductId = $Node.ExampleSoftware.ProductId
      Path      = $Node.ExampleSoftware.SourcePath 
      DependsOn = @('[WindowsFeature]DotNet')
    }
  }

  Node $AllNodes.Where({$_.Roles -contains 'RealStuff'}).NodeName
  {
    WindowsFeature IIS
    {
      Ensure = 'Present'
      Name   = 'Web-Server'
    }

    WindowsFeature IISConsole
    {
      Ensure    = 'Present'
      Name      = 'Web-Mgmt-Console'
      DependsOn = '[WindowsFeature]IIS'
    }

    WindowsFeature IISScriptingTools
    {
      Ensure    = 'Present'
      Name      = 'Web-Scripting-Tools'
      DependsOn = @('[WindowsFeature]IIS','[WindowsFeature]IISConsole')
    }

    WindowsFeature AspNet
    {
      Ensure    = 'Present'
      Name      = 'Web-Asp-Net'
      DependsOn = @('[WindowsFeature]IIS')
    }

    Package  InstallRealStuffSoftware
    {
      Ensure    = 'Present'
      Name      = $Node.RealStuffSoftware.Name
      ProductId = $Node.RealStuffSoftware.ProductId
      Path      = $Node.RealStuffSoftware.Source
      DependsOn = @('[WindowsFeature]IIS', '[WindowsFeature]AspNet')
    }
  }

}

InstallExampleSoftware  -OutputPath $OutputPath -ConfigurationData $ConfigData
# script end

It's obvious that this will get quite long as we keep adding software and other features. If we need to insert a new IIS feature, it has to be inserted in to the dependency chain of the IIS feature set as well as before the MSI package install. We also need to remember to keep the chain of steps between the ExampleSoftware install and RealStuffSoftware separated, and not mix any steps between the two. In a long script, this may not be apparent when you come back to it after months of successful use.

We can reduce this complexity by grouping these declarations into DSC composite resources. In the new approach that follows, we replace the long list of declarations with one DSC composite resource declaration. Notice that we put the configuration data we used above as parameters to the parameter declarations. We will read more on this when we get to the syntax in the next section.

# beginning of script
[CmdletBinding()]
param(
  [Parameter()]$OutputPath = [IO.Path]::Combine($PSScriptRoot, 'InstallExampleSoftware'),
  [Parameter()]$ConfigData
)

Configuration InstallExampleSoftware
{
  Import-DscResource -Module ExampleSoftwareDscResource  
  Import-DscResource -Module RealStuffDscResource
  
  Node $AllNodes.NodeName
  {
    WindowsFeature DotNet
    {
      Ensure = 'Present'
      Name   = 'NET-Framework-45-Core'
    }
  }

  Node $AllNodes.Where({$_.Roles -contains 'FooBar'}).NodeName
  {
    ExampleSoftwareDscResource InstallExampleSoftware
    {
      Name               = $Node.ExampleSoftware.Name
      ProductId          = $Node.ExampleSoftware.ProductId
      Path               = $Node.ExampleSoftware.Source
      ConfigFile         = $Node.ExampleSoftware.ConfigFile
      ConfigFileContents = $ConfigurationData.NonNodeData.ConfigFileContents
    }
  }

  Node $AllNodes.Where({$_.Roles -contains 'RealStuff'}).NodeName
  {
    RealStuffDscResource InstallRealStuffSoftware
    {
      Name      = $Node.RealStuffSoftware.Name
      ProductId = $Node.RealStuffSoftware.ProductId
      Path      = $Node.RealStuffSoftware.Source
    }
  }

}

InstallExampleSoftware  -OutputPath $OutputPath -ConfigurationData $ConfigData
# script end

The DSC composite resource syntax

The syntax for a DSC composite resource is exactly the same as a DSC Configuration block. This makes sense, because they are, in effect, the same thing. This is great because it allows you to copy and paste your existing working DSC Configuration blocks and encapsulate them inside reusable components without much modification.

The exception to the statement that the syntax is the same is that you do not include the Node block inside a DSC composite resource declaration. Since the DSC composite resource is being used inside the "main" DSC Configuration block, it inherits the target node it's being applied to.

Remember when we covered the syntax of a DSC Configuration block and said that you could use parameters with a DSC Configuration block just like a normal PowerShell function, and that you normally wouldn't because the other DSC features, like the DSC special variables, are so much more powerful that you don't really need them? Well, with DSC composite resources, the opposite is true. The parameter statement is the entry point for your DSC composite resource, and the only way to get configuration data or other information inside it. Any data or variables you expect to use inside the DSC composite resource have to come through the parameter statements.

DSC composite resource folder structure

A DSC composite resource relies on a set of folders and files that have to be in their exact correct places in order to work successfully.

Note

Before continuing, ensure that you are familiar with creating PowerShell v2 modules.

A DSC composite resource can be described as a PowerShell module inside a "root" PowerShell module. The root PowerShell module is required to declare this as a DSC composite resource to the DSC engine; it itself does not usually contain any code, nor does it actually contain any DSC Configuration blocks. Inside the root module is a sub folder called DSCResources. The folder must be named exactly this for the DSC composite resource to be parsed correctly. Inside the DSCResources folder are one or more PowerShell module folders. Each module represents an actual DSC composite resource.

For the most part, all the normal PowerShell module files are present, with the exception of the file that actually contains the DSC Configuration blocks. This file must have a schema.psm1 extension instead of just a .psm1 extension.

In the following code block is an example folder structure for a DSC composite resource. In this example, we have two DSC composite resources packaged together:

$env:PSModulePath
  |- <RootModuleName>
    |- <RootModuleName>.psd1 (PowerShell Module Manifest file, Required)
    |- <RootModuleName>.psm1 (PowerShell Module file, Optional)
    |- DSCResources
      |- <DSCCompsiteResource1>
        |- <DSCCompsiteResourceName1>.psd1 (DSC Composite Resource manifest file, Required)
        |- <DSCCompsiteResourceName1>.schema.psm1 (DSC Composite Resource PowerShell module, Required)
      |- <DSCCompsiteResource2> 
        |- <DSCCompsiteResourceName2>.psd1 (DSC Composite Resource manifest file, Required)
        |- <DSCCompsiteResourceName2>.schema.psm1 (DSC Composite Resource PowerShell module, Required)

If this still isn't clear to you, then you are not alone. Let's try this again and apply the above example to the ExampleSoftware and TheRealSoftwareStuff software we are trying to install:

$env:PSModulePath
  |- <KickingCompanyResources>
    |- KickingCompanyResources.psd1
    |- KickingCompanyResources.psm1
    |- DSCResources
      |- ExampleSoftwareDscResource
        |- ExampleSoftwareDscResource.psd1
        |- ExampleSoftwareDscResource.schema.psm1
      |- RealStuffDscResource
        |- RealStuffDscResource.psd1
        |- RealStuffDscResource.schema.psm1

So, we start out at the root module, KickingCompanyResources. We named our root module after our company, but there really are no official rules here. Microsoft initially recommended that the community prefix their DSC Resources and DSC composite resources with a "c" and Microsoft would prefix their experimental ones with an "x." Since this wasn't a requirement in DSC, the actual implementation of these suggested naming schemes has had spotty adoption. When Microsoft moved their DSC Resource Kit to GitHub (this is covered later in the book), it highlighted how all DSC Resources with an "x" are prefixed but are released to the PSGallery with the "x" still there. There is also a ticket explaining that Microsoft will eventually move away from suggesting prefixes (https://github.com/PowerShell/DscResources/issues/27). It is clear that the naming conventions are still being worked out.

Generally speaking, it's prudent to name your root module something sensible that also makes sense in the context of the operations you are performing. Here, we are packaging two resources that operate on software written by the KickingCompany, so it makes sense to be so bold as to name them with the company name.

Inside the root module folder, we see the PowerShell manifest file and the PowerShell module file. The module file can be left blank; it's not required and not used for a DSC composite resource. The manifest file must contain the normal module manifest data to help identify it. This is easily created using the New-ModuleManifest Cmdlet, or you can do it by hand:

[PS]> New-ModuleManifest –Path .ExampleSoftwareDscResource.psd1
[PS]> Get-Content .ExampleSoftwareDscResource.psd1
#
# Module manifest for module 'ExampleSoftware'
#
# Generated by: James Pogran
#
# Generated on: 1/1/2015
#
@{
# Script module or binary module file associated with this manifest.
# RootModule = ''

# Version number of this module.
ModuleVersion = '1.0'

# ID used to uniquely identify this module
GUID = 'a80febb6-39d1-4129-88ae-7572561e43c8'

# Author of this module
Author = 'James Pogran'
<#
...
#>
}

Next, we have the DSCResources folder, which contains the two folders ExampleSoftwareDscResource and RealStuffDscResource. Here is where we start deviating from the normal PowerShell module structure. Each has a normal PowerShell manifest file, in which the most important field is the RootModule entry. However, it has to point to the schema.psm1 file, not just to a file with a .psm1 extension. The schema.psm1 file contains the DSC Configuration block and nothing else.

Still not super clear? You're still not alone. It was confusing enough to remember each time I used DSC composite resources that I created a short PowerShell function to encapsulate the above steps into a simple command-line execution. The function itself is a little too long to include in this chapter, but it can be found in PowerShell.org's GitHub repo here: https://github.com/PowerShellOrg/DSC/blob/master/Tooling/DscDevelopment/New-DscCompositeResource.ps1.

DSC composite resource drawbacks

We have spent significant time explaining the benefits of code reuse, avoiding duplication, and improving maintainability by writing clean code throughout this book. If DSC composite resources enable these things, then how could there be a section with "drawbacks" in its title?

In the original WMF 4 release, you couldn't use the DependsOn property to express a dependency between two DSC composite resources. You could use DependsOn inside the DSC composite resource like normal, but you could not link two DSC composite resources together. This has been corrected in the WMF 5 release. The fix was backported to the latest WMF4 release.

DSC composite resources were sometimes hard to develop. Iterative development (some call this trial and error) was painful, as DSC would cache any DSC Resource, and it seemed like it never let go of the DSC composite resources. Make a change and copy the new DSC composite resource over and you could spend a lot of time hitting your head against the wall while your configuration runs fail, only to then remember the cache and have it work wonderfully when you reset it (take a look at the next section for more about how to remedy this).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset