Dynamically creating elements in a configuration

I’m working on a configuration that will be used to build out ColdFusion servers. Each server will have a variable number of server instances running in its cluster and for each server there is a service created in Windows. First a Script Resource runs that builds out the ColdFusion servers (the services are built as part of this script). Then, I’m trying to set the services in my configuration using a ForEach that loops through an array of service names I pass into the config data. Is this possible? I’m getting a “Unexpected token ‘}’ in expression or statement.” in my syntax after the Service resource.

Here is the PSD1 file:

@{
  AllNodes = @(
    @{
        NodeName = "*"
        NeoConfigDestinationPath = "D:\ServerBox\Servers\JRun4\_build\shared\config"
    }
    @{
        NodeName = 'PullServerName'
        Role=@('DSCPullServer')
    }
    
    @{
        NodeName = 'DevIntTest'
        Role=@('IIS','ServerBox','DevInt')
        CFServices=@("Adobe CF9 1","Adobe CF9 2","Adobe CF9 3")
    }
     @{
         NodeName = 'AlphaTest'
         Role=@('IIS','ServerBox','Alpha')
         CFServices=@("Adobe CF9 1")
     }
  )
  
  ServerBoxConfig = @{
    SourcePath = "\\SHARE\PATH\DevOps\ServerBox"
    DestinationPath = "D:\ServerBox"
    NeoConfigSourcePath = "\\SHARE\PATH\DevOps\ServerConfig\Environments\_Default\NeoConfig"
  }

  DevIntConfig = @{
    SourcePath = "\\SHARE\PATH\DevOps\DevInt"
    DestinationPath = "D:\ServerBox\IIS\wwwroot"
    NeoConfigSourcePath = "\\SHARE\PATH\DevOps\ServerConfig\Environments\DevInt\NeoConfig"
  }

  AlphaConfig = @{
    SourcePath = "\\SHARE\PATH\DevOps\Alpha"
    DestinationPath = "D:\ServerBox\IIS\wwwroot"
    NeoConfigSourcePath = "\\SHARE\PATH\DevOps\ServerConfig\Environments\Alpha\NeoConfig"
  }
}

And then here is the configuration I’m working up:

Configuration EqConfig {

  Import-DSCResource -Module xPSDesiredStateConfiguration

   Node $AllNodes.Where({$_.role -eq 'DSCPullServer'}).NodeName {...} #DSCPullServer
   
   Node $AllNodes.Where({$_.role -eq 'IIS'}).NodeName {...} #IIS 
   
   Node $AllNodes.Where({$_.role -eq 'ServerBox'}).NodeName {

      File ServerBox {
        Ensure = "Present" 
        Type = "Directory"
        Recurse = $true
        MatchSource = $true
        Force = $true
        Checksum = "modifiedDate"
        SourcePath = $ConfigurationData.ServerBoxConfig.SourcePath
        DestinationPath = $ConfigurationData.ServerBoxConfig.DestinationPath
      }

      File ServerBox_Default_Config {
        Ensure = "Present" 
        Type = "Directory"
        MatchSource = $true
        Force = $true
        Checksum = "modifiedDate"
        SourcePath = $ConfigurationData.ServerBoxConfig.NeoConfigSourcePath
        DestinationPath = $AllNodes.NeoConfigDestinationPath
        DependsOn = "[File]ServerBox"
      }

   } #ServerBox   
   
   Node $AllNodes.Where({$_.role -eq 'DevInt'}).NodeName {

          File DevInt {
            Ensure = "Present" 
            Type = "Directory"
            Recurse = $true
            MatchSource = $true
            Force = $true
            Checksum = "modifiedDate"
            SourcePath = $ConfigurationData.DevIntConfig.SourcePath
            DestinationPath = $ConfigurationData.DevIntConfig.DestinationPath
            DependsOn = "[File]ServerBox"
          }

          File DevInt_Config {
            Ensure = "Present" 
            Type = "Directory"
            MatchSource = $true
            Force = $true
            Checksum = "modifiedDate"
            SourcePath = $ConfigurationData.DevIntConfig.NeoConfigSourcePath
            DestinationPath = $AllNodes.NeoConfigDestinationPath
            DependsOn = "[File]ServerBox"
          }

          Script createDevIntServers {
            SetScript = "Create-ColdfusionServer -clusterCount $Node.CFServices.Length"
            TestScript = ""
            GetScript = ""
          }
          
          Service $Node.CFServices.ForEach({
            Name = $_
            State = 'Running'
            DependsOn = "[Script]createDevIntServers"
          })

   } #DevInt

   Node $AllNodes.Where({$_.role -eq 'Alpha'}).NodeName {

          File Alpha {
            Ensure = "Present" 
            Type = "Directory"
            Recurse = $true
            MatchSource = $true
            Force = $true
            Checksum = "modifiedDate"
            SourcePath = $ConfigurationData.AlphaConfig.SourcePath
            DestinationPath = $ConfigurationData.AlphaConfig.DestinationPath
            DependsOn = "[File]ServerBox"
          }

          File Alpha_Config {
            Ensure = "Present" 
            Type = "Directory"
            MatchSource = $true
            Force = $true
            Checksum = "modifiedDate"
            SourcePath = $ConfigurationData.AlphaConfig.NeoConfigSourcePath
            DestinationPath = $AllNodes.NeoConfigDestinationPath
            DependsOn = "[File]ServerBox"
          }

          Script createAlphaServers {
            SetScript = "Create-ColdfusionServer -clusterCount $Node.CFServices.Length"
            TestScript = ""
            GetScript = ""
          }
          
          Service $Node.CFServices.ForEach({
            Name = $_
            State = 'Running'
            DependsOn = "[Script]createAlphaServers"
          })

   } #Alpha

} #Configuration

DSC resources don’t accept an array of names the way the Node command does. However, you can put the resource inside a loop to get the same effect:

# Instead of:

          Service $Node.CFServices.ForEach({
            Name = $_
            State = 'Running'
            DependsOn = "[Script]createAlphaServers"
          })

# Try:

          $Node.CFServices.ForEach({
              # Make sure your resource names are unique
              Service "Service-$_" {
                Name = $_
                State = 'Running'
                DependsOn = "[Script]createAlphaServers"
              }
          })

Awesome! That did the trick, thanks.

Now I’m getting this error when running the config: Add-NodeKeys : The key properties combination ‘D:\ServerBox\Servers\JRun4_build\shared\config’ is duplicated for keys ‘DestinationPath’ of resource ‘File’ in node ‘CADEVEA02’. Please make sure key properties are unique for each resource in a node.

I’ve tried moving NeoConfigDestinationPath from AllNodes down into the non-node data, but keep getting the same issue.

You’ve got duplicate File destination paths in your various ServerBox / DevInt / Alpha conditionals. I don’t fully understand what your logic is trying to do at this point… are you copying files from many different sources into the same directory? If so, maybe your design just doesn’t mesh well with that of the built-in DSC File resource, due to its choice of Key properties. That might be a bit of a headache to fix.

Yeah, that’s exactly what I’m trying to do. I’ve got a default set of config files that copy in first, then depending on the environment I’m building I’d copy in more config files into the same path. This can’t be done in DSC? That seems… limiting.

I guess I can include the default set in the initial copy of “ServerBox” and then just have one file resource that copies the environment config files in after. But still, seems odd that you can’t have more than one file resource have the same destination.

Just about anything can be done with DSC, but sometimes the design choices made when a resource is written can have some unintended effects. In this case, Microsoft wrote a “File” resource that can be used for both Files and Directories, and called the Destination path the “key”. Because there’s a rule that says a DSC configuration can’t have two resources with duplicate keys, this is screwing with what you’re trying to do.

If they had separated File and Directory operations out into two resources (and the Directory version would likely have a compound key of Source and Destination), you’d be fine. This same problem has come up with Environment variables; there’s a flag called Path that changes the resource’s behavior so it treats the variable as a semicolon-separated list of values, instead of a single value. But you can’t add two different values to the Path because you’d be duplicating the Key resource (the environment variable name) again.

So what you need at this point is a new resource that has a better choice of Key properties and behavior that match your intended use. That’s an annoying amount of work to have to do, but it’s possible. Alternatively, you could modify your config to copy each of those source folders to a different folder on the local computer (perhaps in a temporary location), and then use something like a Script resource to combine them all into a single folder, and another File resource to make the complete folder copy happen locally. That’s ugly, but a lot less work than re-implementing the File resource yourself.

Hopefully this sort of situation will be resolved as more people start using DSC and giving feedback to Microsoft.