OPTEN, das einzige Umbraco-zertifizierte Unternehmen der Schweiz

Our Octopus Odyssey: Part 3. Variables and Powershell

This blog is part 3 of a series.

Part 1. The Basics
Part 2. Build Script
Part 4. Database
Part 5. web.config
Part 6. NuGet Repository
Part 7. less & CSS

The next part of our deployment process which I wanted to automate was that we always zipped up the Web folder which contained our website before deploying the new website so we had it as a backup. Also we set IIS to point to an under construction page whilst deploying so that any users visiting the website would see a nice message. I achieved both of these goals by writing Powershell scripts which Octopus Deploy would run before and after deployment.

Pre and post deploy

Powershell is an incredibly powerful tool, in combination with Octopus Deploy it makes anything possible on the deploy server. There are two ways to include powershell scripts in the deploy process. One is to add them to a deploy step. But the way I chose to do it was to create two files in the root of each project which I wanted to deploy, one file named PreDeploy.ps1 and one named PostDeploy.ps1. Octopus Deploy will look for these files by default and if it finds them it will run the pre deploy file before deployment and the post deploy once deployment is complete.

Variables

What makes the combination of Octopus Deploy and Powershell even more powerful are variables. In Octopus deploy you can create variables which can be scoped to certain environments, roles, machines or steps. You can even use a variable inside another variable! This is an incredibly powerful feature and I really love it, it makes deploying with Octopus Deploy so much simpler. For example, I had to set the install path with a different custom install directory for staging and live for each step. Instead of making a new step one for staging and one for live. Variables meant that I could use the same step and make two variables, both named installDir, but one scoped to the Live environment and with the value E:\Data\stagingWebSiteName\WEB and the other scoped to the Staging environment and with the value E:\Data\liveWebSiteName\WEB. Then in the step I could just pass in the variable like so: #{installDir}

Installdir

The more I worked with variables the more I realised that I could use them. I realised in the example above that both paths have the same root E:\Data and this root was used in other paths as well. So I could make a new variable named CommonRoot and pass this in to the installDir variables. Then I also realised that the siteName was used in many places. I could make this into another variable, scoped to the environment, role and step (it is possible to combine scopes, the precedence rules can be found here:  http://docs.octopusdeploy.com/display/OD/Variables). This meant that I could create a variable named WebsiteName scoped to Live, Website1Role and AdminDeployStep with the value liveAdminWebSite1Name and another scoped to Staging, Website2Role abd FrontendWebDeployStep with the value stagingWebSite2Name. Then I did not need two installDir variables any more. I could just use one and pass in the other variables so installDir became #{commonRoot}#{siteName}\WEB

Installdirvariable

Backing up web directory as zip file

Using my newfound knowledge of variables I was able to approach the Powershell code. The first job was to zip up the website directory as a backup. Using variables meant that I could have the path to the website folder as a variable which I could access from the Powershell file meaning I only had to write the Powershell logic once. It is easy to get variables from Octopus in the Powershell file, you use the following syntax $OctopusParameters["variableName"]. Here is the code I used to import the relevant variables into the PreDeploy.ps1 file:

# get octopus variables
$commonRoot = $OctopusParameters["commonRoot"]
$websiteFolder = $OctopusParameters["websiteFolder"]

Once I had the variables I used the following code to zip up the web directory before deploying:

# set paths for zipping
$rootDir = $commonRoot + $websiteFolder
$rootDir = $rootDir.Replace("\","\\")
$date = Get-Date -format "yyyyMMdd_HHmmss"
$webDirectory = $rootDir + "WEB"
$zipFile = $rootDir + "WEB_" + $date + ".zip"

# log to output
Write-Host "root dir " $rootDir
Write-Host "web-directory " $webDirectory
Write-Host "zip file " $zipFile

#zip up old web directory
Add-Type -AssemblyName System.IO.Compression.FileSystem
[Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem");
[System.IO.Compression.ZipFile]::CreateFromDirectory($webDirectory,$zipFile);

The Write-Host command is very useful for debugging the Powershell. Anything that is written to host will appear in the Octopus console as it is deploying:

Deployoutput

In this code the $webDirectory variable is the directory which I want to back up and the $zipFile variable is the zip file which will be created. I use the current date as part of the filename for the zip file, so it is easy to see when the zip was made. Then because we do not want our server full of old zip files if the Octopus Deploy is successful in the PostDeploy.ps1 file I run the following code to delete the oldest zip file.

# delete oldest zipped web folder
Get-ChildItem $rootDir -Filter *.zip | Sort CreationTime | Select -First 1 | remove-item

Set under construction page in IIS

The next job which I wanted to automate was to change IIS so that the current website points to an under construction page. In order to do this I created two new variables named siteName and underConstruction. Then I used the following code to change the IIS settings:

# Load IIS module:
[Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Administration")
Import-Module WebAdministration

# set website to under construction in iis
$serverManager = New-Object Microsoft.Web.Administration.ServerManager
$site = $serverManager.Sites | where { $_.Name -eq $siteName }
$rootApp = $site.Applications | where { $_.Path -eq "/" }
$rootVdir = $rootApp.VirtualDirectories | where { $_.Path -eq "/" }
$rootVdir.PhysicalPath = $underConstruction
$serverManager.CommitChanges()

# Get pool name by the site name:
$pool = (Get-Item "IIS:\Sites\$siteName"| Select-Object applicationPool).applicationPool
# Recycle the application pool:
Restart-WebAppPool $pool

 

This code is made up of two sections, the first changes the physical path of the site in IIS, the second gets the app pool and restarts it. The reason for the restart is make sure that the IIS change is effective and also to clear any locks which the app pool may have on files in the web directory which could block Octopus Deploy. Then in the PostDeploy.ps1 file the same code is run with reversed settings to reset the IIS to point to the website.

Summary

Using Octopus Deploy to run powershell scripts before and after deployment allows you to achieve anything. Then combining this with variables means the Powershell scripts can be tailored to a specific step, role or environment, increasing the usefulness of this feature. I wrote the powershell scripts myself. But since I did that I have discovered that there is a community library of scripts on the Octopus Deploy website which includes scripts to zip a directory many scripts for working with IIS and lots more. https://library.octopusdeploy.com/#!/listing

Complete Powershell code

Here is the final PreDeploy.ps1 code:

# get octopus variables
$commonRoot = $OctopusParameters["commonRoot"]
$websiteFolder = $OctopusParameters["websiteFolder"]
$siteName = $OctopusParameters["siteName"]
$underConstruction = $OctopusParameters["underConstruction"]

# Load IIS module:
[Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Administration")
Import-Module WebAdministration

# set website to under construction in iis
$serverManager = New-Object Microsoft.Web.Administration.ServerManager
$site = $serverManager.Sites | where { $_.Name -eq $siteName }
$rootApp = $site.Applications | where { $_.Path -eq "/" }
$rootVdir = $rootApp.VirtualDirectories | where { $_.Path -eq "/" }
$rootVdir.PhysicalPath = $underConstruction
$serverManager.CommitChanges()

# Get pool name by the site name:
$pool = (Get-Item "IIS:\Sites\$siteName"| Select-Object applicationPool).applicationPool
# Recycle the application pool:
Restart-WebAppPool $pool

# set paths for zipping
$rootDir = $commonRoot + $websiteFolder
$rootDir = $rootDir.Replace("\","\\")
$date = Get-Date -format "yyyyMMdd_HHmmss"
$webDirectory = $rootDir + "WEB"
$zipFile = $rootDir + "WEB_" + $date + ".zip"

# log to output
Write-Host "root dir " $rootDir
Write-Host "web-directory " $webDirectory
Write-Host "zip file " $zipFile

#zip up old web directory
Add-Type -AssemblyName System.IO.Compression.FileSystem
[Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem");
[System.IO.Compression.ZipFile]::CreateFromDirectory($webDirectory,$zipFile);

Here is the final PostDeploy.ps1 code:

$commonRoot = $OctopusParameters["commonRoot"]
$websiteFolder = $OctopusParameters["websiteFolder"]
$deployRoot = $OctopusParameters["deployRoot"]
$theme = $OctopusParameters["appSettings.themeName"]
$rootDir = $commonRoot + $websiteFolder
$newPath = $commonRoot + $websiteFolder + $deployRoot

# delete oldest zipped web folder
Get-ChildItem $rootDir -Filter *.zip | Sort CreationTime | Select -First 1 | remove-item

# reset iis to live site.
[Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Administration")
$siteName = $OctopusParameters["siteName"]

$serverManager = New-Object Microsoft.Web.Administration.ServerManager
$site = $serverManager.Sites | where { $_.Name -eq $siteName }
$rootApp = $site.Applications | where { $_.Path -eq "/" }
$rootVdir = $rootApp.VirtualDirectories | where { $_.Path -eq "/" }
$rootVdir.PhysicalPath = $newPath
$serverManager.CommitChanges()

 Next: databases

In the next blog I will explain how I used DbUp to deploy database changes as part of the Octopus Deploy strategy.


kommentieren


1 Kommentar(e):

Dean
Oktober 20, 2016 11:51

Great post. The tip on using Write-Host is very useful. Thanks.