Lab 13: Deployment nach Azure App Service¶
Hintergrund¶
In den bisherigen Labs haben wir Deployments nur simuliert - die Deploy-Stages gaben Meldungen wie "Deployment erfolgreich!" aus, ohne tatsächlich eine Anwendung irgendwo zu deployen. In diesem Lab führen wir erstmals ein echtes Deployment durch: Wir deployen unsere Node.js-Anwendung auf einen Azure App Service.
Azure App Service ist ein PaaS-Dienst (Platform as a Service) zum Hosten von Webanwendungen. Im Gegensatz zu einer VM oder einem Container musst du dich nicht um das Betriebssystem, Webserver-Konfiguration oder Patches kümmern - du lieferst nur den Anwendungscode, und App Service kümmert sich um den Rest. Der Dienst unterstützt diverse Sprachen (Node.js, Python, .NET, Java, PHP) und bietet Features wie Auto-Scaling, Deployment Slots (für Blue/Green Deployments in Lab 14), integriertes Monitoring und benutzerdefinierte Domains mit SSL-Zertifikaten.
Für das Deployment verwenden wir den AzureWebApp@1-Task, der die Anwendung als
ZIP-Paket auf den App Service hochlädt. Der Task authentifiziert sich über die
Service Connection azure-training-connection aus Lab 05. Das Deployment-Paket
wird mit dem ArchiveFiles@2-Task erstellt, der das gesamte
Arbeitsverzeichnis (Quellcode + installierte Abhängigkeiten) in eine ZIP-Datei
packt.
Ein wichtiges Konzept bei App Service ist der App Service Plan: Er definiert die zugrunde liegenden Compute-Ressourcen (CPU, RAM, Betriebssystem) und den Tarif. Mehrere Web Apps können sich einen Plan teilen. Für dieses Training wurde bereits ein Standard (S1)-Plan per Terraform bereitgestellt, der auch Deployment Slots unterstützt (siehe Lab 14).
Voraussetzungen¶
- Die Service Connection
azure-training-connectionaus Lab 05. - Die Environments
devundproductionaus Lab 11 (falls du die Deployment-Historie nutzen möchtest).
Aufgabenstellung¶
Schritt 1: Deine Web App kennenlernen¶
Für jeden Teilnehmer wurde bereits eine Web App per Terraform bereitgestellt.
Der App-Name folgt dem Muster app-training-teilnehmerNN (z. B.
app-training-teilnehmer01). Alle Apps teilen sich einen gemeinsamen
Standard (S1)-Plan namens plan-training-standard in der Resource Group
rg-pipeline-training.
Bash:
# Setze deinen App-Namen (ersetze NN mit deiner Teilnehmernummer)
APP_NAME="app-training-teilnehmerNN"
PowerShell:
# Setze deinen App-Namen (ersetze NN mit deiner Teilnehmernummer)
$APP_NAME = "app-training-teilnehmerNN"
# Prüfe, ob die App existiert und erreichbar ist
az webapp show --name $APP_NAME --resource-group rg-pipeline-training --query "state" -o tsv
echo "App URL: https://$APP_NAME.azurewebsites.net"
Unter https://<app-name>.azurewebsites.net siehst du eine
Standard-Willkommensseite von Azure. Diese wird nach dem ersten Deployment
durch unsere Anwendung ersetzt.
Schritt 2: Anwendung für App Service vorbereiten¶
Bisher bestand unsere Anwendung nur aus statischen Dateien (app.js,
index.html), die wir mit npm run build in ein dist/-Verzeichnis kopiert
haben. Für App Service brauchen wir einen HTTP-Server, der die Dateien
ausliefert. App Service startet die Anwendung mit npm start und erwartet, dass
sie auf dem Port lauscht, der über die Umgebungsvariable PORT definiert wird.
Erstelle die Datei src/server.js - ein einfacher Node.js-HTTP-Server mit einem Health-Endpoint:
const http = require('http');
const fs = require('fs');
const path = require('path');
const port = process.env.PORT || 8080;
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({
status: 'healthy',
version: process.env.APP_VERSION || 'unknown',
timestamp: new Date().toISOString()
}));
return;
}
// index.html ausliefern
const indexPath = path.join(__dirname, '..', 'index.html');
if (fs.existsSync(indexPath)) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(fs.readFileSync(indexPath));
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<h1>Hello from Azure App Service!</h1>');
}
});
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Der Server hat zwei Endpunkte:
/health: Gibt JSON mit Status, Version und Zeitstempel zurück. Diesen Endpoint nutzt die Pipeline für den Health Check nach dem Deployment. In der Praxis verwenden auch Load Balancer und Monitoring-Tools solche Health-Endpoints./(und alle anderen Pfade): Liefert dieindex.htmlaus dem übergeordneten Verzeichnis. Falls die Datei nicht existiert, wird eine Fallback-Seite angezeigt.
Wichtig ist process.env.PORT || 8080: App Service setzt die PORT-Variable
auf den internen Port, auf dem die App lauschen muss. Lokal (ohne die Variable)
verwendet der Server Port 8080.
Ersetze den Inhalt von package.json, um den start-Befehl und die
Engine-Anforderung hinzuzufügen:
{
"name": "hello-pipeline",
"version": "1.0.0",
"description": "Demo-App für App Service Deployment",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"build": "echo 'Build complete'",
"test": "node test/test.js"
},
"engines": {
"node": ">=18.0.0"
}
}
Das start-Skript ist entscheidend: App Service führt npm start aus, um die
Anwendung zu starten. Das engines-Feld dokumentiert die benötigte
Node.js-Version - App Service beachtet dieses Feld und gibt eine Warnung aus,
falls die konfigurierte Runtime-Version nicht passt.
Erstelle außerdem eine web.config-Datei für den Fall, dass die App auf einem Windows-basierten App Service läuft. Auf Linux wird diese Datei ignoriert, schadet aber nicht:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="iisnode" path="src/server.js" verb="*"
modules="iisnode"/>
</handlers>
<rewrite>
<rules>
<rule name="NodeInspector" patternSyntax="ECMAScript"
stopProcessing="true">
<match url="^src/server.js\/debug[\/]?"/>
</rule>
<rule name="StaticContent">
<action type="Rewrite" url="public{REQUEST_URI}"/>
</rule>
<rule name="DynamicContent">
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile"
negate="True"/>
</conditions>
<action type="Rewrite" url="src/server.js"/>
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
Die web.config konfiguriert IIS (den Windows-Webserver) so, dass alle Anfragen
an src/server.js weitergeleitet werden. Auf Linux-App-Services wird
stattdessen PM2 oder der direkte Node.js-Prozess verwendet.
Schritt 3: Pipeline mit App Service Deployment¶
Jetzt erstellen wir eine Pipeline mit zwei Stages: Build und Deploy. Die Build-Stage installiert die Abhängigkeiten, packt alles in eine ZIP-Datei und publiziert sie als Artefakt. Die Deploy-Stage deployt das ZIP-Paket auf den App Service und führt einen Health Check durch.
Ersetze den Inhalt von azure-pipelines.yml. Wichtig: Ersetze den
Platzhalter <dein-app-name> mit deinem tatsächlichen App-Namen aus Schritt 1:
trigger:
branches:
include:
- master
variables:
azureSubscription: 'azure-training-connection'
# Ersetze mit deinem App-Namen:
appName: '<dein-app-name>'
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: BuildApp
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Node.js installieren'
- script: npm install --production
displayName: 'Abhängigkeiten installieren'
# Deployment-Paket erstellen
- task: ArchiveFiles@2
displayName: 'Deployment-Paket erstellen'
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/app.zip'
replaceExistingArchive: true
- publish: '$(Build.ArtifactStagingDirectory)/app.zip'
artifact: 'web-app'
displayName: 'Artefakt publizieren'
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: Build
jobs:
- deployment: DeployWebApp
displayName: 'Deploy Azure Web App'
pool:
vmImage: 'ubuntu-latest'
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to App Service'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webAppLinux'
appName: '$(appName)'
package: '$(Pipeline.Workspace)/web-app/app.zip'
runtimeStack: 'NODE|20-lts'
- task: AzureCLI@2
displayName: 'Health Check'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
echo "Warte 30 Sekunden auf App-Start..."
sleep 30
echo "=== Health Check ==="
HEALTH_URL="https://$(appName).azurewebsites.net/health"
echo "URL: $HEALTH_URL"
HTTP_CODE=$(curl -s -o /tmp/health-response.txt -w "%{http_code}" $HEALTH_URL)
echo "HTTP Status: $HTTP_CODE"
echo "Response:"
cat /tmp/health-response.txt
echo ""
if [ "$HTTP_CODE" = "200" ]; then
echo "Health Check bestanden!"
else
echo "WARNUNG: Health Check nicht bestanden (HTTP $HTTP_CODE)"
echo "Die App braucht möglicherweise mehr Zeit zum Starten."
fi
Gehe die Pipeline Abschnitt für Abschnitt durch:
- Build-Stage: Installiert die npm-Abhängigkeiten mit
--production(nur reguläre Dependencies, keine devDependencies). DerArchiveFiles@2-Task packt das gesamte Arbeitsverzeichnis (Quellcode +node_modules) in eine ZIP-Datei.includeRootFolder: falsesorgt dafür, dass die Dateien direkt im ZIP-Root liegen (nicht in einem Unterordner). Das ZIP wird als Artefaktweb-apppubliziert. - DeployDev-Stage: Ein Deployment Job, der das Artefakt automatisch
herunterlädt und auf den App Service deployt. Der
AzureWebApp@1-Task übernimmt das eigentliche Deployment:appType: 'webAppLinux': Gibt an, dass es sich um eine Linux-App handelt.package: Pfad zum ZIP-Artefakt. Der Deployment Job lädt Artefakte automatisch nach$(Pipeline.Workspace)/<artefakt-name>/herunter.runtimeStack: 'NODE|20-lts': Konfiguriert die Node.js-Version auf dem App Service.
- Health Check: Nach dem Deployment wartet der
AzureCLI@2-Task 30 Sekunden (Cold-Start-Zeit) und prüft dann den/health-Endpoint. Falls der HTTP-Status 200 ist, gilt der Health Check als bestanden. Bei einem anderen Status wird eine Warnung ausgegeben, aber die Pipeline nicht abgebrochen - der App-Start kann bei Free-Tier-Apps manchmal länger dauern.
Schritt 4: Pipeline konfigurieren und starten¶
Ersetze den Platzhalter in der Pipeline-Datei und committe alle Dateien:
git add src/server.js package.json web.config azure-pipelines.yml
git commit -m "Add App Service deployment pipeline"
git push origin master
Beim ersten Lauf kann es sein, dass die Pipeline die Nutzung der Service Connection und/oder des Environments genehmigen lassen muss. Öffne den Pipeline-Run im Browser und klicke gegebenenfalls auf "Permit".
Schritt 5: App im Browser prüfen¶
Nach erfolgreichem Deployment sollte die App unter der Azure-URL erreichbar sein. Prüfe sowohl die Hauptseite als auch den Health-Endpoint:
Das Deployment kann jetzt einige Minuten dauern. Vorher funktionieren die Befehle nicht
Bash:
# Health Endpoint prüfen
curl https://$APP_NAME.azurewebsites.net/health
# Hauptseite prüfen (sollte index.html oder Fallback anzeigen)
curl https://$APP_NAME.azurewebsites.net
PowerShell:
# Health Endpoint prüfen
Invoke-RestMethod https://$APP_NAME.azurewebsites.net/health
# Hauptseite prüfen (sollte index.html oder Fallback anzeigen)
Invoke-RestMethod https://$APP_NAME.azurewebsites.net
Öffne die URL https://<dein-app-name>.azurewebsites.net auch im Browser, um
die Seite visuell zu prüfen.
Aufräumen¶
Die Web App und der App Service Plan werden zentral per Terraform verwaltet. Lösche diese Ressourcen nicht manuell - sie werden für Lab 14 (Blue/Green Deployment) weiterverwendet und am Ende des Trainings automatisch aufgeräumt.