One Config | Multiple Nodes

Hi All,

I’m wondering about the best way to apply one config to multiple machines without creating multiple mof files etc.

So I’m thinking about ‘roles’ so a config for say a ConfigMgr Distribution Point…

At the moment, I call servers from an OU in AD, and configure the LCM with the same GUID for all of them… that way they all pull the same config. I’ve simply created this GUID from an online guid generator ha… it works, but I think there must be a tidier way?!

Peter

That’s the basic gist of it. If your goal is to avoid having multiple (potentially duplicate) MOFs, then you need to create a single MOF file with a particular GUID (or friendly name, in PSv5), and configure all of the LCMs to pull that config.

Hi Pete:
I don’t know if this is the best way but I can share how I handled this. BTW, I am using WMF5 and configuring the LCM via Configuration Names.

Basically, what I do is get the node name and node objectGUID from AD. Then I I create my mof generation script using a descriptive name for the ‘node’ (i.e. what ever the config is for, in my case ‘DSCServers’). I then take the mof; generate the checksum, copy to the config folder, also I query AD again and get the node Object GUID and add it to the ‘RegistrationKeys.txt’ file. Then I set the LCM to use the ObjectGUIDS for the RegistrationKey and the single mof name for the ConfigurationNames.

Here is my scripts:
Gets Computers and GUID from AD:

#Pull computer objects for AD
    function GetComputers {
        import-module ActiveDirectory
        Get-ADComputer -SearchBase "OU=DSC Managed Nodes,OU=SERVERS,OU=NYC,OU=Americas,DC=testlab,DC=testing,DC=com" -Filter *
    }
    $computers = GetComputers

#Pull list of computers and GUIDs into hash table
    $ConfigData = @{
        AllNodes = @(
            foreach ($node in $computers) {
                @{NodeName = $node.Name; NodeGUID = $node.objectGUID;}
            }
        )
    } 

MOF Generation Script

Configuration TestConfig

{

Import-DscResource -ModuleName 'PSDesiredStateConfiguration'

 Node DSCServers 
 {

  #Leaves a timestamp indicating the last time this script was run on the node (this resource is intentionally not idempotent)
    Script LeaveTimestamp
    {
        SetScript = {
        $currentTime = $null
        $currentTimeString = $null

        $currentTime = Get-Date
        $currentTimeString = $currentTime.ToString()
        [Environment]::SetEnvironmentVariable("DSCClientRun","Last DSC-Client run: $currentTimeString","Machine")
        eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "DSC-Client" /D "(1)Last DSC Servers Configuration run: $currentTimeString"
    }
    TestScript = {
      #$False
        $LastLogEntry = $null
        $diffMinutes = $null
        $EvtLog30Min = $False
        $LastLogEntry = (Get-EventLog "Application" | Where-Object {$_.EventID -eq 1})
        If ($LastLogEntry -like "*") {
	    $diffMinutes = (New-TimeSpan $LastLogEntry.TimeGenerated[0] (Get-Date)).TotalMinutes
            If ($diffMinutes -lt 29) {$EvtLog30Min = $True}
      
                }
        
		$EvtLog30Min
        
    }
    GetScript = {
    return @{Result="Writes a Application Log Event every time DSC Config is run"}
    }
    }
	#Install Role 'DHCP Server'
		WindowsFeature DHCP
		{
		Ensure = "Present"
		Name = "DHCP"
		}
	#Install Role 'DNS Server'
		WindowsFeature DNS
		{
		Ensure = "Present"
		Name = "DNS"
		}
	#Copies the Firewall Rule File
		File "FWRuleFilecopy"
        {
            Ensure = "Present" 
            Type = "Directory"
            MatchSource = $True
			Checksum = "SHA-256"
            Recurse = $true
            SourcePath = "\\lab-dsc-01\shared\FWRules"
            DestinationPath = "C:\DSCFiles\FWRules"    
        }
        Log AfterFWRuleFileCopy
        {
            # The message below gets written to the Microsoft-Windows-Desired State Configuration/Analytic log
            Message = "Finished running the file resource with ID FW Rule File copy"
            DependsOn = "[File]FWRuleFilecopy" # This means run "DirectoryCopy" first.
        }
	#Copies the Print Drivers File
		File "PrintDriverFilecopy"
        {
            Ensure = "Present" 
            Type = "Directory"
            MatchSource = $True
			Checksum = "SHA-256"
            Recurse = $true
            SourcePath = "\\lab-dsc-01\shared\PrintDrivers"
            DestinationPath = "C:\DSCFiles\PrintDrivers"    
        }
        Log AfterPrintDriverFileCopy
        {
            # The message below gets written to the Microsoft-Windows-Desired State Configuration/Analytic log
            Message = "Finished running the file resource with ID Print Drivers File copy"
            DependsOn = "[File]PrintDriverFilecopy" # This means run "DirectoryCopy" first.
        }	
    #Ensure WindowsFirewall Running
        Service WindowsFirewall
        {
            Name = "MPSSvc"
            StartupType = "Automatic"
            State = "Running"
        }
	#Sets Page File Size
        Registry "Sets Page File Size"
        {
            Ensure = "Present"
            Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"
            ValueName = "PagingFiles"
            ValueType = "Multistring"
			ValueData = "c:\pagefile.sys 6142 6142"
        }
    #Disable TCPIPv6
        Registry DisableTCPIPv6 
        {
            Ensure = "Present"
            Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP6\Parameters"
            ValueName = "DisabledComponents"
            ValueType = "DWord"
            ValueData = "255"
        }
    #Disable EDNSProbes
        Registry DisableEDNSProbes 
        {
            Ensure = "Present"
            Key = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\DNS\Parameters"
            ValueName = "EnableEDNSProbes"
            ValueType = "DWord"
            ValueData = "0"
        } 
    }
}

TestConfig -ConfigurationData $ConfigData -OutputPath "$Env:Temp\TestConfig"

Generates the Checksum, copies to configuration folder, and adds GUID to RegistrationKeys.txt

#Creates CheckSums for the .mof files

    write-host "Creating checksums..."
    New-DSCCheckSum -ConfigurationPath "$Env:Temp\TestConfig" -OutPath "$Env:Temp\TestConfig" -Verbose -Force

#Copies the Node ObjectGUID to the RegistrationKeys file

    write-host "Getting the Node ObjectGUID and copying to the RegistrationKeys file..."

    $CompGuid = (Get-ADComputer -SearchBase "OU=DSC Managed Nodes,OU=SERVERS,OU=NYC,OU=Americas,DC=lab2,DC=mckinsey,DC=com" -filter *).objectguid


    foreach ($Guid in $CompGuid) {
    if($guid.guid -notin (gc "C:\Program Files\WindowsPowerShell\DscService\RegistrationKeys.txt")){

    $GUID.guid | out-file "C:\Program Files\WindowsPowerShell\DscService\RegistrationKeys.txt" -append
    }
    }

#Copies the configuration/.mof files and checksum files to the configuration store

    write-host "Copying configurations to pull service configuration store..."
    $SourceFiles = "$Env:Temp\TestConfig\*.mof*"
    $TargetFiles = "$env:SystemDrive\Program Files\WindowsPowershell\DscService\Configuration"
    Move-Item $SourceFiles $TargetFiles -Force
    Remove-Item "$Env:Temp\TestConfig\"

Config the LCM

[DSCLocalConfigurationManager()]

Configuration ConfigurationForPull
{ 
    Node $allnodes.NodeName
    {
        Settings 
        { 
            RefreshMode = "PULL";
            AllowModuleOverwrite = $True;
            RebootNodeIfNeeded = $True;
            RefreshFrequencyMins = 30;
            ConfigurationModeFrequencyMins = 15; 
            ConfigurationMode = "ApplyAndAutoCorrect";
        }
        ConfigurationRepositoryWeb lab-dsc-01
        {
            AllowUnsecureConnection = $True
            ServerUrl = "http://lab-dsc-01:8080/PSDSCPullServer.svc"
            RegistrationKey = "$($Node.NodeGUID)"
            ConfigurationNames = @("DSCServers")
        }
    }
    } 

ConfigurationForPull -ConfigurationData $ConfigData -OutputPath "$Env:Temp\PullDSCCfg"
Set-DscLocalConfigurationManager -Path "$Env:Temp\PullDSCCfg" -verbose -force

I hope this helps a bit.

Hi,
@Ed : That’s a nice trick with the GUID in the registrationkey.txt but I consider it an overkill as one key would be enough. I’d hate to maintain that long list of GUIDs when you decommission a server. Also the key itself is deleted from the LCM after the initial registration to the pull server.

@Peter : The example that Ed posted is the way to do it imho. What the example needs are changes to the $Configdata to set Roles, which you might get from a custom property in your ad schema for example, and in the mof generation script at the node section to check the role and apply the settings, and the have a node section per role with appropriate settings in it. This will undoubtedly make this script huge and abit hard to manage.

That said one mof to control them all isn’t the best way. Not saying one mof per node is better…its not, but say 5-10 mofs will be better solution. In Eds example, you can separate the long configuration file into blocks, together with Roles in your configurationData, generate the mofs with specific names and add them all to the LCM ConfigurationNames value.
This will give several values:

  1. More flexibility in allowing more differentiation in the servers

  2. Easier unit testing. Cant emphasize enough how much pester is important.

  3. You never change configuration scripts, you just push a LCM that has different values in the ConfigurationNames.

  4. You’re not limited to host provisioning but you can also apply this to software you want to run on that node, although Don keeps reminding us, rightfully, that we shouldn’t bend DSC to do things it might not have been ideally created for :wink:
    I still use dsc for applications, via Package resource or PackageManagt to pull from our internal nuget repo, mainly as I going to swap my RM agent based deployments to dsc based to allow me to get tfs 2015 and RM vNext on-premise coming later this year.

To be fair there is a cavity in the form of harder control when you want to have cross-node dependency, but that can be addressed as well.

The correct way of specifying the same mof to different nodes is using the ConfigurationNames property as described here https://msdn.microsoft.com/en-us/powershell/dsc/pullclientconfignames