You've successfully subscribed to Florin Loghiade
Great! Next, complete checkout for full access to Florin Loghiade
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.

PowerShell DSC - Writing Configurations

in

In my previous articles (DSC on Linux & Building a DSC Pull Server), I discussed about installing and configuring DSC on Linux machines, and after that I talked about creating your very first DSC Pull Server, which grants you the capability of serving configuration documents and resources to both Windows and Linux machines. In this blog post I will be talking about writing configuration files and about the methods that you could leverage the code you wrote in order to write once and use it on multiple machines.

Before we start talking about how configurations look and what they are, let’s start first on a key principle for when you’re writing and pushing configurations to the machines. When you start writing configurations, you must have a plan in mind on how you’re going to re-use those configurations scripts, without wasting your time doing copy/paste actions all the time. My best advice that I can give, is that you should NEVER hard code data inside scripts or configuration files. This is a best practice and it will help you later on. I know it involves more planning and a lot more work but the benefits are worth it.

When you’re first trying out PowerShell DSC, you start small, one configuration at a time with hard-coded data inside the scripts for simplicity purposes, but when you start scaling big, hard coding data inside scripts will eat a lot of your time when it’s time to do a change. Trust me on this one because I had to learn it the hard way and it’s painful to start re-writing a bunch of scripts.

For consistency purposes, in order to follow this article, you must have the latest version of Windows Management Framework. If you’re on Windows 10 version 1511 (November release) then you don’t have to do anything as you already have the latest WMF bits installed, but if you’re on an older version of Windows, you will need to install Windows Management Framework version 5 and you can get it from here: WMF 5 Download Link
So let’s start first with how a simple configuration looks like.

configuration SampleConfiguration
{
    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    Node 'DSCClient'
    {
        Service StopBITSService
        {
            Name = 'BITS'
            StartupType = 'Manual'
            State = 'Stopped'
            Ensure = 'Present'
        }
    }
}
SampleConfiguration

In the code block above we have a simple configuration that basically manipulates a service, in our case the service is BITS. So let’s see what do we have to understand about DSC configurations and what are the naming conventions involved.

We’ll go about this line by line.

  • Line 1 – Every DSC configuration has to start with the keyword Configuration which basically tells the ISE that this is a DSC configuration and not a regular script. Every configuration has to have a name which can be anything you like. Name the configurations accordingly as to what they do because it will help you in the long run.
  • Line 2 – We use curly braces to open and close statement blocks which follows the same principle of any modern programming language where you define statement blocks.
  • Line 3 – This is where you import the modules you’re going to use in the configuration script. In this example I imported the PSDesiredStateConfiguration DSC built-in module. If I wanted to import some other resources like the ones released by the PowerShell team, then this is the place where I would reference them.
  • Line 4 – The Node keyword is where we specify the name of the host that will be receiving the DSC configuration. In other examples we will turn the hard-coded value in a parameter that will allow us to change that name in whatever we like, without modifying the configuration script.
  • Line 5 – Another set of curly braces meant to define the statement block that will contain the configuration data that’s specific to the referenced host “DSCClient”. We can have multiple NODE blocks inside a single configuration file in case we want to deploy / maintain deployments as a project. For example, we have a project that requires two NLB servers in high availability, a web farm consisting of four IIS server and a AlwaysON 2-Node SQL Cluster and the project is named “[tooltip text=”Reference from the movie, The Martian”]Elrond[/tooltip]”
  • Line 6 – This is the part where start referencing the DSC resources we would like to use in our configuration scripts. In this example I’m using the built-in DSC resource named Service that gives you a set of options for your existing services, or if you want to create a new service. The name StopBITSService that’s after the referenced DSC resource is just a basic string, just be careful when you’re setting names because you cannot have the same name twice. You can find out what parameters are available for a DSC resource by using Get-DscResource -Name -Syntax
  • Service [String] #ResourceName
    {
        Name = [string]
        [BuiltInAccount = [string]{ LocalService | LocalSystem | NetworkService }]
        [Credential = [PSCredential]]
        [Dependencies = [string[]]]
        [DependsOn = [string[]]]
        [Description = [string]]
        [DisplayName = [string]]
        [Ensure = [string]{ Absent | Present }]
        [Path = [string]]
        [PsDscRunAsCredential = [PSCredential]]
        [StartupType = [string]{ Automatic | Disabled | Manual }]
        [State = [string]{ Running | Stopped }]
    }
  • Lines 7-12 – This is the part where I’m declaring what needs to be created, or changed and remain in a consistent state after it has been applied. In the aforementioned example, I declared that I want the BITS service to have the Startup Type set as Manual, I want it to be always stopped. The Ensure parameter has a default setting of Present even if it’s not referenced in the configuration script. The Ensure parameter has two properties. Present which means that I want that service to exist and if it doesn’t exist, then create it, and Absent is the opposite, which means that if the service exists, remove it. The Ensure property is available on all DSC Resources and this is how you control what is and what shouldn’t be on a server.
  • Lines 13-14 – We close the open curly braces for the Node and Configurations blocks.
  • Line 15 – This last line is not mandatory. This is the part where we call the configuration in order to produce the .mof file that will be pushed to the target machine. Basically this works the same as calling a PowerShell Function.

When you ‘execute’ a configuration script that you just wrote in the PowerShell ISE, the PowerShell engine will create a folder named after the configuration and inside the folder it will create a .mof file for each node that was referenced in the script. So if I would have referenced a hundred nodes inside the script, I would have had a hundred .mof files named after each node.

Now that we understand the basics of writing DSC configurations, it’s time to go a little deeper inside the subject and transforming those hard-coded values into variables that require user input. This is the case when you start considering doing more than demos and presentations on how DSC works. When you’re looking at working with test/dev and production environments where you have more than one or two server roles, you need to have configuration scripts that are modular and require user input in order to produce files that can be then consumed by the Local Configuration Manager of the target nodes.

The easiest way and let’s say not so good way, would be to establish parameters inside the configuration script, but after you have more than a dozen configuration scripts, you will find out that remembering those parameters can be a huge pain. Thus the not so good way part.

Here’s an example of the above script without hard-coded values:

#requires -Version 5
configuration SampleConfiguration
{
    param(
        [string[]]$NodeName,
        [string[]]$Services
    )

    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    node $NodeName
    {
        foreach($Service in $Services)
        {
            Service $Service
            {
                StartupType = 'Manual'
                Name = $Service
                State = 'Stopped'
                Ensure = 'Present'
            
            }
        }
    }
}
SampleConfiguration -NodeName ('DSCClient01','DSCClient02') -Services ('BITS', 'Spooler')

See what I did there? In a nutshell I transformed the script to be useless without the proper input. I even added foreach loop, in case I wanted to configure multiple services at once, thus hitting two servers with one stone. If I call the configuration script without any of the referenced parameters, the PowerShell engine will not produce any .mof documents. Which is exactly what we’re looking for.

So I took a configuration script with hard-coded values for the target machines and targeted services, removed them, and added parameters instead. Now I can go further with this example, but there’s an even more elegant way of doing things that will allow you to build scalable DSC configurations.

With PowerShell DSC, you have the possibility of separating the configuration data from the general logic of your script. This is where you take the example from above, and make it so that you can reuse the configuration script in order to do the same type of configuration but on different hosts.

DSC can take a ConfigurationData parameter, which is a hash table that contains all the information necessary for the script to run. The ConfigurationData parameter can take either a hash table variable or you can reference a path to a .psd1 file that contains the hash table. Either the file or the variable has to contain three key values which are part of the DSC naming convention in order for this to work.

Here are the key values:

AllNodes – AllNodes is basically an array that contains key value pair entries with all the node information that you’re going to push to the PowerShell Engine. This is where you declare configuration settings for each target client that you’re looking to configure.
NodeName – This key requires either the target client hostname or IP address.
NonNodeData – This hash table can be empty or it can contain multiple keys that can be referenced in the configuration script. These can be used for different purposes, like reading the contents of a file, etc.

A ConfigurationData variable or file has to have the following structure that’s referenced below otherwise the PowerShell engine will throw an error. This is a standard and it’s best to keep it that way.

$ConfigData = 
@{
    AllNodes = @();
    NonNodeData = ""   
}

The AllNodes takes hash table input in the form of @{} where you reference the mandatory NodeName and any additional properties that you will leverage in the final configuration script. The idea here is to use the Where{} filter inside the configuration script, which gives you the possibility to classify the servers you’re looking to configure and with that added benefit, you win a lot of flexibility. You can even specify a property that will apply to all the nodes without doing any filtering.

Example:

$ConfigData = 
@{
    AllNodes = @(
        @{
            NodeName           = '*'
            FilePath            = 'C:\Scripts'
        }
);
    NonNodeData = ''   
}

So basically the example above can be used to do some base configurations on all the nodes referenced in the data file.

Is it starting to makes sense? No? Here’s a clear example, on how we can leverage the utility of the ConfigurationData parameter:

#requires -Version 5
$ConfigData = 
@{
    AllNodes = @(

        @{
            NodeName           = 'DSCClient01'
            Role = 'NLBServer'
            Services = ('BITS','Spooler')
        },
        @{
            NodeName           = 'DSCClient02'
            Role = 'SQLServer'
            Services = 'BITS'
        }
);
    NonNodeData = '' #This is a hash-table of properties that may or may not be related to any of the defined nodes in the AllNodes table.   
}

configuration SampleConfiguration
{


    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    node $AllNodes.Where{$_.Role -eq 'SQLServer'}.NodeName
    {
        foreach($Service in $Node.Services)
        {
            Service $Service
            {
                StartupType = 'Manual'
                Name = $Service 
                State = 'Stopped'
                Ensure = 'Present'
            
            }
        }
    }

     node $AllNodes.Where{$_.Role -eq 'NLBServer'}.NodeName
    {
        foreach($Service in $Node.Services)
        {
            Service $Service
            {
                StartupType = 'Manual'
                Name = $Service
                State = 'Stopped'
                Ensure = 'Present'
            
            }
        }
    }

}
SampleConfiguration -ConfigurationData $ConfigData

Now it makes sense? See how much flexibility you gain by using the ConfigurationData parameter?

You can scale as much as you want, without having to remember a bunch of stuff that you wrote 2-3 months ago. I even mentioned a moment ago about using .psd1 files, in order to stop having configuration variables inside the configuration scripts. The way leverage this capability is quite simple, you take the configuration data hash table and paste it in a file that has the extension .psd1 (PowerShell Data File) and after that, you reference it when you’re calling the configuration script.

Example:

Using configuration variables:

SampleConfiguration -ConfigurationData $ConfigData

Using configuration data files:

SampleConfiguration -ConfigurationData .\ConfigData\Services.psd1 

The output of using any of the mentioned methods will produce the same configuration documents. Using the method of referencing data files, you can have a folder structure that contains configuration data files and configuration scripts.

DSC_ConfigVariable_ConfigFile

So all you have to do after you have all the building blocks in place, is to use either Push or Pull and tell the servers to make it so ?

That’s all folks!

I hope this article answered all your questions that you had about writing DSC configurations, and if I missed a key point or there’s something that isn’t properly explained, just comment down below ?

Have a nice one!