Custom Module for PowerShell DSC

Refreshing my memory on using PowerShell DSC to create a consistent built configuration for a set of virtual machines, I needed a way to customise app settings in a series of web and application configurations. A lot of the modules available as extensions or custom modules seemed to operate by going to IIS and modifying the underlying file by changing settings in IIS. This wouldn't work for me so I set out researching how to write my own module. It turned out not to be as difficult as I had thought. There are two ways to build a DSC module and I elected to go for the class-based option as it seemed simpler to implement.

First we need two files, a .psd1 and a .psm1 file. The .psd1 is the meta data for the new module and contains the exports, the guid etc. Note I am following the convention here of prefixing with a lower case C to signify that this is a custom module not one of the built in or experimental modules from Microsoft or the Community.

Meta

cApplicationConfiguration.psd1


@{

# Script module or binary module file associated with this manifest.
RootModule = 'cApplicationConfiguration.psm1'

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

# ID used to identify this module uniquely
GUID = '59E3596B-8447-4FC8-84AE-0F35F93881F1'

# Description of the functionality provided by this module
Description = 'DSC resource provider to modify application configuration settings'

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.0'

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Required for DSC to detect PS class-based resources.
DscResourcesToExport =  @('cApplicationConfiguration') 

}  

Class

The .psm1 contains the actual code that does the work, with the class named for the module name and with properties that reflect settings in the DSC script. In this case, the path to the config file, the name of the key in appsettings and the name of the new value.

cApplicationConfiguration.psm1


enum Ensure {
    Present
    Absent
}

[DscResource()]
class cApplicationConfiguration {

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Mandatory)]
    [string]$Path

    [DscProperty(Mandatory)]
    [string]$Key

    [DscProperty()]
    [string]$Value

    [DSCProperty()]
    [Ensure] $Ensure = [Ensure]::Present

    [cApplicationConfiguration] Get() {
        
        $state = [hashtable]::new()
        $state.Name = $this.Name
        $state.Path = $this.Path
        $state.Key = $this.Key

        If (Test-Path -Path $this.Path) {

            [xml]$AppConfigXml = Get-Content -Path $this.Path

            If ($this.Key) {
                $XPath = "//appSettings/add[@key = '$($this.Key)']"
                $Node = $AppConfigXml.SelectSingleNode($XPath)

                If ($Node) {
                    $state.Value = $Node.value
                    $state.Ensure = [Ensure]::Present
                } Else {
                    $state.Ensure = [Ensure]::Absent
                }
            }
        } 

        return [cApplicationConfiguration]$state
    }

    [bool] Test() {

        If (Test-Path -Path $this.Path) {
            [xml]$AppConfigXml = Get-Content -Path $this.Path

            $XPath = "//appSettings/add[@key = '$($this.Key)']"
            $Node = $AppConfigXml.SelectSingleNode($XPath)

            [bool]$Found = $null -ne $Node -and $Node.value -eq $this.Value

            If ($Found) {
                Write-Verbose "'$($this.Key)' exists with value '$($Node.value)' in '$($this.Path)'"
            } Else {
                Write-Verbose "'$($this.Key)' does not have the correct value in '$($this.Path)'"
            }

            return $Found
        }

        return $False
    }

    [void] Set() {

        [xml]$AppConfigXml = Get-Content -Path $this.Path
        $XPath = "//appSettings/add[@key = '$($this.Key)']"

        $Node = $AppConfigXml.SelectSingleNode($XPath)

        If($null -ne $Node -and $this.Ensure -eq [Ensure]::Present) {
            Write-Verbose "Setting '$($this.Key)' to '$($this.Value)' in '$($this.Path)'"
            $Node.SetAttribute('value', $this.Value)
        } 

        $AppConfigXml.Save($this.Path)
    }
}

Both of these files need to be copied into a folder named the same as the module and in the PowerShell search path, typically in C:\Program Files\WindowsPowerShell\Modules

Use

Here I'm importing the module into the configuration and using a dictionary to iteratively subsitute values in a web.config.

dsc-example.ps1


#requires -version 5.0

$ConfigData = @{

    AllNodes = @(
        @{
            NodeName = $env:COMPUTERNAME
        }
    )
}


Configuration MyExampleConfiguration {

    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    Import-DscResource -ModuleName 'cApplicationConfiguration'

    Node $AllNodes.NodeName {
         
        # Other DSC Stuff...

        <# 
            MODIFY WEB.CONFIGS
        
        #>

        $Dictionary = @{
            Name = 'Alice'
            Url = 'http://www.google.com'
        }

        $WebConfigFile = Join-Path -Path $WebAppPath -ChildPath 'Web.config'
        $Dictionary.Keys.ForEach({ 
                
            $key = $_
            $value = $Dictionary[$key]

            cApplicationConfiguration $key {
                Name = $key
                Path = $WebConfigFile
                Key = $key 
                Value = $value
            }
        })

        # More DSC stuff..

    } #Node

} # Configuration


$MofFolder = Join-Path $PSScriptRoot 'MOF'

MyExampleConfiguration -ConfigurationData $ConfigData -OutputPath $MofFolder | Out-Null
Start-DscConfiguration -Path $MofFolder -Force -Wait -Verbose

Since DSC startup a new instance of PowerShell to run under, it's enough to import the module without the usual worry about unloading the original during the development cycle.