Continuous Integration mit Visual Studio Online und Microsoft Azure

7. Oktober 2015

Zur professionellen Software-Entwicklung gehört die Einrichtung eines automatischen Build-, Test- und Deployment-Systems. Microsoft stellt mit seinem neuen Build-System ein auf den ersten Blick leicht zu bedienendes Werkzeug zur Verfügung. Setzt man es praktisch ein, stößt man jedoch schnell an die Grenzen von Intuition und Dokumentation. Dieser Artikel zeigt exemplarisch an einem realen Projekt die Einrichtung eines kompletten Continuous Integration-Prozesses (CI) und schildert die gewonnenen Erfahrungen.

Im vorliegenden Projekt wird der Code in Visual Studio Online (VSO) gehostet und die Anwendung auf Microsoft Azure deployt. Die Anwendung besteht im Wesentlichen aus einem Web Service (Web API), der in Azure als API App deployt wird.

Während man früher mit MSBuild- und PowerShell-Skripten hantieren musste, um ein leistungsfähiges CI-System aufzubauen, bringt das neue Build-System ein Web User Interface mit, mit dem sich die verschiedenen Prozessschritte visuell zusammenbauen lassen. Ein fertiger Prozess stellt sich dann so dar:

BuildDefinition

Wie baut man nun das Ganze auf? Zunächst einmal sind Voraussetzungen zu erfüllen.

Vorbereitung

So benötigt man mindestens einen Build Agent, der den Code baut. Ist der gesamte Code in VSO gehostet, kann man auf einen Hosted Pool von Build Agents zurückgreifen, muss also keinen eigenen Build Agent bereitstellen. Dabei gelten zwar ein paar Einschränkungen, die aber für das vorliegende Beispielprojekt nicht von Bedeutung waren. Unter dem obigen Link findet man unter anderem die Liste der Software, die auf einem Hosted Build Agent installiert ist. Braucht man mehr, muss man einen eigenen Build Agent aufsetzen (on-premise).

Um den Hosted Pool anzusprechen, ist beim Ausführen eines Build nur eine Option zu setzen. Ich komme darauf später zurück (siehe Globale Parameter).

Um auf Microsoft Azure deployen zu können, muss das Build-System Kenntnis von der entsprechenden Azure Subscription haben, auf die deployt werden soll. Das erreicht man mit folgenden Schritten:

  1. Öffnen der Projektseite im VSO, z. B. https://{VSO Account}.visualstudio.com/DefaultCollection/{Team Project}
  2. Navigieren in die Account Administration (kleines Zahnrad oben rechts)
  3. Wechseln auf den Tab Services

Hier kann man über New Service Endpoint > Azure eine Azure Subscription hinzufügen:

DeploymentServiceEndpoint

Wie dieser Dialog auszufüllen ist, kann man unter “Register your Azure subscription” nachlesen.

Somit steht die Subscription für die Erstellung einer Build-Definition und folglich als Deployment-Ziel zur Verfügung.

Der Build-Prozess

Jetzt geht es an das Aufbauen des Build-Prozesses, welcher die folgenden Schritte enthalten wird:

  1. PowerShell: Ausführen eines PowerShell-Skripts, um pre-build-Aktionen durchzuführen (hier ein Backup der aktuellen OData-Metadaten)
  2. Visual Studio Build: Bauen der Solution
  3. Visual Studio Test: Ausführen der Unit Tests
  4. Publish Build Artifacts: Kopieren des Build Output vom Build Agent in einen Drop Folder im VSO
  5. Azure Web App Deployment: Deployment der Applikation als Microsoft Azure API App

Doch zunächst die allgemeine Konfiguration des Build.

Globale Parameter

Repository Tab: Hier stellt man das Source Code Repository ein, aus dem die zu bauenden Sourcen heruntergeladen werden sollen.

Repository

Der Repository Name entspricht im Falle von Team Foundation Version Control dem Namen des Team Project.

Unter Mappings wird festgelegt, welche Repository-Ordner beim Herunterladen des Codes eingeschlossen (Map) und welche ausgeschlossen (Cloak) werden. Meistens wird man als Map den Solution-Ordner einstellen. Es werden damit zu Beginn des Build-Prozesses alle Dateien und Ordner unterhalb des “geMapten” Ordners heruntergeladen, außer den als Cloak definierten Ordnern.

Variables Tab: Hier kann man Variablen vordefinieren und an allen Stellen innerhalb der verschiedenen Build Steps wiederverwenden. Pro Variable kann auch festgelegt werden, ob diese für einen einzelnen Build-Lauf überschrieben werden kann (Allow at queue time).

Variables

Die hier verwendeten Variablen haben folgende Bedeutungen:

  • BuildConfiguration entspricht der im Visual Studio verwendeten Build-Konfiguration, also im Regelfall Release. Etwaige Compiler Conditions oder auch web.config-Transformationen beziehen sich auf diese Einstellung.
  • BuildPlatform entspricht der im Visual Studio verwendeten Build Platform.
  • WebAppName ist der Name der Azure App (hier API App). Achtung, das ist nicht der Anzeige-Name der API App, sondern der Name des API App Host, wie in der folgenden Abbildung zu sehen.

WebAppName

Triggers Tab: Hier kann eingestellt werden, wann und unter welcher Bedingung der Build automatisch ausgeführt wird. Im vorliegenden Fall wird der Build immer dann ausgeführt, wenn in den angegebenen Respository-Pfad (Filters) etwas eingecheckt wurde. Die Option Batch changes besagt, dass nicht bei jedem einzelnen Check-in ein Build gestartet wird, sondern nur dann, wenn nicht schon ein Build derselben Definition läuft. Dann fließt also nicht genau ein Check-in in den Build ein, sondern alle Check-ins, die noch nicht Teil des vorangegangenen Builds waren (Batch).

Triggers

General Tab: Das wichtigste hier ist die Default queue. Möchte man auf den Build Agent Pool zurückgreifen und hat keinen eigenen Build Agent aufgesetzt, wählt man hier Hosted. Der Wert ist ein Default-Wert und kann beim manuellen Starten eines Build übersteuert werden.

Damit haben wir die generellen Einstellungen für die Build-Definition und können mit der Konfiguration der einzelnen Build Steps fortfahren.

Build Steps

PowerShell

Möchte man im Verlaufe des Build-Prozesses ein allgemeines PowerShell Script ausführen, kann man diesen Build Step einbauen.

PowerShell

Der Script filename ist der Pfad des PowerShell Script im Source Code Repository.

Die Arguments entsprechen den Parametern, die das Script erwartet.

Die Control Options bieten noch die Möglichkeiten, einen Build Step zu disablen, den Build-Prozess nicht zu stoppen, falls dieser Step auf einen Fehler läuft (Continue on Error) oder einen Build Step immer auszuführen, d. h. auch dann, wenn die vorhergehenden Steps auf Fehler liefen (Always Run). Diese Optionen können für jeden Build Step-Typen eingestellt werden.

Visual Studio Build

Dies ist der eigentliche Build Step, der den Source Code kompiliert und den Build Output erzeugt.

VisualStudioBuild

Die Solution ist der Pfad im Source Code Repository zu dem Solution File, das gebaut werden soll.

Die Configuration ist die Build-Konfiguration. Im Beispiel ist die Verwendung der entsprechenden Variable zu sehen, wie sie im Variables Tab definiert ist.

Visual Studio Test

Dieser Step führt sämtliche Unit Tests durch.

VisualStudioTest

Test Assembly enthält eine Pfad-Definition mit Wildcards, um die auszuführenden Test Assemblies zu definieren. Im vorliegenden Fall werden in allen Ordnern mit Namen der Build-Konfiguration die Assemblies mit “test” im Namen hergenommen, wobei alle in den Ordnern “obj” ausgeschlossen werden sollen. Der Pfad ist relativ zum Solution-Ordner zu sehen.

Publish Build Artifacts

Dieser Step transportiert den Build Output vom Build Agent in einen Ordner im Visual Studio Online. Somit werden die Build-Ergebnisse aufgehoben und stehen so lange zur Verfügung, wie der Build Record an sich zur Verfügung steht (siehe auch Retention Tab in der Build-Definition).

PublishArtifacts

Der Copy Root setzt sich wie folgt zusammen:
$(Agent.BuildDirectory)/{Repository-Name}/{Projektordner}, wobei {Projektordner} der Ordner des gebauten Web App-Projekts ist.

Artifact Name ist der Name des Drop Folder, in den die Build Outputs kopiert werden sollen. Wichtig: Aus dem Drop Folder werden die Dateien für das Deployment auf Azure genommen (folgender Build Step).

Der Drop Folder ist, nachdem der Build mal gelaufen ist, in den Build-Ergebnissen im Artifacts Tab abrufbar:

DropFolder

Bleibt noch das eigentliche Deployment der App nach Microsoft Azure.

Azure Web App Deployment

In diesem Schritt wird die Anwendung in Azure als API App bereitgestellt.

WebAppDeployment

Als Azure Subscription ist die Subscription zu wählen, in die deployt werden soll. Hier stehen die Subscriptions zur Auswahl, die bei der Vorbereitung (s. o.) dem Team Project zugeordnet worden sind.

Der Web App Name entspricht dem Namen des API App Host (s. o.) und wird als Variable referenziert.

Die Web App Location entspricht der Location der API App, wie sie in Azure angelegt ist:

Location

Das Web Deploy Package verdient besonderes Augenmerk. Es ist der Pfad auf das zu deployende Web App Package. Der Pfad setzt sich hier wie folgt zusammen:
$(Agent.BuildDirectory)/artifacts/{Artifact Name}/obj/$(BuildConfiguration)/Package/{Projektname}.zip
Nun wird dieses ZIP File nicht standardmäßig erzeugt. Dafür braucht es etwas Handarbeit in der Projektdatei (.csproj). Hier sind 4 Konfigurationseinträge einzufügen innerhalb der Property Group für die Release Build-Konfiguration.

<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props')" />
...
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
...
<FilesToIncludeForPublish>OnlyFilesToRunTheApp</FilesToIncludeForPublish>
<DeployOnBuild>true</DeployOnBuild>
<DeployTarget>Package</DeployTarget>
<PackageAsSingleFile>true</PackageAsSingleFile>
</PropertyGroup>
...
</Project>

Damit wird für das Web App-Projekt beim Release Build ein Package erzeugt und im Ordner {Projektordner}objReleasePackage mit dem Namen {Projektname}.zip abgelegt. Dieses Package wird beim Azure Deployment hochgeladen.

Nachdem also schlussendlich die modifizierte Projektdatei im Source Code Repository aktualisiert wurde, kann der Build Prozess ausgeführt werden.

Versionierung

Build-Definitionen werden automatisch durch das Build-System versioniert. Bei jedem Speichern der Definition wird nach einem Kommentar für die Änderungen gefragt. Daraus entsteht eine Historie der Änderungen, die auf dem History Tab abgerufen werden kann. Dort können auch Vergleiche von zwei Versionen angestellt werden. Führt man einen solchen Vergleich durch, erhält man einen Blick auf das Definitionsformat (JSON).

Fazit

Das neue Build-System bietet einen bequemen Weg, eine CI-Umgebung aufzubauen, ohne sich durch MSBuild-Skripte oder XAML-Build-Skripte zu wühlen. Allerdings ist die Dokumentation, die man dazu im Netz der Netze findet, alles andere als vollständig. Insbesondere die einzelnen Build Step-Typen sind nur spärlich dokumentiert. Gerade was diverse Pfadangaben in den Build Steps betrifft, gilt mehr als einem lieb ist das Prinzip Trial&Error. Aber vielleicht kann dieser Artikel auch ein wenig dazu beitragen, dass der Einstieg in die Thematik leichter fällt.