In part one I configured the environment, the pull server is set up and the target server is waiting for instructions. In this part I am going to create a DSC-resource, configure a DSC-configuration and tell the target machine what to search for at the pull server. Finally I will let it all come together in a single script to be used by any deployment tool.
The theory
By using Desired State Configuration (DSC) you describe the state you want your target machine to be in. When in that state it is assumed valid. That is the upper level of the DSC-scripts, here I define which applications and features should be on the targeted machine. Most OPS minded teams would want to be in control of these configurations but as long as you work together and provide versioning by code-repository this should be a good thing to start enabling DevOps. Teams provide the binaries (applications) to be deployed to the targets, these binaries should be accompanied by a DSC-resource file, all versioned, who can better deliver this than the same team which built the component. In the DSC-resource they describe how to bring the binaries in the required state and what parameters there are needed. When all compiled and placed correctly on the pullserver all targets will automatically sync up with the new configuration and only change when versions are out of sync.
Creating a DSC-resource
This very complex resource will just copy files from one folder to another. I am far from an expert in PowerShell but the scripts work :). You can download my resource xCustomCopyBits or you can read the entire article on how to create it yourself here.
Placing the resource in c:\program files\WindowsPowershell\Modules will enable DSC-scripts to use it during compilation. Get-DscResource can be used to validate whether the resource can be found and which versions are available.
A Dsc-Resource is created by implementing 3 methods: Test-TargetResource, Get-TargetResource, Set-TargetResource. By choosing how I implement the Test-TargetResource I can control when the resource is out of sync with its state. In my example I created a simple test whether a given path exists but I could also implement a more complex way with versioning to control the way my resource works.
function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [System.String] $Path, [System.String] $Source, [ValidateSet("Present","Absent")] [System.String] $Ensure = "Present" ) Write-Verbose("Start Test") Write-Verbose("Check target path existence") if (!(Test-Path $Path)) { Write-Verbose("Target path $Path doesnt exists!") if ($ensure -eq "Absent") { return $true } return $false } #Path exists if($Ensure -eq "Present") { return $true } return $false }
In the Set-TargetResource I implemented the code to copy my binaries to the targeted path when its state is “Present” and to remove the path when the state is “Absent”. This is where the actual deployment of the binaries takes place.
function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $Path, [System.String] $Source, [ValidateSet("Present","Absent")] [System.String] $Ensure = "Present" ) Write-Verbose("Start Set") if($Ensure -eq "Absent") { Write-Verbose("Removing directory") remove-item $Path -Recurse } else { Write-Verbose("Make sure directory exists") $Directory = Get-TargetResource -path $Path if($Directory.Ensure -ne "Present") { Write-Verbose("Creating the Directory") Copy-Item $Source $Path -Recurse $Directory = Get-TargetResource -path $Path } } }
Deploying the resource
Now I created my very complex resource I should make sure it is downloadable by the target. The way that works is to create a .zip file of the resource followed by versioning eg. “xCustomCopyBits_1.0.0.0.zip” and place it in “c:\program files\windowspowershell\dscservice\modules\”. By a created checksum the target can validate if there is a change and it should download a new version.
New-DSCChecksum .\xCustomCopyBits_1.0.0.0.zip
I decided to place my binaries side by side with the resource.zip so that when the target pulls a new version of the resource my binaries are also on the targeted machine. Another way to get your binaries to the targeted machine is to implement a file download service so the target can download the binaries there. A nice example of how to do this exists inside the xChrome resource to download and install Google Chrome.
Install-Module "xChrome" ### The MSFT_xChrome DSC Resource is implemented as a "composite" DSC Resource, which is ### a DSC Configuration Document comprised of other DSC Resource instances. Consequently, ### the MSFT_xChrome DSC Resource does not implement its own Get, Test, and Set methods. Configuration MSFT_xChrome { param ( [string]$Language = "en", [string]$LocalPath = "$env:SystemDrive\Windows\DtlDownloads\GoogleChromeStandaloneEnterprise.msi" ) Import-DscResource -ModuleName xPSDesiredStateConfiguration xRemoteFile Downloader { Uri = "https://dl.google.com/tag/s/appguid={8A69D345-D564-463C-AFF1-A69D9E530F96}&iid={00000000-0000-0000-0000-000000000000}&lang="+$Language+"&browser=3&usagestats=0&appname=Google%2520Chrome&needsadmin=prefers/edgedl/chrome/install/GoogleChromeStandaloneEnterprise.msi" DestinationPath = $LocalPath } Package Installer { Ensure = "Present" Path = $LocalPath Name = "Google Chrome" ProductId = '' DependsOn = "[xRemoteFile]Downloader" } }
Create a DSC-Script
Now everything is in place to be installed, I have to create a DSC-Script that uses the newly created binaries and resources. I created a simple script that installs the windows feature IIS, deploys my special resource with binaries and write some logging.
configuration FullSetup { Import-DscResource -ModuleName @{ModuleName="xCustomCopyBits”;ModuleVersion="1.0.0.0"} node ("localhost") { WindowsFeature IIS { Ensure = "Present" Name = "Web-server" } xCopyBits CopyDeploymentBits { Ensure = "Present" Source = join-path ${env:ProgramFiles} "WindowsPowerShell\Modules\xCustomCopyBits\1.0.0.0\DeploySources\" Path = "c:\Temp\Deployed" } Log AfterDirectoryCopy { Message = "Finished running the file resource with ID CopyDeploymentBits" DependsOn = "[xCopyBits]CopyDeploymentBits" } } } FullSetup
For the target server to find it we need to compile it into a Managed Object Format (MOF) and name it by GUID. Place the created file in “c:\program files\windowspowershell\dscservice\configuration\” and generate a new checksum like I did for the DSC-Resource.
.\FullSetup.ps1 $dest = join-path ${env:ProgramFiles} "windowspowershell\dscservice\configuration\$guid.mof" copy $source.FullName $dest New-DSCChecksum $dest
TargetMachine
Everything is now setup on the pullserver, any target server who is searching for the GUID which I just placed will pull the configuration and download the additional DSC-Resource. To set up the target machine I used the script below. Configuring where to look for new configurations and by what GUID.
Configuration PullMode{ param ( [string]$Computername, [string]$Guid ) node $Computername{ LocalConfigurationManager{ ConfigurationMode = 'ApplyOnly' ConfigurationID = $guid RefreshMode = 'Pull' DownloadManagerName = 'WebDownloadManager' DownloadManagerCustomData = @{ServerUrl = 'http://dscpullserver:8080/PSDSCPullServer.svc' AllowUnsecureConnection = 'true'} } } } PullMode -Computername 'localhost' -guid '834b9991-0506-4275-a5aa-9d22c4b25d23' Set-DscLocalConfigurationManager -Computer 'localhost' -Path ./PullMode -Verbose
Within 30 minutes the target server will get the configuration and get itself in the requested state. I can confirm that by running the Test-DSCConfiguration -verbose.
With this article I showed how we could use a DSC-Resource to deploy my binaries to a target machine. I can configure any number of machines to point to the same GUID and by that be in sync with the current production version. Teams can update their DSC-Resource and just increment the version number to roll out a new deployment. But what about keeping the DTAP environments of other teams in sync? Well that’s just some infrastructure, by providing a pull server for each DTAP environment I will be able to scale any way I want to. Just make sure I push the configuration to each pull server when I release to production.
To simplify all the steps I created a single powershell script to be used by command line or by any releasemanagement tool for example MS ReleaseManagement. By modifying the version number of the DSC-Resource and providing the parameters.
param ( [string]$componentname=$(throw "Componentnname is mandatory eg. MyComponent"), [string]$Binariesname=$(throw "Binariesname is mandatory eg. MyBinariesname"), [guid]$Guid=$(throw "Guid is mandatory eg. '834b9991-0506-4275-a5aa-9d22c4b25d23'"), [string]$EnvironmentConfiguration =$(throw "EnvironmentConfiguration is mandatory eg. myconfig.ps1") ) Function FindVersion { param ( [string]$psdFilePath =$(throw "psdfile is mandatory. eg. c:\temp\xx.psd1") ) [string]$mystring = Get-Content $psdFilePath | Select-String -Pattern "ModuleVersion" if ([string]::IsNullOrEmpty($mystring)) { throw "Couldn't determine versionnumber, make sure ModuleVersion is set in the .psd1 file" exit 1 } $verionNumber = $mystring.Substring($mystring.LastIndexOf('=') +1 ).Replace("'", "").Replace('"', "").trim() if ([string]::IsNullOrEmpty($verionNumber)) { throw "Couldn't determine versionnumer, make sure ModuleVersion is set in the .psd1 file" exit 1 } return $verionNumber } Function FindConfigName { param ( [string]$ps1FilePath =$(throw "ps1file is mandatory. eg. c:\temp\xx.ps1") ) [string]$mystring = Get-Content $ps1FilePath | Select-String -Pattern "configuration" if ([string]::IsNullOrEmpty($mystring)) { throw "Couldn't determine configuration, make sure configuration is set in the .ps1 file" exit 1 } $Name = $mystring.Substring($mystring.LastIndexOf(' ') +1 ).Replace("'", "").Replace('"', "").trim() if ([string]::IsNullOrEmpty($Name)) { throw "Couldn't configuration versionnumer, make sure configuration is set in the .psd1 file" exit 1 } return $Name } Function ZipPackage { param( [string]$componentname=$(throw "Componentnname is mandatory eg. MyComponent") ) #locate psdfile $psdFile = ".\Result\DscResource\$componentname\$componentname.psd1" #read module version information $version = FindVersion -psdFilePath $psdFile #Zippit $ResourceToZip = Convert-Path(".\Result\DscResource\$componentname") $ZipResource = Convert-path(".\Result\DscResource\") $FileName = $componentname+"_"+"$version.zip" $ZipResource = Join-Path $ZipResource $FileName Add-Type -assembly "system.io.compression.filesystem" if (Test-Path($ZipResource)) { write-host "$ZipResource already exists, removing it." Remove-Item $ZipResource -Force -Recurse } [io.compression.zipfile]::CreateFromDirectory($ResourceToZip, $ZipResource) write-host "$ZipResource created" return $ZipResource } #Grabbing binaries and placing them together with the DSC-Resource files #In the result folder Function GrabBinaries { param ( [string]$componentname=$(throw "Componentnname is mandatory eg. MyComponent"), [string]$Binariesname=$(throw "Binariesname is mandatory eg. MyBinariesname") ) $source = ".\DscResource\$componentname" $target = ".\Result\DscResource\$componentname" if (Test-Path($target)) { write-host "$target already exists, removing it." Remove-Item $target -force -Recurse } Copy-Item $source $target -Recurse -force $source = ".\Binaries\$Binariesname" $target = ".\Result\DscResource\$componentname\DeploySources\$Binariesname" if (Test-Path($target)) { write-host "$target already exists, removing it." Remove-Item $target -force -Recurse } Copy-Item $source $target -Recurse -force } #Publish the resource to the powershell service directory and make a checksum #Also copy it to the modules directory to be able to generate mof files Function PublishPackage { param ( [string]$PackageToPublish =$(throw "package to publish is mandatory eg c:\temp\xxx.zip") ) $target = join-path ${env:ProgramFiles} "WindowsPowerShell\DscService\Modules\" $Filename = split-path "$PackageToPublish" -leaf -resolve Copy-Item $PackageToPublish $target -Recurse -force New-DscCheckSum (join-path $target $Filename) $filename = [io.path]::GetFileNameWithoutExtension($Filename) write-host $Filename write-host $Filename.Length write-host $filename.LastIndexOf('_') +1 write-host $filename.Substring(0, $filename.LastIndexOf('_') ) $filename = $filename.Substring(0, $filename.LastIndexOf('_') ) #Extract to local dsc modules $target = join-path ${env:ProgramFiles} "WindowsPowerShell\Modules\$Filename" if (Test-Path($target)) { write-host "$target already exists, removing it." Remove-Item $target -Force -Recurse } Add-Type -assembly "system.io.compression.filesystem" [io.compression.zipfile]::ExtractToDirectory($PackageToPublish, $target) } #Search for the environment configuration and generate a new checksum by guid function CreatePullResource { param ( [Guid]$guid =$(throw "Guid is mandatory eg. '834b9991-0506-4275-a5aa-9d22c4b25d23'"), [string]$EnvironmentConfiguration =$(throw "EnvironmentConfiguration is mandatory eg. myconfig.ps1") ) $configFile = ".\DscEnvironment\$EnvironmentConfiguration" $name = FindConfigName $configFile $configdirectory = ".\$name" if (Test-Path($configdirectory)) { write-host "$configdirectory already exists, removing it." Remove-Item $configdirectory -Force -Recurse } #Genereate MOF . $configFile $source = Get-childitem -path $configdirectory -filter "*.mof" $dest = join-path ${env:ProgramFiles} "windowspowershell\dscservice\configuration\$guid.mof" copy $source.FullName $dest if (Test-Path "$dest.checksum") { remove-item -path "$dest.checksum" } New-DSCChecksum $dest Write-Verbose("Done with $dest") } GrabBinaries -componentname $componentname -Binariesname $Binariesname $package = ZipPackage -componentname $componentname PublishPackage -PackageToPublish $package
DevOps can be implemented in many ways, this example was only to provide a solution for the given situation and should not be implemented without being well thought-through with the people and processes available. For a complete example with all files you can click here.
Have fun,
Erick
You must be logged in to post a comment.