22 February 2011 Azure, Deployment, Powershell, TeamCity Robert Muehsig

imageIn den letzten Blogposts bin ich schon auf den Einsatz von TeamCity als Buildserver eingegangen. Da wir bei dem BizzBingo Projekt auf Windows Azure setzen ging es nun darum, wie man automatisiert neue Deployments auf Azure vornehmen kann - insgesamt ist es doch einfacher als gedacht :)

"Visual Studio Deployment” == Fail!

Wer ein Windows Azure Projekt anlegt bekommt auch direkt im Visual Studio direkt die Möglichkeit ein Deployment durchzuführen:

image

Das ist für die ersten Entwicklungsschritte ganz nett und ich möchte dies auch nicht missen. Wer allerdings Software entwickelt und bei seinem Build- und Deploymentprozess nur auf seiner lokalen Maschine mit Visual Studio setzt sollte sich evtl. Gedanken machen ;)

Zielarchitektur

Es kann mehrere Entwickler geben (Dev A/B), welche auf einem Soure Control System, wie z.B. dem TFS oder SVN oder Git etc. arbeiten. Ein Buildsystem, in meinem Beispiel ist das TeamCity, schaut entweder in Intervallen nach oder wird manuell angestoßen und holt sich die aktuellen Sourcen ab und baut diese. Da BizzBingo Open Source ist, muss ich hier die sensiblen Daten allerdings auslagern. Nachdem das Projekt gebaut ist, wird dies auf die Staging Umgebung von Azure deployed. Wenn dies erfolgt ist, wird die Staging Umgebung zur Production Umgebung "geswapt” um den Betrieb möglichst nicht allzulange zu stören (möglichst wenig Downtime ist das Ziel).

image

Welchen Teil man automatisieren möchte, muss natürlich jeder selbst bestimmen - evtl. wird nur auf die Staging Umgebung deployed oder es wird immer direkt auf die Production deployed - jeder wie er mag. Zusätzlich könnten auf der Staging Umgebung auch noch Tests ausgeführt werden (Selenium, UI Tests etc.) und erst dann geschieht irgendwas. Aufgrund der "Hobbymentalität” bei BizzBingo (nichts anderes ist es ja ;) ) geh ich den einfachen Weg.

Grundvoraussetzungen

Ich setze ein installiertes TeamCity und ein Code Repository (TFS/SVN etc.) mal vorraus. TeamCity muss irgendwie in der Lage sein die Sourcen zu ziehen.  Der Einsatz von TeamCity ist natürlich freiwillig - man kann es auch lokal oder über ein anderes System (TFS Teambuild, klassisches MSBuild) aufrufen. Für Windows Azure sollte man Accountdaten besitzen - wichtigste Daten:

  • Servicename
  • Subscription Key

Windows Azure Management Cmdlets zum Automatisieren

Rund um das Management für Windows Azure stellt Microsoft eine REST Schnittstelle zur Verfügung. Allerdings wäre es etwas mühsam die API direkt dafür zu nutzen. Ebenfalls von Microsoft (und evtl. auch noch anderen Freiwilligen - das ganze ist Open Source) gibt es die Azure Management Cmdlets (Powershell!).

Folgende Software sollte installiert sein:

Installation Azure Management Cmdlets

Evtl. kann es bei der Installation der Probleme geben. Die Installationsroutine überprüft ganz genau die Azure SDK Version. Beim letzten Security Update hat sich die Versionsnummer erhöht. Der check kann aber in dieser Datei angepasst werden:

C:\WASMCmdlets\setup\scripts\dependencies\check\CheckAzureSDK.ps1

Dort befinden sich folgende zwei Zeilen mit der genauen SDK Version:

$res1 = SearchUninstall -SearchFor 'Windows Azure SDK*' -SearchVersion '1.3.20121.1237' -UninstallKey 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\';
$res2 = SearchUninstall -SearchFor 'Windows Azure SDK*' -SearchVersion '1.3.20121.1237' -UninstallKey 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\';

Durch jedes Security Update verändert sich die Versionsnummer - ich hab jedenfalls von Hand die Version "1.3.20121.1237” eingetragen. Das ist bei mir jedenfalls gut gegangen und der Installer lief erfolgreich durch.

Management Zertifikat

Ich zitiere mal David Aiken:

  1. Load the IIS 7 management console. I”™m assuming here you have IIS7 installed since its required for the Windows Azure SDK.
  2. Click on your Server.
  3. Double Click Server Certificates in the IIS Section in the main panel.
  4. Click Create Self-Signed Certificate... in the Actions panel.
  5. Give it a Friendly Name.
  6. Close IIS Manager.
  7. Open Certificate Manager (Start->Run->certmgr.msc)
  8. Open Trusted Root Certification Authorities, then Certificates.
  9. Look for your certificate (Tip: Look in the Friendly Name column).
  10. Right Click your certificate, then choose All Tasks, then Export...
  11. In the Wizard, choose No, do not export the private key, then choose the DER file format.
  12. Give your cert a name. (remember to call it something.cer).
  13. Navigate to the Windows Azure Portal - http://windows.azure.com

Eine andere Variante zum Erzeugen des Zertifikats "makecert”:

makecert -r -pe -a sha1 -n "CN=Windows Azure Authentication Certificate" -ss My -len 2048 -sp "Microsoft Enhanced RSA and AES Cryptographic Provider" -sy 24 testcert.cer

Das Tool befindet sich u.A. im Windows SDK: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin

Egal welche Variante man nimmt - von irgendwoher braucht man es :)

Das Zertifikat kann nun unter "Management Certificates” hinzugefügt werden:

image

TeamCity Build

Nun kommen wir zum Buildprozess. Der wichtigste Schritt ist hier der Aufruf des MSBuilds. Den Schritt den ich davor mache, hat im Prinzip nichts direkt mit dem Azure Deployment zutun.

image

Im Detail:

image

In diesem Schritt soll das MSBuild Script aufgerufen werden, welches hier liegt:

%system.teamcity.build.checkoutDir%\Main\Build\CommonBuildSteps.targets

Im TFS ist die Datei da hinterlegt:

image

Wichtig ist noch mitzugeben, was das Working Directory ist, damit ich mich im MSBuild "navigieren” kann.

Wichtig: MSBuild muss im x64 Modus sein, wenn der Server eine 64bit Maschine ist. Die Azure Management Cmdlets sind scheinbar nur im 64bit Modus installiert.

Oberes Fenster: x86 - Fehler
Unteres Fenster: x64 - Alles gut

image

Als Target ruf ich "PrepareAzureDeployment” auf und gebe als Properties mein Subscription Key und den Ort des Zertifikats mit. Das mache ich aber auch nur, weil BizzBingo Open Source ist und ich daher diese sensiblen Daten nicht einfach in das File speichern kann.

Die Parameter werden in dieser Form mitgegeben:

/p:AzureSubKey="83-SUB-KEY-9812" /p:AzureCertPath="cert:\LocalMachine\my\XXXXXXXXX"

"PrepareAzureDeployment” MSBuild Target

Hier sind im großen Teil Logausschriften mit dabei und eine kleiner "Dirty Hack”, weil ich keine sensiblen Daten in die Web.Release.config schreiben wollte ;)

Aktuell lesen wir bei BizzBingo die Web.config für den Datenbank Zugriff aus. Um den ConnectionString geheim zu halten, kopiere ich mir eine bestimmte web.release.config Datei und überschreibe die Variante welche aus dem TFS kommt. Dirty Hack, aber es passt bei mir ;) (würde natürlich fehlschlagen, wenn der Ordner nicht da ist).

Ich zeige erst den vollständigen Code, dann erkläre ich weiter:

  <Target Name="PrepareAzureDeployment">
    <Message Text="PrepareAzureDeployment called."/>
    
    <!-- For security reasons web.release.config will be stored outside the codeplex repository and
        I will override it here (of you run this local with the web.release.config file under source control:
        this might be fail) -->
    <Message Text="CertPath: $(AzureCertPath)" />
    <Message Text="Key: $(AzureSubKey)" />

    <Copy SourceFiles="C:\BizzBingoSecureContainer\Web.Release.config" DestinationFolder="$(MSBuildStartupDirectory)\..\Source\BusinessBingo\Source\BusinessBingo.Web\" />
    
    <MSBuild Projects="$(MSBuildStartupDirectory)\..\Source\BusinessBingo\Hosts\BusinessBingo.Hosts.Azure.Web\BusinessBingo.Hosts.Azure.Web.ccproj"
             Targets="CorePublish"
             Properties="Configuration=Release"/>

    <Message Text="Deploy To Staging on Azure" />
    
    <Exec Command="powershell .\Azure_DeployToStaging.ps1 -certPath '$(AzureCertPath)' -subKey '$(AzureSubKey)'" />
    <Message Text="Sleep for 400sec as a workaround" />
    <Sleep Seconds="400" />

    <Message Text="Swap To Production on Azure" />

    <Exec Command="powershell .\Azure_SwapToProduction.ps1 -certPath '$(AzureCertPath)' -subKey '$(AzureSubKey)'" />

  </Target>

Um das Azure Package zu bauen, rufe ich einfach in dem Azure Projekt das entsprechende MSBuild Target auf. Hier muss ich mich nun mittels "$(MSBuildStartupDirectory)” hinnavigieren (dafür auch das WorkingDirectory im TeamCity) und rufe "CorePublish” mit der entsprechenden Konfiguration auf:

    <MSBuild Projects="$(MSBuildStartupDirectory)\..\Source\BusinessBingo\Hosts\BusinessBingo.Hosts.Azure.Web\BusinessBingo.Hosts.Azure.Web.ccproj"
             Targets="CorePublish"
             Properties="Configuration=Release"/>

"BusinessBingo.Hosts.Azure.Web” ist ein ganz normales Azure Projekt, welches als WebRole die ASP.NET MVC App hat:

image

Durch den Aufruf von "CorePublish”, wird das BusinessBingo.Web Projekt gebaut, die Web.config Transformation ausgelöst und das Azure Package gebaut. Das Ergebnis liegt schlicht im "bin\Release\Publish” Ordner des Azure Projekts:

image

Deploy on Azure Staging

Nun ist im MSBuild der nächste Schritt den Aufruf des Powershell Scripts zum Deployen auf Azure. Als Parameter gebe ich noch SubscriptionKey und ServiceName mit:

    <Message Text="Deploy To Staging on Azure" />
    
    <Exec Command="powershell .\Azure_DeployToStaging.ps1 -certPath '$(AzureCertPath)' -subKey '$(AzureSubKey)'" />

Kleine Nebenbemerkung: Dadurch, dass MSBuild im x64 bit Modus aufgerufen wurde, wird nun auch die Powershell im x64 Modus aufgerufen. Ansonsten würden die Azure Cmdlets nicht gefunden werden!

Große Teile des Powershell Scripts stammen von der Windows Azure Guidance vom p&p Team von Microsoft. Genaue Beschreibung ist hier zu finden.

Azure_DeployToStaging.ps1

Nun ein genauer Blick auf das Powershell Script. Ich musste es etwas anpassen, damit die sensiblen Daten von aussen als Parameter reingegeben werden, aber ansonsten ist es eigentlich recht einfach zu verstehen (und man sieht wie sexy Powershell sein kann!).

Wichtiger Hinweis:

Häufige Fehlermeldung war bei mir "The HTTP request was forbidden with client authentication scheme 'Anonymous'.”. Das passiert, wenn der Subscription Key falsch ist.

Zum Zertifikat:

Wie weiter oben beschrieben muss das Zertifikat auch auf Azure hochgeladen sein.  So könnte die Angabe zum Zertifikat aussehen, ohne dass man es als Parameter mit reingibt:

$cert = Get-Item  cert:\CurrentUser\my\xxxxxxx

Den Zertifikatsstore kann man sich auf zwei wegen anschauen:

  • CertMgr.msc
  • Über die Powershell "cd cert:” eingeben

image

Der Zertifikatsstore kann ähnlich einem Dateisystem angeschaut werden. Wobei meine "xxxx” Angabe von oben mit dem Thumbprint ersetzt werden muss:

image

 Subscription Key

Den Key findet man im Windows Azure Portal bei den Hosted Services. Ich hab es selber nicht rausbekommen, ob der Key auch die Zahl 0 oder nur den Buchstaben 0 enthält - dummerweise kann man den Key nicht aus der Silverlight Oberfläche rauskopieren (danke RIA Technologie!). Evtl. findet man den Key auch in den Billinginformationen (das hässliche HTML - aber mit Copy/Paste Möglichkiet ;) )

ServiceName

Auch wenn der Eintrag "Servicename” heisst, ist hier der DNS Name gefragt:

image

Ich glaube, der Name ist auch case sensitive, also genau so abschreiben, wie er da steht :)

 Zum eigentlichen Script

Param($certPath, $subKey)
# Secrets
$cert = Get-Item $certPath
$sub = $subKey
$servicename = "bizzbingo"

# Paths
$buildPath = "..\Source\BusinessBingo\Hosts\BusinessBingo.Hosts.Azure.Web\bin\Release\Publish\"
$packagename = "BusinessBingo.Hosts.Azure.Web.cspkg"
$serviceconfig = "ServiceConfiguration.cscfg"
$package = join-path $buildPath $packageName
$config = join-path $buildPath $serviceconfig

# Date
$a = Get-Date
$buildLabel = $a.ToShortDateString() + "-" + $a.ToShortTimeString()

# Install PS-SnapIn
if ((Get-PSSnapin | ?{$_.Name -eq "AzureManagementToolsSnapIn"}) -eq $null)
{
  Add-PSSnapin AzureManagementToolsSnapIn
}

# Get Staging
$hostedService = Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | Get-Deployment -Slot Staging

# Delete Staging if nessarary
if ($hostedService.Status -ne $null)
{
    $hostedService |
      Set-DeploymentStatus 'Suspended' |
      Get-OperationStatus -WaitToComplete
    $hostedService | 
      Remove-Deployment | 
      Get-OperationStatus -WaitToComplete
}

# Deploy on Staging
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |
    New-Deployment Staging -package $package -configuration $config -label $buildLabel -serviceName $servicename | 
    Get-OperationStatus -WaitToComplete

# Start on Staging	
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | 
    Get-Deployment -Slot Staging | 
    Set-DeploymentStatus 'Running' | 
    Get-OperationStatus -WaitToComplete

Im MSBuild Schritt vorher habe ich ja das Paket gebaut und kann mich entsprechend dahin navigieren. Nun wird im Prinzip geschaut, ob bereits auf der Staging Umgebung etwas läuft, wenn ja wird es abgeschalten. Dann geht das Deployment los und die Instanzen werden angeschaltet. Der Zusatz "-WaitToComplete” soll dafür sorgen, dass jede Aktion auf die andere wartet. Jede Aktion dauert ja eine gewisse Zeit.

Dirty Workaround beim Starten der Instanz

Wenn ich in der Zeile 44 die Staging Umgebung starten will, dann wird das "-WaitToComplete” Event viel zu schnell abgearbeitet. Noch während der Initialisierung wird es als "fertig” ausgewiesen. Das ist natürlich ein Problem, wenn ich nun automatisiert von Staging auf Production umschalten will. Ob das so gedacht ist oder ob es ein Bug ist, weiß ich allerdings selber nicht. Ich bin aber nicht allein mit dem Problem :)

Hier nochmal die Deploymentschritte, welche im MSBuild Script geschehen:

    <Exec Command="powershell .\Azure_DeployToStaging.ps1 -certPath '$(AzureCertPath)' -subKey '$(AzureSubKey)'" />
    
	<Message Text="Sleep for 400sec as a workaround" />
    <Sleep Seconds="400" />

    <Message Text="Swap To Production on Azure" />
    <Exec Command="powershell .\Azure_SwapToProduction.ps1 -certPath '$(AzureCertPath)' -subKey '$(AzureSubKey)'" />

In Zeile 1 wird das gebaute Paket auf der Staging Umgebung deployed. Nun kommt der Workaround: Damit ich von "Staging” zu "Production” umschwenken kann, muss die Staging vollständig hochgefahren sein, weil ich ansonsten auf der "Production” eine hässliche Downtime habe. Daher warte ich 400 Sekunden -solange braucht man Azure Projekt zum Initialisieren und Starten. Der Task kommt von den MSBuild Community Task Projekt Nun wird die gestartete Staging Umgebung zur Production "geswapt” (tolles Unwort)

Azure Staging Swap to Production

Am Anfang wieder selbes Spiel: Key/Zertifikat holen und prüfen ob SnapIn da ist. Dann hole ich mir das Deployment auf der Staging und mache ein "Move-Deployment”, was wie im Webfrontend ein "Swap” macht.

Danach fahre ich die Staging Umgebung wieder runter und lösche die Staging Umgebung, weil die mich ja auch kostet.

Param($certPath, $subKey)
# Secrets
$cert = Get-Item $certPath
$sub = $subKey
$servicename = "bizzbingo"

# Install PS-SnapIn
if ((Get-PSSnapin | ?{$_.Name -eq "AzureManagementToolsSnapIn"}) -eq $null)
{
  Add-PSSnapin AzureManagementToolsSnapIn
}

# Switch staging <-> production
Get-Deployment staging -subscriptionId $sub -certificate $cert -serviceName $servicename | 
		Move-Deployment | 
		Get-OperationStatus -WaitToComplete

# Stop in staging
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |
    Get-Deployment -Slot 'Staging' |
    Set-DeploymentStatus 'Suspended' |
    Get-OperationStatus -WaitToComplete

# Remove from staging
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | 
    Get-Deployment -Slot 'Staging' | 
    Remove-Deployment | 
    Get-OperationStatus -WaitToComplete

Fertig

Der Prozess mag jetzt erst einmal komplex aussehen - ist er aber eigentlich mit den hier gezeigten Cmdlets nicht. Die größten Hürden bei mir waren die Probleme mit den Fehlermeldungen der Azure Cmdlets.

tl;dr

Um es kurz zu fassen:

  • SDK, Cmdlets Installieren
  • Windows Azure Projekt + Azure Konto erfolgreich anlegen
  • Management Key generieren und hochladen
  • Windows Azure Projekt über MSBuild mit "CorePublish” aufrufen
  • Gebautes Paket via Powershell auf Staging
  • Kurz abwarten - weil scheinbar buggy (?)
  • Von Staging auf Production umschwenken

Den Code gibt es auf Codeplex und die Liveseite ist unter www.BizzBingo.de erreichbar.


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!