Linux Administrators: This is an article about “Puppet” for Windows, and there is already a beta version of DSC for Linux.
For those who are in the subject line: there will be nothing about updates to PowerShell 5.0, just what is available from the “box” of Windows Server 2012 R2.Preamble
In 2013, with the release of Windows Server 2012 R2, Microsoft announced the advent of Powershell Desired State Configuration (DSC).
')
At this point, I more or less imagined what similar systems for Linux were doing (for example, the already mentioned Puppet). Therefore, the proposed features seemed to me insufficient for full automatic system configuration. And only recent reports about the upcoming Powershell 5.0 and new features of DSC prompted me to pay attention to this technology again.
To figure it out, I invented a simpler puzzle:
- Suppose there is a client who wants to install an ASP.NET application developed by us on his server. In addition to IIS, we need MS SQL Server, and we also need to make some settings of the operating system and install some important utilities.
Is it possible, instead of the installation and configuration instructions, to issue a configuration script that will do everything that is required on the newly installed Windows Server 2012 R2?
For a better understanding of this article, you probably should first read the description in the Microsoft blog -
http://habrahabr.ru/company/microsoft/blog/253497/ .
Initial position
Initially it was assumed that somewhere at the hosting provider server was ordered. It has Windows Server 2012 R2 installed and we just received a notification with the administrator password and the ip-address of the server.
And we recruit one single command, for example:
makemagic -server new.example.com
and after some time we get a ready-to-use system.
Unfortunately, this is not possible yet. But I have good news - this will be in the next version of Windows Server 2016. While I am writing this article, the configuration described below (of course, without installing updates) rolls onto only the installed Technical Preview 3.
Server
If you have a Windows Server 2012 R2 image with integrated updates that you can use - feel free to skip this section.In the current version (2012 R2) - a problem in the update chain:
- One of the first tasks I tried to do was to check / adjust the time zone. To do this, you need to install the latest update of time zones.
- This update is not installed because it requires a large update KB2919355.
- Which, in turn, wants to update KB2975061 - this is in my case.
None of these updates are available for installation through Windows Update on the newly installed system.
Therefore, there are two options: 1) install all updates via Windows Update, but it will take a long time (this process can be done later), or 2) install only a couple of the most necessary ones.
In the second case, we do this: connect via RDP to the server, launch the Powershell console with elevated privileges and download the updates we need via a direct link and install them:
Invoke-WebRequest -Uri http://download.microsoft.com/download/3/9/7/3971FEA1-C483-409E-BF13-219F8A6E907E/Windows8.1-KB2975061-x64.msu -OutFile .\Downloads\Windows8.1-KB2975061-x64.msu .\Downloads\Windows8.1-KB2975061-x64.msu /quiet /norestart Invoke-WebRequest -Uri http://download.microsoft.com/download/2/5/6/256CCCFB-5341-4A8D-A277-8A81B21A1E35/Windows8.1-KB2919355-x64.msu -OutFile .\Downloads\Windows8.1-KB2919355-x64.msu .\Downloads\Windows8.1-KB2919355-x64.msu /quiet /promtrestart
After the reboot, our server will be ready to receive the configuration.
Once again I remind you that Windows Server 2016 TP3 is immediately ready for experiments with DSC.
Admin Computer
Important: So some of you might have skipped the previous section: To test this sample configuration, connect the MS SQL Server 2014 image on the server using any available method, I chose Express Edition and connected it as R :.I must upset those who have already upgraded to Windows 10 - there are nuances that will not allow using the configurations created in this system. The same applies to those who installed Powershell 5.0.
Creating and applying configurations was done on Windows 8.1. Powershell version:
PS C:\Users\nelsh> $PSVersionTable Name Value ---- ----- PSVersion 4.0 WSManStackVersion 3.0 SerializationVersion 1.1.0.1 CLRVersion 4.0.30319.34209 BuildVersion 6.3.9600.17400 PSCompatibleVersions {1.0, 2.0, 3.0, 4.0} PSRemotingProtocolVersion 2.2
But this is still not enough. It is necessary to enable Powershell Remoting on the administrator’s computer (on the server, after installing two updates, PSRemoting is already enabled). This is done in an elevated Powershell console:
Enable-PSRemoting -Force
In addition, you need to allow (!) From the administrator’s computer to connect to other computers.
Set-Item WSMan:\localhost\Client\TrustedHosts -Value *
For convenience, I also added a line with the ip-address of the created server to the
hosts
.
<ip-address> cs1.example.com
The source code of the sample is available on Github:
https://github.com/nelsh/DSC-WS2012R2 .
First configuration
This is a screenshot of the first version of the DSC-W2012R2.ps1 file (in the repository, for convenience, it lies under the name DSC-W2012R2-First.ps1).

- 1-21 lines - the actual configuration itself, called DSCW2012R2.
- In it, on line 3-7, we report that there will be only one parameter - an array of server names.
- On line 8, we connect the required PowerShell module
- 10-20 lines - a list of used resources in the configuration. In our case, there is only one “Script” resource, about it just below.
- 23-25 lines - since one of the steps is to add a user, we will need to create a password for it. In order not to deal with encryption - let's allow storing passwords in the configuration in the clear.
- 27 line - creating a configuration. As a result of executing this script, we will have the file DSCW2012R2 \ cs1.example.com.mof - something like a compiled configuration.
- 29-31 lines - launches the application of the configuration to the server named cs1.example.com. A standard window for requesting a name and password to access the server appears.
- Starting from line 33 - an example for simultaneously configuring a pair of servers
Back to resources - Powershell DSC has 12 built-in resources. Most (or rather 11 out of 12) of them are simple and clear. But for a full system setup, they are clearly not enough. Microsoft proposes to independently create the necessary resources. But, frankly, even now I have no particular desire to deal with this.
However, when I first met, I did not pay attention to the
Script resource. Consider an example in more detail:
Script First { TestScript = { if ( "Test script content" ) { $true } else { $false } } SetScript = { "Set script content" } GetScript = { return @{ Result = "Result for GetScript" GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } }
This is the simplest option that does nothing. How does he work:
- To apply the configuration, we call the Start-DSCConfiguration command (line 29).
- When the script reaches the “Script First” resource during the processing of the resource list, it first calls the code from the TestScript variable. In our example, it always returns $ true.
- But if he returned $ false, then the code from the SetScript variable would be called.
In these variables there can be any code on Powershell - the scales depend only on our fantasies. Ideally, the code in TestScipt should verify the correctness of all settings that are performed in SetScript code.
Let's try to run our first configuration (using the old habit I run from Far Manager):
powershell.exe -ExecutionPolicy RemoteSigned .\DSC-WS2012R2.ps1

Completed without errors.
In order not to explain how to start the scan from the administrator’s computer, I went to our server and ran a couple of commands.

First:
Test-DSCConfiguration
- checks the configuration. Note the last message (after the yellow text) - True. Those. configuration checked and no error detected.
The following command:
Get-DSCConfiguration
- reports details about the current configuration. It is enough to check the code of our resource “Script First” with this screenshot in order to understand what is coming from.
At this very moment, I realized that, perhaps, everything will turn out.
So. Go to ...
Advanced configuration
We start adding real tasks to the node configuration.
The first thing I want to be sure of is the name of the computer and the main dns suffix. If in our DNS zone this computer will be called cs1.example.com, then the name is cs1, and the dns suffix is example.com
Let's start with the name. At first I wrote this code:
$shortName = $Server.Split(".")[0].ToLower() Script ComputerName { SetScript = { Rename-Computer -NewName $shortName } GetScript = { return @{ Result = $env:computerName GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim() } } TestScript = { $env:computerName.ToLower() -eq $shortName } }
But it does not work. SetScript, GetScript, and TestScript do not know anything about variables outside of their range. You can only transfer using string formatting. Like this:
$shortName = $Server.Split(".")[0].ToLower() Script ComputerName { SetScript = ({ Rename-Computer -NewName "{0}" } -f @($shortName)) GetScript = { return @{ Result = $env:computerName GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim() } } TestScript = ({ $env:computerName.ToLower() -eq "{0}" } -f @($shortName)) }
With the verification of the dns suffix, everything turned out to be simpler - this is a parameter in the registry, so we use the standard Registry resource:
Registry PrimaryDomainSuffix { Key = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\" ValueName = "NV Domain" Ensure = "Present" ValueData = "example.com" ValueType = "String" }
To not update automatically during the experiments, we’ll configure Windows Update. Only receive notifications about available updates:
Script WindowsUpdateSettings { SetScript = { $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings $WUSettings.NotificationLevel=2 $WUSettings.IncludeRecommendedUpdates=$true $WUSettings.Save() } GetScript = { return @{ Result = '' GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings; $WUSettings.NotificationLevel -eq 2 -and $WUSettings.IncludeRecommendedUpdates -eq $true } }
We get a couple of important updates and still set up the time zone
The next important parameter is the time zone. And why did I assume that Moscow time is not for everyone? Apparently, for this:
So, the first problem: it looks like the built-in installation tools for a separate update via Windows Update do not exist. Fortunately, there is a small utility for this task.
Create a directory for the utility:
$abcUpdatePath = "C:\UTILS\ABC-Update" $abcUpdateZip = Join-Path $abcUpdatePath "ABC-Update.zip" File AbcUpdateDir { Ensure = "present" DestinationPath = $abcUpdatePath Type = "Directory" }
Download:
Script AbcUpdateDownload { DependsOn = "[File]AbcUpdateDir" SetScript = ({ Invoke-WebRequest -Uri http://abc-deploy.com/Files/ABC-Update.zip -OutFile {0} } -f @($abcUpdateZip)) GetScript = { return @{ Result = $TestScript GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = ({ Test-Path {0} } -f @($abcUpdateZip)) }
Unpack using the built-in Archive resource:
Archive AbcUpdateUnpack { Ensure = "Present" DependsOn = "[Script]AbcUpdateDownload" Path = $abcUpdateZip Destination = $abcUpdatePath }
We start, and, in addition to updating time zones, we will immediately update the .NET Framework to version 4.5.2:
Script AbcUpdateNet452Install { DependsOn = "[Archive]AbcUpdateUnpack" SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:2934520 } GetScript = { return @{ Result = if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { "KB2934520: Installed" } else { "KB2934520: Not Found" } GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { $true } else { $false } } } Script AbcUpdateTimeZoneInstall { DependsOn = "[Archive]AbcUpdateUnpack" SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:3013410 } GetScript = { return @{ Result = if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { "KB3013410: Installed" } else { "KB3013410: Not Found" } GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { $true } else { $false } } }
Finally we got to the time zones. I chose the time zone near Lake Baikal as a scientific method.
I found this option: you can only change the time zone using the tzutil.exe command line utility, and check only with Powershell. But this case is special - when installing, one value is used “North Asia East Standard Time”, and a completely different “Russia TZ 7 Standard Time” is checked:
Script TimeZoneSettings { SetScript = { tzutil.exe /s "North Asia East Standard Time" } GetScript = { return @{ Result = [System.TimeZone]::CurrentTimeZone.StandardName GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim() } } TestScript = { [System.TimeZone]::CurrentTimeZone.StandardName -eq "Russia TZ 7 Standard Time" } }
It seems that such a trouble with all the time zones of Russia.
Windows components
With them, everything is very simple and a very large number of examples on the Internet. It may well seem that Windows administrators are only involved in installing and removing components. Only the first two WindowsFeature resources from the configuration:
WindowsFeature offFSSMB1 { Ensure = "Absent" Name = "FS-SMB1" } WindowsFeature WebAspNet45 { Ensure = "Present" Name = "Web-Asp-Net45" IncludeAllSubFeature = $True }
In the first case, the component is deleted; in the second, it is put together with all dependencies.
Package installation
On the example of Far Manager. First, you need to download the package in a way we already know:
Script FarDownLoad { SetScript = { Invoke-WebRequest -Uri http://www.farmanager.com/files/Far30b4400.x64.20150709.msi -OutFile C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi } GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi } }
In the parameters of the Package resource is "ProductId". And there seems to be even a program that analyzes the msi file and reports this very “ProductId”. I went right through: I immediately tried to apply the configuration without this parameter and found the “ProductId” as well as the correct “Name” in the error text. The description of the resource turned out the following:
Package FarInstall { Ensure = "Present" DependsOn = "[Script]FarDownLoad" Name = "Far Manager 3 x64" ProductId = 'E5512F32-B7C1-48E3-B6AF-E5F962F99ED6' Path = "C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi" Arguments = '' LogPath = "C:\Users\Public\Downloads\FarInstall.log" }
Users and rights
By the formulation of the problem, the server is under the control of the customer, but nevertheless I have allowed that there will be a possibility of updating the web application using our continuous integration server. We use Jenkins CI (by the way, all tasks in it are also implemented on Powershell).
In the minimal version, we need a Jenkins user in the Users group and with write access to the directory where the web application is located. Let it be
c:\web
.
The user is created this way:
$JenkinsCredential = New-Object System.Management.Automation.PSCredential(` "Jenkins", ("Pa`$`$w0rd" | ConvertTo-SecureString -asPlainText -Force)` ) User JenkinsUser { UserName = "Jenkins" Ensure = "Present" Password = $JenkinsCredential PasswordChangeNotAllowed = $true PasswordNeverExpires = $true }
There is a way to use encrypted passwords in the configuration, but we will go easy. In this case, the user “Jenkins” will be created with the password “Pa $$ w0rd”.
Creating a directory is already done in the usual way. But with the appointment of rights to directories and checking had to tinker:
$AccessStringTmpl = "NT AUTHORITY\SYSTEM Allow FullControl`nBUILTIN\Administrators Allow FullControl`nBUILTIN\Users Allow ReadAndExecute, Synchronize`nCS1\Jenkins Allow Modify, Synchronize" File DirDweb { Ensure = "present" DestinationPath = "c:\web" Type = "Directory" } Script AclsDweb { DependsOn = "[File]DirDweb" SetScript = { icacls c:\web /reset /t /q takeown.exe /fc:\web /r /a /dy icacls.exe c:\web /inheritance:r icacls.exe c:\web /grant:r "Administrators:(OI)(CI)(F)" "System:(OI)(CI)(F)" "Users:(OI)(CI)(RX)" "Jenkins:(OI)(CI)(M)" /t /q } GetScript = { return @{ Result = (get-acl c:\web).AccessToString GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = ({ (get-acl c:\web).AccessToString -eq "{0}" } -f @($AccessStringTmpl)) }
It's easier to assign permissions using
icacls.exe
. In this case, it is performed in order:
- in the first line: reset all rights and enable inheritance from the parent directory
- in the second: the owner is assigned to the built-in Administrators group
- in the third: inheritance is canceled and all rights are deleted
- in the fourth: the assignment of full rights for Administrators and SYSTEM, reading for users and change for Jenkins.
For verification, use the
(get-acl c:\web).AccessToString
- the resulting string must match the
$AccessStringTmpl
variable. By the way, the error in the example - the name of the server “CS1” is explicitly indicated in the string - and the value
$Server.Split(".")[0].ToUpper()
should be substituted.
MS SQL
I was somewhat sorry that I decided not to use third-party modules. Since there is already a module for installing and configuring MS SQL Server. But I have a configuration file for automatic installation and I decided to try it.
First, we need another Windows component — the “WindowsFeature NetFrameworkCore” resource:
WindowsFeature NetFrameworkCore { Ensure = "Present" Name = "Net-Framework-Core" IncludeAllSubFeature = $True }
Secondly, the configuration file for the installer is the “Script MSSQLConfigDownLoad” resource:
Script MSSQLConfigDownLoad { SetScript = { Invoke-WebRequest -Uri https://raw.githubusercontent.com/nelsh/DSC-WS2012R2/master/SQL2014-Setup.ini -OutFile C:\Users\Public\Downloads\SQL2014-Setup.ini } GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini } }
Thirdly, you did not forget to connect the image with the distribution kit of any edition of MS SQL Server 2014?
Script MSSQL { SetScript = { r:\setup.exe /configurationfile=C:\Users\Public\Downloads\SQL2014-Setup.ini /SAPWD=1q@w3e } GetScript = { return @{ Result = if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { "Servise MSSQLSERVER is exist" } else { "Servise MSSQLSERVER not found" } GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript } } TestScript = { if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { $true } else { $false } } }
I used the Express Edition disc, which is connected as R :. The process will install the Database Engine plus FullSearch, as well as administrative tools. Testing in TestScript is the easiest - is there a service MSSQLSERVER or not.
... And when, according to the list of processes on the server, I realized that the installation had started, it became clear that the experiment can be considered complete.Further configuration may depend on the situation: if you need to urgently show a working application, then in some way we get and install our application.
If we have a scheduled installation (without emergency hands), then you can pre-configure the firewall and install utilities (monitoring, backup) - everyone can have their own options. From what is used here, only AWStats can cause the greatest difficulty: the code in TestScript will resemble a small program, but this can also be solved.
Therefore, I decided to stop at this point. In my opinion, it turned out a good example that anyone can adapt to their situation.
Preliminary results
In my opinion, DSC can be adopted without waiting for the next version of Windows Server.
In domain infrastructure, this technology cannot completely replace group policies, but it has certain advantages:
- It can be used both in manual mode and in automatic mode with a configuration server.
- It can be used regardless of the presence of Active Directory, and maybe together with group policies.
- The configuration directory can be put in the version control system.
- With the release of Powershell 5.0, we get a convenient way to use additional modules - see powershellgallery.com. There are already several dozen modules created by the community. Perhaps among them there are already those with which you can replace the scripts from my example.
Please note that errors in the example from this article are possible, and there are also probably more effective solutions.
Last important note : as far as I understand, the issue of the necessary reboot in the configuration process, even in new versions, has no solution. Therefore, the configuration described above is applied in two passes — and for both 2012R2 and 2016 — it is interrupted during the installation of components and asks for a reboot. After that, you need to restart the application configuration.
useful links