Nov 28, 2015

Deployments and environments

powershell, psake, devops

Last time I spoke of deployment with Powershell I didn’t really get into the nitty gritty details of how to actually do some of this stuff so i just want to go a little deeper into how to actually accomplish this.

We have a number of different environments here - some for devs, other for QA and some for end users to see our progress before going live.

Each one of these environments has a mixture of ArcGIS, database and web servers and dependencies to various third party services that are specific to that environment (think Bing APIs and keys, etc.) To manage all of this I use an xml file per environment that describes these resources and the roles they play in the environment. You will see something similar to this in our dev.xml file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<environmentSettings>
    <servers>
        <server name="app01" roles="dbproxy,api" />
        <server name="app02" roles="api" />
        <server name="app03" roles="mapservices" />
        <server name="app04" roles="web" />
        <server name="app05" roles="web" />

        <!-- Virtual node for stuff that needs to run once at the end -->
        <server name="virtual" roles="virtual" />
    </servers>
</environmentSettings>

You can see there are 5 nodes that are assigned different roles. When the deployment script runs it will loop trough these nodes and run the deployment package with the correct roles. This push model works well for us for the time being as it allows for very granular control over the process with no waiting and guessing what state each node is in. We haven’t really got any tolerance for that yet, it’s something you kinda work you way up to.

The final node is virtual and it covers tasks that need to be run but not against any particular node. I have been planing to add one to the start as well and move the dbproxy to a virtual node since don’t acutely run this on the db server - hence the proxy in the dbproxy name.

The Powershell script that does the heavy lifting is the deploy script but to bootstrap that we use something like below. I’ve omitted some parts that are not super interesting for brevity’s sake.

param (
    [ValidateSet("local","dev","test","stg","qual","prj", "prod")]
    [string] $environment   = "dev",
    [string] $task          = "default",
    [string] $target_server = $null,
    [ValidateSet("all","start","end")]
    [string] $sendEmails    = "all",
    [bool] $debug           = $false,
    [string] $user,
    [switch] $docs
)

$buildId                  = 0
$currentDir               = resolve-path .
$environmentsDir          = "$currentDir\environments"
[xml]$environmentSettings = Get-Content "$environmentsDir\$environment.xml"

# Import tools
Import-Module "$currentDir\packages\poshBAR.*\tools\modules\poshBAR" -force
Import-Module "$currentDir\packages\psake.*\tools\psake.psm1" -Force

# This just asserts that the current session has elevated privileges
Test-RunAsAdmin

$psake.use_exit_on_error = $true

# Just list tasks if docs is requested
if($docs) {
    Invoke-PSake -docs ".\deploy.ps1"
    return
}

# Include our helper scripts, they live in the tools folder ###
. .\tools\Emailer.ps1
. .\tools\Utils.ps1
. .\tools\TeamCity-Tools.ps1

if(Test-Path "$currentDir\changelog.txt") {
    $changes = Get-Content "$currentDir\changelog.txt"
}

if(Test-Path "$currentDir\buildmeta.txt") {
    $buildId = Get-Content "$currentDir\buildmeta.txt"
}

if(($sendEmails -eq "all") -or ($sendEmails -eq "start")) {
    Send-Email -environ $environment -state "Start" -changes $changes -user $user -task $task
}

# ... omitted a few things for brevity's sake

$serverNodes = $environmentSettings.selectNodes("//servers/server")
$success = $true

foreach($node in $serverNodes) {

    $server = $node.name
    Write-Output "Starting deployment to $server"

    if(($target_server -eq $server) -or ([string]::isNullOrEmpty($target_server))) {

        Invoke-PSake ".\deploy.ps1" $task -parameters @{
            environment          = $environment;
            targetServer         = $server;
            debug                = $debug;
            securedbpassword     = $securedbpassword;
            securegdidbpassword  = $securegdidbpassword;
            securecpdbpassword   = $securecpdbpassword;
            securearcgispassword = $securearcgispassword;
            securenaspassword    = $securenaspassword;
        }

        Write-Output "Finished deployment to $server."

        if ($psake.build_success -eq $false) {
            $success = $false
            break
        }
    }
}