AUTOMATING PRIVATE BUILDS WITH GITHUB, JENKINS, PSAKE AND MS WEB DEPLOY

I have written some posts before on similar topics. In the latest post about this topic I wrote: “To get a more reliable solution we could deploy using web deploy”. In this post I will go through how we at Cloud Connected has setup our build server with GitHub, Jenkins, Psake and MS Web Deploy.

This setup is primary for building and deploying ASP.NET web application like EPiServer CMS but we could use it to build and deploy other projects as well like for example PHP, NodeJs and Java projects.

Prerequisites

At Cloud Connected we host almost all of our source code at GitHub. Our build server is a Windows Server 2008 R2 with
Jenkins 1.501, MS Web deploy 3, GIT 1.8.1.2 for windows, and .NET 4.5 installed on it. The destination webserver for this post is a Windows Server 2012 with NET 4.5 and MS Web Deploy installed on it.

Private repositories at GitHub

Most of our clients does not want us to host their projects as open source projects. So we have created private repositories for them. Private GitHub repositories requires our build server to authenticate with GitHub to be able to pull the source code. We do this with local windows accounts and private SSH keys like this:

  • We have a local windows user called “Jenkins” on our build server
  • Our windows service that is hosting Jenkins is running as the “Jenkins” user:

SSH keys

To authenticate we use SSH keys and then we pull the source code over SSH. We generated the keys with GIT bash (with a empty passphrase) exactly like GitHub describes it here. We created the keys in the context of the Jenkins user, we were actually logged in as the Jenkins user during the key generation to make sure the keys would relate to the Jenkins user.

Cloud Connected is an organization at GitHub and organizations can not have a single ssh key added globally for all repositories (as I understand?). For simplicity we had the build server authenticate through a member of the Cloud Connected organization, we just added the SSH key to my account like this:

If the setup is correct you should have the SSH keys generated at “C:\Users\Jenkins.ssh” like this:

And you should be able to authenticate with GitHub through GIT bash using:

Deploying using MS Web Deploy

As I said in my previous post deploying using web deploy is much more stable that deploying using something like file shares or ftp and the build script is simpler!

To pull the source from GitHub we use this plugin (version 1.5). I can not show you the exact configuration because it contains some secret information but to configure the GitHub account information is no hassle. The set up is almost identical to the configuration from my previous post with the difference of using SSH instead of HTTPS.

We are not using any deployment hooks. We have just scheduled Jenkins to fetch the source and do the deploy after a specified interval.

After Jenkins has pulled the source Jenkins executes a PowerShell script that does the actual deploy (the script is pushed to the GitHub repository so Jenkis can find it) like this:

the build.ps1 looks like this (this script is mostly unique per project we build):

param(  
    [alias("env")]
    $Environment = 'debug'
)

function Build() {  
    if($Environment -ieq 'debug') {
        Write-Host "sorry, script does not support debug build..."
    }
    if($Environment -ieq 'test') {
        .\ProjectName.Build\psake.ps1 ".\ProjectName.Build\Deploy.ps1" -properties @{ config='test'; environment="$Environment";} "test"
    }
    Write-Host "$Environment build done!"
    if ($psake.build_success -eq $false) {
        exit 1
    } else {
        exit 0
    }
}

Build  
  1. The script takes a single parameter that specifies the environment to build and deploy for.
  2. The script checks which environment to build for and then calls psake.ps1 with correct build script and parameters for the specified environment.

Psake.ps1 is a wrapper around the Psake project to be able to execute Psake without being forced to install Psake as a PowerShell module. Psake.ps1 can be found in the same repository as Psake.

We have placed Psake.ps1 in same folder as psake.psm1 together with our deploy script (for simplicity) like this:

Now the fun part, Deploy.ps1 looks like this (this script is mostly unique per project we build):

properties {  
    $dateLabel = ([DateTime]::Now.ToString("yyyy-MM-dd_HH-mm-ss"))
    $baseDir = resolve-path .\..\
    $testProjectDir = "$baseDir\ProjectName.Tests"
    $testRunnerExe = "$baseDir\packages\NUnit.Runners.2.6.2\tools\nunit-console.exe"
    $webAppProjectDir = "$baseDir\ProjectName.Web"
    $deployDir = "$baseDir\ProjectName.Deploy"
    $deployPackagePathAndFileName = "$deployDir\ProjectName-$config-$dateLabel.zip"
    $webDeployExePath = "C:\Program Files\IIS\Microsoft Web Deploy V3\"
    $config = 'debug'
    $environment = 'debug'
}

task test -depends deploy

task compileUnitTests {  
    $csprojFile = "$testProjectDir\ProjectName.Tests.csproj"
    & msbuild $csprojFile /t:Clean /t:Build /p:Configuration="debug" /v:q
    if($LASTEXITCODE -ne 0) {
        throw "Failed to compile unit test assembly"
        exit 1
    }
}

task runUnitTests -depends compileUnitTests {  
    & $testRunnerExe "$testProjectDir\bin\debug\ProjectName.Tests.dll"
    if($LASTEXITCODE -ne 0) {
        throw "Failed to run unit tests"
        exit 1
    }
}

task createDeployPackage -depends runUnitTests {  
    Remove-ThenAddFolder($deployDir)
    $csprojFile = "$webAppProjectDir\ProjectName.Web.csproj"
    & msbuild $csprojFile /t:Package /p:Platform=AnyCPU /p:Configuration=$config /p:PackageLocation=$deployPackagePathAndFileName /v:q
    if($LASTEXITCODE -ne 0) {
        throw "Failed to create deploy package"
        exit 1
    }
}

task deploy -depends createDeployPackage {  
    $msdeploy = "$webDeployExePath\msdeploy.exe"
    $arg1 = "-verb:sync"
    $arg2 = "-source:package=$deployPackagePathAndFileName"
    $arg3 = "-dest:auto,ComputerName='http://10.100.100.101/MsDeployAgentService',username='WEBSERVERNAME\WebDeploy',password='password'"
    $arg4 = "-retryAttempts=2"
    & $msdeploy $arg1 $arg2 $arg3 $arg4
    if($LASTEXITCODE -ne 0) {
        throw "Failed to deploy to $config"
        exit 1
    }
}

#helper methods
function Remove-ThenAddFolder([string]$name) {  
    Remove-IfExists $name
    New-Item -Path $name -ItemType "directory"
}

function Remove-IfExists([string]$name) {  
    if ((Test-Path -path $name)) {
        dir $name -recurse | where {!@(dir -force $_.fullname)} | rm
        Remove-Item $name -Recurse
    }
}

The script contains four tasks that is executed in this order:

  • compileUnitTests
    • Compiles the projects unit test assembly, breaks the build if the assembly can not compile
  • runUnitTests
    • Runs the unit test in the assembly using nunit’s console runner. Breaks the build of not all tests passes.
  • createDeployPackage
    • Creates a empty folder then uses msbuild to build the web application and create a deployment package in the empty folder. Breaks the build if the package could not be created.
  • deploy
    • Uses MS Web Deploy to deploy the package to our test server. Breaks the build if the package can not be deployed to the webserver.

Nuget package restore

We use the awesome package restore feature of Nuget. We just have our package config files pushed to github and lets our build server fetch required packages before building. The only issues we had was the fact that we were required to add the environment variable EnableNuGetPackageRestore with the value true.

MS Web Deploy 3.0 issues

Authentication

This were actually the hardest thing to configure in this setup which is pretty stupid because you could think that this should be supported out of the box. Maybe I am missing something (almost hope so) otherwise this needs to be simpler than this:

The test webserver has a local windows user account named “webdeploy” when executing ms webdeploy we set the credentials for the webdeploy user. The webdeploy user must be an local administrator (annoys me) else webdeploy will shout out that the user must be have “administrative privileges” when trying to deploy.

This is were stuff is getting funky… If you do as web deploy sais and uses a user that has administrative privileges you will get the error “(401) Unauthorized” back from the destination server, say whaaat?

It turns out that remote login is not enabled by default. To enable it we must add a registry entry :O.

After adding the entry LocalAccountTokenFilterPolicy with DWORD Vaule : 1 under HKEYLOCALMACHINE\SOFTWARE\ Microsoft\Windows\CurrentVersion\Policies\System at the test webserver the webdeploy user can login through msdeploy.

Paths

We were struggling for some time to get msbuild to accept a path with spaces for the -source:package parameter. We never succeeded with this I think it is a bug in msdeploy (or I missed something, please let me know). However we changed Jenkins default workspace folder to a path that did not include any spaces, problem solved.

Security

All deploys to test servers are done on a internal network over VPN’s. No deploys are done on the “public internet” and all endpoint for the ms web deploy services are blocked through firewalls.

Conclusion

The setup is up and running and deploying to our test environment every night. MSDeploy is really stable and deploys the packages without any troubles It was not to hard to set up besides that registry and file path crap. Took me about 8 hours with coffee breaks :). But I have experience with Jenkins, Psake, PowerShell, Git and MsDeploy since before.

I hope this is a setup you would consider for your deployment servers as well, please let me know if you have ideas for improvements our tips from your ci setups.

comments powered by Disqus