Angular Anwendungen in Azure DevOps bauen
Azure DevOps ist der Nachfolger des Microsoft Team Foundation Server (TFS).
Inbegriffen sind Buildserver (CI), Version Control (git), Verwaltung manueller Testpläne und Artefaktauslieferung (CD).
Die Features von Azure DevOps können dabei je nach Bedarf aktiviert werden.
Im folgenden soll eine Angular Webanwendung mit Azure DevOps als Buildserver gebaut werden. Damit der Build gemeinsam mit der Software versioniert und entwickelt werden kann, wird die Buildkonfiguration im YAML Format als Azure Pipeline im Repository abgelegt.
Azure DevOps Setup
Wichtig zu wissen ist dabei, dass Azure DevOps das YAML Pipeline-Format aktuell lediglich für Microsoft-eigene git Repositories bei GitHub oder "Azure Repos" Git unterstützt.
Mehr Informationen dazu finden sich im Microsoft Azure DevOps Support Dokument: https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/?view=azure-devops
Für das Beispiel wird daher das Repos Feature im Azure DevOps aktiviert und als Versionsverwaltung genutzt.
Das Setup eines Azure DevOps Projekts für einen Angular Build mit Azure DevOps sieht dann wie folgt aus:
Als erstes wird ein neues Azure DevOps Projekt erzeugt.
Falls GitHub verwendet wird, ist das Setup besonders einfacher. Für die von Microsoft in Azure DevOps angebotene Git-Integration wird zunächst ein leeres Projekt angelegt. In den Projekteinstellungen kann dann das Repos Feature aktiviert werden.
Anschließend kann eine neue Build Pipeline erzeugt werden, Als Quelle für den Sourcecode wird dann "Azure Repos git" (oder eben GitHub) ausgewählt.
Azure DevOps YAML Pipeline
Azure DevOps verbindet sich dann mit der Versionsverwaltung und sucht nach der Datei azure-pipelines.yaml
.
In der Pipeline können dann Schritte (steps
) definiert werden, die für den Build abgearbeitet werden.
Die Pipeline wird durch sogenannte Agents ausgeführt. Microsoft bietet die Plattformen Linux, Windows und macOS an. Die Konfiguration der Plattform erfolgt durch einen Pool, der für die gesamte Pipeline oder einen Unterabschnitt gilt.
pool:
vmImage: 'Ubuntu-20.04'
Der eigentliche Build wird durch Scripte oder vordefinierte Tasks abgebildet. Verwendet man stets Scripte, hat dies den Vorteil, dass diese sich leichter auf andere Umgebungen übertragen lassen. Damit lassen sich die Buildabschnitte auch lokal auf einem Entwicklerrechner gut testen, und man benötigt nicht stets die Azure Umgebung.
- script: npm ci
displayName: Install NPM dependencies
- script: ng test --watch false
displayName: Component tests
- script: ng build -c production
displayName: Build app
Auf den Agents lässt sich für den Build Software installieren, wie zum Beispiel Angular CLI oder andere Werkzeuge. Da Azure DevOps auch die Ausführung von (Docker-) Containern erlaubt, bietet sich dies Verfahren ebenfalls an, um vorgefertigte Werkzeuge oder Service-Container bereitzustellen.
Azure DevOps Docker Container
Docker Container können prinzipiell in Azure DevOps Pipelines ausgeführt werden, es gilt jedoch einige Besonderheiten zu beachten:
Azure Pipelines wollen einen eigenen User im Container anlegen und dazu das Werkzeug useradd
verwenden.
Daher muss das Image entsprechend sudo
Rechte bereitstellen, useradd
so ausführbar machen, dass es von normalen Nutzern ausgeführt wird, oder der im Container verwendete User muss entsprechend privilegiert sein.
Die einfachste Lösung ist, den Container mit der Option --user 0:0
auszuführen, und so im Container entsprechend root-Rechte zu haben.
Außerdem muss im Container in jedem Fall NodeJS installiert sein, da die Azure Pipelines damit im Container Befehle ausführen.
Zu verwendende Container müssen zudem im Abschnitt resources
deklariert werden, damit sie später durch container
referenziert werden können.
Für den Build von Angular Anwendungen bieten sich die trion/ng-cli-*
Images an.
Nicht nur ist damit eine versionierte Node-Version und Angular CLI Version verfügbar.
Es befindet sich im trion/ng-cli-karma
Image ein vorkonfigurierter Chrome, der auch als nicht-headless gestartet werden kann, um z.B. mit Karma Komponententests auszuführen.
resources:
containers:
- container: trion-ng-cli
image: trion/ng-cli:latest
options: --user 0:0
- container: trion-ng-cli-karma
image: trion/ng-cli-karma:latest
options: --user 0:0
- container: trion-ng-cli-e2e
image: trion/ng-cli-e2e:latest
options: --user 0:0
Die so deklarierten Container können dann an einer späteren Stelle auf Job-Ebene verwendet werden.
Alle steps
werden dann in dem Kontext des Containers ausgeführt.
- job: install
container: trion-ng-cli
displayName: NPM-Install
steps:
- script: npm ci
Build Inhalte in mehreren Stages
Eine Azure DevOps Pipeline strukturiert sich in folgende Einheiten:
-
Stages; können unabhängig und parallel ausgeführt werden
-
Jobs; gehören zu einer Stage, können unabhängig und parallel ausgeführt werden und teilen sich unter speziellen Umständen ein Verzeichnis
-
Steps; gehören zu einem Job und werden sequentiell ausgeführt
Das Design hat zum Ziel, eine möglichst hohe Parallelisierung, und damit einen schnellen Build erzielen zu können. Daraus ist schnell zu erkennen, dass keine impliziten Abhängigkeiten zwischen den Jobs oder Stages bestehen dürfen, da dies die Buildstabilität bzw. Korrektheit beeinträchtigen kann.
Explizite Abhängigkeiten können auf Job- und Stage-Ebene durch dependsOn
ausgedrückt werden.
Werden Daten aus einer vorherigen Stage oder einem vorherigen Job benötigt, so müssen diese separat bereitgestellt werden.
In dem Sonderfall, dass es lediglich einen Buildagent gibt, können Jobs auf die Daten vorheriger Jobs zugreifen.
Der Zugriff auf Daten aus separaten Stages ist jedoch nicht ohne weitere Aktivitäten möglich.
Für NodeJS basierte Anwendungen werden typischerweise mit npm
oder yarn
Abhängigkeiten geladen.
Diese sollen natürlich nicht in jedem Job oder jeder Stage komplett neu geladen werden.
Dazu gibt es in Azure DevOps Pipelines den Cache
Task:
Daten lassen sich damit zwischenspeichern und in folgenden Jobs oder Stages wieder heranziehen.
Das kann sogar übergreifend für mehrere Pipelinedurchläufe genutzt werden.
Um festzustellen, ob der Cache noch verwendet werden kann, wird ein Cache-Key verwendet.
Dieser kann aus einem oder mehreren Strings oder Inhalten von Dateien bestimmt werden.
Bei NPM bietet sich natürlich der Inhalt der package-lock.json
an - wenn sich an den Abhängigkeiten nichts ändert, müssen diese auch nicht neu bezogen werden.
Wurde ermittelt, dass der Cache verwendet werden kann, wird der Inhalt wieder hergestellt und es kann eine Variable gesetzt werden, die den Positivfall anzeigt.
Die Variable kann dann in weiteren Schritten genutzt werden, um zu entscheiden, ob ein Schritt - hier der erneute Abruf der NPM Libraries - ausgeführt werden soll, oder nicht.
Dazu wird eine condition
verwendet, die den Wert der Variablen auswertet:
Im folgenden Beispiel steht das ne
für not equals
und prüft somit, ob der NPM Cache bereitgestellt wurde.
- task: Cache@2
displayName: Cache NPM modules
inputs:
key: 'npm | package-lock.json'
path: '$(Build.SourcesDirectory)/node_modules'
cacheHitVar: NPM_CACHE_RESTORED
- script: npm ci
displayName: NPM CI
condition: ne(variables.NPM_CACHE_RESTORED, 'true')
Wichtig ist, dass der Cache Schritt bei allen folgenden Stages - ganz korrekt: bei jedem Job - erneut ausgeführt wird.
Leider hat Microsoft die YAML Unterstützung unvollständig implementiert:
Es lassen sich keine Referenzen definieren oder verwenden.
(YAML 1.0 Spezifikation: https://yaml.org/spec/1.0/#id2489959)
Damit lassen sich Wiederholungen leider nicht so elegant ausdrücken.
Alternativ zum Cache kann eine Pipeline auch Artifacts bereitstellen. Dabei handelt es sich um Ergebnisse oder Zwischenergebnisse aus dem Build, die dann an anderer Stelle genutzt werden können. Die Artefakte können auch außerhalb der Pipeline verwendet werden: Sie können über die Weboberfläche bequem heruntergeladen werden oder als Basis dienen, um daraus Folgeartefakte zu bauen.
- task: PublishBuildArtifacts@1
displayName: 'Publish dist'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/dist.tgz'
ArtifactName: 'dist'
Buildartefakte haben einen Namen, der gleichzeitig der Ordnername ist, unter dem das Artefakt bereitgestellt wird. Darauf gilt es zu achten, wenn ein Artefakt in einem Folgejob heruntergeladen wird: Der Ordnername bleibt dabei erhalten und muß bei Zugriffen beachtet werden.
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'dist'
downloadPath: '$(Build.ArtifactStagingDirectory)'
Azure DevOps Angular Beispielprojekt
Das vollständige Beispielprojekt als Angular Anwendung mit Linting (eslint), Komponententests (Karma), e2e Tests (Cypress) und natürlich Build der Anwendung befindet sich hier:
Angular Azure Devops-Beispielprojekt
In der Beispielpipeline werden werden an zwei Stellen Artefakte erzeugt:
Sowohl in der build
als auch in der publish
Stage.
Im Beispiel entsteht hier kein Nutzen aus diesem Vorgehen.
Es geht dabei vielmehr darum, zu zeigen, wie Artefakte in verschiedenen Stages erstellt werden und später weiterverwendet werden können.
In der finalen publish
Stage würden typischerweise noch weitere Elemente hinzugefügt, wie beispielsweise Quellcode und Dokumentation, wenn das Ergebnis an einen Kunden ausgeliefert wird.
Die Beispielpipeline könnte in weiteren Ausbaustufen erweitert werden:
So könnte die Angular Anwendung als Docker Image gebaut und in einer Container Registry bereitgestellt werden, oder Reports der Testergebnisse erstellt und ebenfalls in Azure DevOps publiziert werden.
Zu den Themen Kubernetes, Docker und Angular bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.