DEV Community

Jānis Veinbergs
Jānis Veinbergs

Posted on

Achieving single command Infrastructure deployment using PowerShell DSC.

So, previously I wrote about deployment process to IIS servers. However wouldn't it be nice to keep that IIS configuration in sync, moreover between production and staging environment?

Configuration deployment world.

One way is to create the initial installation and configuration script. Nice and fast to setup additional server, a great first step. But:

  • The configuration has to be maintained and your script has to account for that (if this already exists then do nothing else configure).
  • You need to have a mechanism to run the script. Scheduler?
  • Your systems may drift if some processes or users do some configuration.
  • You do not know the diff between the current system configuration and the script.

There are many tools that may be used to provide uniform server configuration, like Group Policy, System Center Configuration Manager/Microsoft Endpoint Configuration Manager, PowerShell DSC (Desired State Configuration), Docker and various other software configuration management tools etc. All of those could be used to help achieve completely or partially uniform configuration across multiple hosts. Some of them are not free, some of them depends on Active Directory, some of them have GUI and so on.

Enter PowerShell DSC

Lets just explore PowerShell DSC, that you don't have to license and if you, like me, enjoy using PowerShell, this may be a tool for you.

Things I immediately found satisfying with PowerShell DSC:

In an ideal world, once our server is joined to domain, I would like a one-click install for servers, be it staging or production environment. PowerShell DSC achieves quite a lot, but not necessarily a single click deployment. We will see why and how to overcome it.

Why did I go out to try PS DSC?

  1. Didn't want to write configuration documentation - let there be text that can't be outdated.
  2. IaC - wanted to have valid configuration in source control
  3. I heavily use PowerShell and this would be a "natural extension".
  4. This has come as a bonus: On deployment, check if IIS webserver configuration is as written.

Writing DSC script

I will be using PowerShell 5.1 with DSC 1.1. Within v2, DSC has been split out from powershell package and v3 will provide cross-platform capabilities.

What I'm going to concentrate on is overcoming hurdles when writing DSC and not the actual configuration itself. You can drill down the concepts and how-to documentation to learn DSC. My requirements were:

  1. I want ONE or maybe two simple commands I can use to configure IIS servers
  2. I want to be able target all or subset of servers
  3. I want to target different environments (prod, staging)

Let's look at a simple dsc.ps1 script.

configuration SimpleConfiguration {
    # DSC provides only simple tasks. Usually modules will be imported to do additional tasks.
    Import-DscResource -ModuleName 'ComputerManagementDsc' -Name 'PendingReboot', 'TimeZone', 'SystemLocale'
    # One can evaluate expressions to get the node list E.g: $AllNodes.NodeName - AllNodes can be passed when calling function.
    node ("Node1","Node2","Node3")
    {
        # Call Resource Provider E.g: WindowsFeature, File
        WindowsFeature WebServerRole { Ensure = "Present"; Name = "Web-Server" }
        SystemLocale EnglishLocale { SystemLocale = "en-US" ; IsSingleInstance = "Yes" }
        File site1Folder { DestinationPath = "$env:SystemDrive\inetpub\sites\site1"; Type = 'Directory' }
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding resources

What does SystemLocale EnglishLocale { SystemLocale = "en-US" ; IsSingleInstance = "Yes" } mean?

It means I'm calling SystemLocale resource, giving it whatever friendly name EnglishLocale and passing hashtable with properties resource accepts: SystemLocale, IsSingleInstance. You can get available properties like that:

Get-DscResource SystemLocale -Syntax
SystemLocale [String] #ResourceName
{
    IsSingleInstance = [string]{ Yes }
    SystemLocale = [string]
    [DependsOn = [string[]]]
    [PsDscRunAsCredential = [PSCredential]]
}
Enter fullscreen mode Exit fullscreen mode

Why can't it be a simple install script?

It can, but in real world - without the word "simple". It's written in Desired State Configuration Overview for Engineers. In short: you focus on WHAT to do not on HOW. DSC provides idempotent (repeatable) deployments. Along with goodies like Continue configuration after reboot, Credential encryption and other goodies.

Why this silly syntax?

Well, because this resource under the hood actually implements 3 functions: Get, Set, Test

  • Get is a representation of state when, for example, running Test-DscConfiguration -Detailed or Get-DscConfiguration
  • Set - Applies configuration
  • Test - Determines whether configuration has to be applied. If not, Set is NOT run. That is, if system is already compliant with this resource, it won't re-run.

When Custom resources gets written, these functions get implemented. For example, using built-in Script resource, you also have to implement all of them. Take for example a configuration, that ensures BUILTIN\Users have no access to particular folder:

Script site1FolderPermissions {
  GetScript = { @{Result="Path = $env:SystemDrive\inetpub\sites\site1; Acl = $(Get-NTFSAccess "$env:SystemDrive\inetpub\sites\site1" | %{ $_.ToString() })" } }
  TestScript = {
     (Get-NTFSAccess "$env:SystemDrive\inetpub\sites\site1" | ? Account -eq 'BUILTIN\Users' | measure | select -ExpandProperty Count) -eq 0
  }
  SetScript = {
    Disable-NTFSAccessInheritance -Path "$env:SystemDrive\inetpub\sites\site1"
    $acl = Get-Acl "$env:SystemDrive\inetpub\sites\site1"
    $acl.Access | ?{ $_.IsInherited -eq $false -and $_.IdentityReference -eq 'BUILTIN\Users' } | % {$acl.RemoveAccessRule($_)}
    Set-Acl -AclObject $acl "$env:SystemDrive\inetpub\sites\site1"
  }
  DependsOn = "[File]site1Folder"
}
Enter fullscreen mode Exit fullscreen mode

Drawbacks

Modules

Drawbacks or pain-points or maybe stuff that hasn't been provided by DSC out-of-the-box: Distributing modules. And this is the reason I can't have a single command to bootstrap my servers. The modules have to be installed on the host that are building the configuration AND the host that gets configuration deployed. Otherwise, when encountering Import-DscResource -Module CertificateDsc -ModuleVersion 5.1.0, it will tell you:

Could not find the module '<CertificateDsc, 5.1.0>'.
Enter fullscreen mode Exit fullscreen mode

Could not find the module error

However moving Import-DscResource command just will result in error:

Import-DscResource cannot be specified inside of Node context
Enter fullscreen mode Exit fullscreen mode

Import-DscResource cannot be specified inside of Node context

So the takeaway: we can't install a module and use its resources in a single configuration. These modules are required on the host that are building configuration and on target nodes. Modules are essential to any configuration, except the most trivial ones.

Moreover, the PackageManagement resource you see in a screenshot is a module itself that must be installed from PowerShell gallery before I can actually use it to install other modules 🤦‍♂️

Remember me telling how DSC enables writing more simple configuration comparing to install scripts? Turns out with DSC comes complexity anyways if I want automatic module provisioning. 🤷‍♂️ I don't want anyone (and my future self) running the deployment having to think about what modules must be installed on source & target machines.

So I will be using another configuration to install modules, but without any dependencies on modules. I'll run this configuration on hosts that require these modules and only then apply main configuration. First, I apply InstallPSModules configuration and then the SampleConfig configuration. There is a gotcha: I need modules on localhost too. But running Start-DscConfiguration .\InstallPSModules\ -ComputerName localhost gives me error:

The WinRM client sent a request to the remote WS-Management service and was notified that the request size exceeded the configured MaxEnvelopeSize quota
Enter fullscreen mode Exit fullscreen mode

It may be misleading. If I pass -Credential parameter, specifying which user I want to use to connect to localhost (admin), then it works. This is due to the way users/computers/winrm are configured in my environment.

The other thing is that it may override some other DSC configuration applied to this computer. Anyways, I had to choose a tradeoff. I have a build task (a function) that installs modules on local computer and another for deploying modules to target nodes. But then I have to remember adding any dependencies in 2 places. Maybe I'm trying too hard for striving to reach the goal of "1 simple command to perform deployment"...

What I'm doing is installing from PowerShell gallery. You could set-up so that you have modules available on some share/local machine and copy them to target machine C:\Windows\system32\WindowsPowerShell\v1.0\Modules folder. Or perhaps any of these folders:

PS C:\Repos\psdsctest> C:\Tools\PSTools\PsExec64.exe -s powershell.exe -Command "`$env:PSModulePath -split ';'"

C:\Windows\system32\config\systemprofile\Documents\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules
Enter fullscreen mode Exit fullscreen mode

That's PSModulePath for SYSTEM account. The configuration itself is being applied in the context of SYSTEM account on target nodes. Important to keep in mind when writing configurations. If you need to apply some stuff within context of another user, read about Credential Options in Configuration Data.

Here is an example build task for some configuration I wrote that copies files to particular computers before doing deployment (err, read "Hooking it all up" to see that I'm using a build tool, thus another "weird" syntax):

task copyRequiredFilesToNodes {
    exec {
        Write-Host "Copying $BuildRoot\winlogbeat to target nodes"
        $nodeSessions = New-PSSession -ComputerName $Nodes
        Invoke-Command -Session $nodeSessions { Remove-Item -Recurse -Path "C:\Windows\Temp\winlogbeat" -Verbose -ErrorAction SilentlyContinue}
        $nodeSessions | % { Copy-Item -Recurse -Path "$BuildRoot\winlogbeat" -Destination "C:\Windows\Temp\" -ToSession $_ -Force -Verbose }
    }
}
Enter fullscreen mode Exit fullscreen mode

DSC Engine configuration for enabling reboot

Moreover, DSC Engine itself must be configured to, for example, allow reboots with PendingReboot resource. DSC Engine configuration is applied with Set-DscLocalConfigurationManager command and uses different configuration:

[DSCLocalConfigurationManager()]
configuration DSCConfig
{
    Node $AllNodes.NodeName
    {
        Settings
        {
            #allow reboot with PendingReboot resource from ComputerManagementDsc module
            RebootNodeIfNeeded = $true
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Applying configuration:

Set-DscLocalConfigurationManager .\DSCConfig\ -ComputerName localhost -Verbose
Enter fullscreen mode Exit fullscreen mode

So, multiple gotchas here. Lets hook it all up within a single build script we can call.

Targeting other environments

Configuration scripts are powershell scripts. You may choose to pass parameter or detect which env you are in any other way:

$environment = if ($env:USERDOMAIN -ieq "prod.example.com") {"production"} else {"staging"}
Enter fullscreen mode Exit fullscreen mode

Then you can use if statements or override variables based on environment.

if ($environment -eq "production") {
    #Call some resources
} else {
    #Call other resources
}
Enter fullscreen mode Exit fullscreen mode

Hooking it all up

So I need to take a step back and Start-DscConfiguration won't be my entry point to deployment. I need a .ps1 script that installs modules. Or, bear with me, I'll be using a PowerShell build tool: Invoke-Build.

GitHub logo nightroman / Invoke-Build

Build Automation in PowerShell

NuGet PSGallery

Build Automation in PowerShell

Invoke-Build is a build and test automation tool which invokes tasks defined in PowerShell v2.0+ scripts. It is similar to psake but arguably easier to use and more powerful. It is complete, bug free, well covered by tests.

In addition to basic task processing the engine supports

  • Incremental tasks with effectively processed inputs and outputs.
  • Persistent builds which can be resumed after interruptions.
  • Parallel builds in separate workspaces with common stats.
  • Batch invocation of tests composed as tasks.
  • Ability to define new classes of tasks.

Invoke-Build v3.0.1+ is cross-platform with PowerShell Core.

Invoke-Build can be effectively used in VSCode and ISE.

Several PowerShell Team projects use Invoke-Build.

The package

The package includes the engine, helpers, and help:

You may use other tools too: psake, make, cake, fake or any other *ake you are familiar with. I look at them as a tools that make build tasks behind simple commands and help me answer: How did I run that code again?

So, we still must install Invoke-Build? Well, conveniently, there is a template for invoke-build file that detects if Invoke-Build is installed or not and installs it and we'll use it.

The result ended up using 4 scripts. However it can be used as an example/bootstrap project to speed up your implementation, if you choose to go down the same path:

  • build.ps1 - Invoke-Build script. Entry point
  • DSCConfig.ps1 - DSC Engine configuration. Allows reboots
  • InstallPSModules.ps1 - Module deployment
  • SampleConfig.ps1 - Where I would put my configuration.

In the end, I can checkout the code and run:

.\build.ps1 deploy -Nodes localhost
# or
.\build.ps1 deploy -Nodes server1, server2, server3 -Credential (Get-Credential)
# By the way, whatever credential you pass to the build script is only for connecting to target node and starting DSC configuration. Remember that resources itself are applied within SYSTEM context.
Enter fullscreen mode Exit fullscreen mode

I'v put up the scripts at psdsctest repository:

PowerShell DSC - Infrastructure as Code. Work-around pain points.

Show how to initiate PowerShell DSC configuration with one-single command. Includes configuration of DSC Engine and module dependency installation.

Available tasks:

./build.ps1 ?
    Name                   Jobs                                                                 Synopsis
    ----                   ----                                                                 --------
    deployDSCConfig        {buildDSCConfig, {}}                                                 Apply DSC engine configuration to allow reboots. Kind of a special case: https://docs.mi... 
    installModules         {}                                                                   Install required powershell modules for current host for build to work.
    buildDSCConfig         {}                                                                   Generate .mof files configuring local DSC settings
    buildInstallPSModules  {}                                                                   Generate .mof files for PowerShell module installation for passed nodes (prerequisite fo...
    buildConfig            {}                                                                   Generates sample config
    build                  {installModules, buildDSCConfig, buildInstallPSModules, buildConfig} Build project. Build will call all those other taskkks
    deploy                 {clean, build, deployDSCConfig, deployInstallPSModules...}           Deploy will push configuration to nodes. Will call build before.
    deployInstallPSModules {buildInstallPSModules, {}}                                          Deploy only module installation. Will push configuration to nodes.
Enter fullscreen mode Exit fullscreen mode

Tying in Azure DevOps

In the end, when deploying code to IIS servers, I included a call to Test-DscConfiguration -Detailed to inform me whether host configuration has drifted away.

$testdsc = Test-DscConfiguration -Detailed
if (-not $testdsc.InDesiredState) {
  Write-Warning "Host configuration has drifted. Test-DscConfiguration returned False"
  Write-Output $testdsc.ResourcesNotInDesiredState
}
Enter fullscreen mode Exit fullscreen mode

Oh no, 2 servers have configuration drift?

I'm not using any automatic corrections or not yet using DevOps to automatically apply configuration when DSC configuration changes have been committed. But it's possible to do that. However there comes additional complexity - perhaps I wouldn't like to reboot all servers at once if reboot is required. Or implement rolling deployment - apply configuration to some servers, see how they behave and then others.

Conclusion

I really wish PowerShell DSC would have provided OOTB way of installing modules and requiring them after installation. Anyway, I'll have the "frame-work" ready for the next DSC project.

Otherwise it's a nice tool for configuration deployment, if you can find the modules. But there are plenty.

Moreover, I do recommend looking at Building a Continuous Integration and Continuous Deployment pipeline with DSC article - it even shows how to run tests to validate your configuration.

Looking forward to any comments on what could have been done better.

Top comments (3)

Collapse
 
vlariono profile image
vlariono

Thanks for the article. I would suggest adding Azure Automation to deployment pipeline. Modules can be uploaded to automation account and then, when configuration is applied to a server, they will be pooled from Automation account to target machine automatically. Azure automation also helps to track state of the machine if configured to "Apply and monitor" mode.

I also heard rumors around custom pool server, but I never used it. It can be helpful in fully isolated environments.

Collapse
 
janisveinbergs profile image
Jānis Veinbergs

@vlariono thanks for the valuable comment. So there is a better way (if using pulling strategy)! I looked at docs.microsoft.com/en-us/azure/aut... and could read what you say:

When Automation executes runbook and DSC compilation jobs, it loads the modules into sandboxes where the runbooks can run and the DSC configurations can compile. Automation also automatically places any DSC resources in modules on the DSC pull server. Machines can pull the resources when they apply the DSC configurations.

Custom pull server is actually officially documented: Desired State Configuration Pull Service.

It talks about publishing Module and .mof files to pull server. And I could find a confirmation that PowerShell would pull modules from there:

Clients that request a configuration will need the required DSC modules. A functionality of the pull server is to automate distribution on demand of DSC modules to clients. If you are deploying a pull server for the first time, perhaps as a lab or proof of concept, you are likely going to depend on DSC modules that are available from public repositories such as the PowerShell Gallery or the PowerShell.org GitHub repositories for DSC modules.

So basically adding pull server to the recipe may really simplify DSC configuration deployment. And it would be good for another reason: No dependency on PowerShell Gallery, locally available modules. That may reduce risks of npm-like attacks where malicious versions are being posted only by 3rd party and used without reviewing. Luckily it is way easier to audit a version of PS Module than an NPM package a deep dependency tree.

Collapse
 
seann profile image
Seann Alexander

A way to use one script, is to put the DSC config in a "TEXT" variable, and run it as a script block.

`Install-Module -Name 'xPSDesiredStateConfiguration' -Repository PSGallery -Force -Scope AllUsers -Verbose
Install-Module -Name 'xWebAdministration' -Repository PSGallery -Force -Scope AllUsers -Verbose
Import-Module 'xPSDesiredStateConfiguration' -Force -Verbose
Import-Module 'xWebAdministration' -Force -Verbose

$dscScript = @'
Import-Module 'xPSDesiredStateConfiguration' -Force -Verbose
Import-Module 'xWebAdministration' -Force -Verbose

Configuration CreateDirectories
{
param(
[string[]]$NodeName = 'localhost',
[string[]]$modulePath = 'C:\MyModules'
)
Node $NodeName
{
File CreateModulePathDirectory
{
Type = 'Directory'
DestinationPath = "$($modulePath)" # Replace with the value of $modulePath
Ensure = 'Present'
}
}
}
FileResourceConfiguration -NodeName 'localhost'
Start-DscConfiguration -Path .\FileResourceConfiguration -Wait -Verbose
'@

$scriptBlock = [scriptblock]::Create($dscScript)
& $scriptBlock`