CI/CD für .NET auf Azure: Von Null bis zur Produktion
15.02.2026
CI/CD für eine .NET-Anwendung auf Azure einzurichten bedeutet, den gesamten Weg vom Code-Commit bis zum Produktions-Deployment zu automatisieren — Build, Test und Release — mit entweder Azure DevOps Pipelines oder GitHub Actions. Nach fünf Jahren Aufbau und Wartung dieser Pipelines bei HUSS B.V. kann ich Ihnen sagen: Der Unterschied zwischen einer Tutorial-Pipeline und einer produktionsreifen Pipeline ist erheblich. Dieser Leitfaden zeigt, wie dieser Unterschied aussieht und wie Sie ihn überbrücken.
Warum CI/CD für .NET-Projekte wichtig ist
Manuelle Deployments sind ein Risiko. Jedes Mal, wenn jemand in Visual Studio mit Rechtsklick auf “Publish” klickt, würfeln Sie bei der Konsistenz. CI/CD eliminiert dieses Risiko, indem jedes Deployment identisch, nachvollziehbar und reversibel wird. Außerdem zwingt es Sie, testbaren Code zu schreiben — denn wenn Ihre Pipeline bei jedem Commit Tests ausführt, müssen Sie diese tatsächlich schreiben.
Speziell für .NET auf Azure ist das Tooling ausgereift. Microsoft hat erheblich investiert, um diesen Weg reibungslos zu gestalten, egal ob Sie Azure DevOps oder GitHub Actions wählen. Die Frage ist nicht ob Sie automatisieren sollten, sondern wie Sie es richtig machen.
Azure DevOps vs GitHub Actions für .NET
Beide Plattformen können die Aufgabe erfüllen. Die Wahl hängt in der Regel von den bestehenden Tools und Präferenzen Ihrer Organisation ab, nicht von einem grundlegenden technischen Unterschied. So sieht der Vergleich in der Praxis aus:
| Feature | Azure DevOps Pipelines | GitHub Actions |
|---|---|---|
| YAML-Pipeline-Unterstützung | Ja | Ja |
| Integrierte .NET SDK-Tasks | Umfangreiche Task-Bibliothek (DotNetCoreCLI@2) | Community Actions + dotnet CLI direkt |
| Azure-Integration | Native Service Connections | Azure Login Action + Service Principals |
| Deployment Slots | Erstklassiger Swap-Task | Unterstützt über azure/webapps-deploy |
| Artefaktverwaltung | Azure Artifacts integriert | GitHub Packages oder externe Feeds |
| Self-Hosted Agents | Unterstützt | Unterstützt (Self-Hosted Runners) |
| Environments & Genehmigungen | Environments mit Gates und Checks | Environments mit Protection Rules |
| Preise für private Repos | 1 kostenloser paralleler Job (1800 Min/Monat) | 2000 Min/Monat kostenlos |
| Lernkurve | Steiler — mehr Konzepte zu erlernen | Flacher — einfachere YAML-Struktur |
Meine Empfehlung: Wenn Ihre Organisation bereits Azure DevOps für Boards und Repos nutzt, bleiben Sie bei Azure DevOps Pipelines. Wenn Ihr Code auf GitHub liegt, verwenden Sie GitHub Actions. Die Migration zwischen beiden ist unkompliziert — die Pipeline-Logik ist nahezu identisch, nur die YAML-Syntax unterscheidet sich.
So richten Sie CI/CD für .NET auf Azure ein
Eine produktionsreife Pipeline folgt unabhängig von der Plattform demselben Kernablauf: Restore, Build, Test, Publish, Deploy. Nachfolgend finden Sie funktionierende Beispiele für Azure DevOps und GitHub Actions, die eine .NET 8-Webanwendung auf Azure App Service deployen.
Azure DevOps Pipeline
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.x'
azureSubscription: 'MyAzureServiceConnection'
appName: 'my-app-production'
stages:
- stage: Build
displayName: 'Build & Test'
jobs:
- job: BuildJob
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'webapp'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webAppLinux'
appName: '$(appName)'
deployToSlotOrASE: true
slotName: 'staging'
package: '$(Pipeline.Workspace)/webapp/**/*.zip'
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
jobs:
- deployment: DeployProduction
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
inputs:
azureSubscription: '$(azureSubscription)'
action: 'Swap Slots'
webAppName: '$(appName)'
sourceSlot: 'staging'
targetSlot: 'production' GitHub Actions Workflow
name: Build and Deploy
on:
push:
branches: [main]
env:
DOTNET_VERSION: '8.x'
AZURE_WEBAPP_NAME: 'my-app-production'
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Publish
run: dotnet publish --configuration Release --output ./publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
environment: staging
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to staging slot
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
slot-name: staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Swap staging to production
run: |
az webapp deployment slot swap
--name ${{ env.AZURE_WEBAPP_NAME }}
--resource-group my-resource-group
--slot staging
--target-slot production Deployment Slots und Zero-Downtime-Releases
Deployment Slots sind das wichtigste Feature für produktive .NET-Deployments auf Azure App Service. Die Strategie ist einfach: Deployen Sie in einen Staging-Slot, überprüfen Sie, ob alles funktioniert, und tauschen Sie dann Staging gegen Produktion. Der Swap erfolgt nahezu sofort, weil Azure lediglich das virtuelle IP-Routing umschaltet — Ihre Anwendung ist im Staging-Slot bereits aufgewärmt.
Einige Dinge, die ich auf die harte Tour gelernt habe:
- Konfigurieren Sie immer Slot-gebundene Einstellungen. Verbindungszeichenfolgen und App-Einstellungen, die sich zwischen Staging und Produktion unterscheiden — wie Datenbankverbindungen — müssen als “Slot-gebunden” markiert werden, damit sie beim Swap nicht übertragen werden.
- Verwenden Sie Health Checks. Konfigurieren Sie den Health-Check-Endpunkt des App Service, damit Azure überprüfen kann, ob Ihre Anwendung gesund ist, bevor der Swap abgeschlossen wird.
- Testen Sie den Swap-Pfad, nicht nur das Deployment. Ich habe Pipelines gesehen, bei denen der Deploy-to-Slot-Schritt einwandfrei funktionierte, der Swap aber aufgrund von Konfigurationsabweichungen fehlschlug. Automatisieren Sie den gesamten Ablauf.
Infrastructure as Code: Provisionieren vor dem Deployment
Ihre Pipeline sollte nicht davon ausgehen, dass die Infrastruktur bereits existiert. Verwenden Sie Bicep- oder ARM-Templates, um Ihre Azure-Ressourcen als Teil der Pipeline bereitzustellen — oder zumindest in einer separaten Infrastruktur-Pipeline, die zuerst ausgeführt wird.
Bei HUSS habe ich Bicep-Templates verwendet, um den App Service Plan, den App Service, Deployment Slots, Application Insights und Key Vault-Referenzen zu definieren. Das bedeutete, dass das Hochfahren einer neuen Umgebung ein einziger Pipeline-Lauf war und kein ganzer Tag Klicken durch das Azure-Portal.
Ein minimales Bicep-Snippet für einen App Service mit Staging-Slot:
resource appService 'Microsoft.Web/sites@2023-01-01' = {
name: appName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: 'v8.0'
}
}
}
resource stagingSlot 'Microsoft.Web/sites/slots@2023-01-01' = {
parent: appService
name: 'staging'
location: location
properties: {
serverFarmId: appServicePlan.id
}
} Häufige Fehler, die Sie vermeiden sollten
Die .NET SDK-Version nicht festlegen. Wenn Sie sich auf die vorinstallierte Version des Build-Agents verlassen, werden Ihre Builds zufällig fehlschlagen, wenn das Agent-Image aktualisiert wird. Geben Sie immer die exakte SDK-Version an.
Tests in der Pipeline überspringen. Ich verstehe es — Tests verlangsamen den Prozess. Aber eine Pipeline ohne Tests ist nur automatisiertes Risiko. Führen Sie mindestens Ihre Unit-Tests aus. Fügen Sie Integrationstests gegen eine Testdatenbank hinzu, wenn möglich.
Denselben Service Principal für alles verwenden. Erstellen Sie separate Service Principals für Staging und Produktion mit angemessen eingeschränkten Berechtigungen. Least Privilege ist nicht optional.
NuGet-Pakete nicht cachen. Beide Plattformen unterstützen Dependency-Caching. Nutzen Sie es. Es reduziert die Build-Zeiten bei größeren Solutions erheblich.
Direkt in die Produktion deployen. Selbst wenn Sie alleine entwickeln, verwenden Sie einen Staging-Slot. Die fünf Minuten, die er Ihrer Pipeline hinzufügt, sparen Stunden bei der Fehlersuche in Produktionsvorfällen.
Häufig gestellte Fragen
Brauche ich Azure DevOps, wenn mein Code auf GitHub liegt?
Nein. GitHub Actions bietet eine hervorragende Azure-Integration durch offizielle Actions wie azure/login und azure/webapps-deploy. Sie benötigen Azure DevOps nur, wenn Sie dessen andere Features wie Boards, Test Plans oder Azure Artifacts nutzen möchten. Für reines CI/CD ist GitHub Actions vollkommen ausreichend.
Kann ich .NET Framework (nicht .NET Core) mit diesen Pipelines deployen?
Ja, aber mit Einschränkungen. .NET Framework benötigt Windows-Build-Agents (windows-latest statt ubuntu-latest), und Sie verwenden MSBuild-Tasks anstelle von dotnet-CLI-Befehlen. Die gesamte Pipeline-Struktur bleibt gleich — Build, Test, Publish, Deploy — aber die spezifischen Befehle unterscheiden sich.
Wie gehe ich mit Datenbankmigrationen in der Pipeline um?
Führen Sie EF Core-Migrationen als Schritt zwischen Deployment und Slot-Swap aus. Deployen Sie Ihren Code in den Staging-Slot, führen Sie dotnet ef database update gegen die Staging-Datenbank aus, überprüfen Sie, ob alles funktioniert, und führen Sie dann den Swap durch. Führen Sie Migrationen niemals direkt gegen die Produktion während des Swaps aus — erledigen Sie sie vorher.
Was kosten Deployment Slots auf Azure App Service?
Deployment Slots sind ab dem Standard-Tarif ohne zusätzliche Kosten für den Slot selbst verfügbar. Allerdings verbraucht der Staging-Slot Ressourcen aus Ihrem App Service Plan, solange er läuft. Sie können den Staging-Slot nach dem Swap stoppen, um diese Ressourcen zurückzugewinnen, wenn die Kosten ein Thema sind.
Sollte ich YAML-Pipelines oder den klassischen Editor in Azure DevOps verwenden?
Verwenden Sie immer YAML-Pipelines. Der klassische Editor wird schrittweise abgeschafft, und YAML-Pipelines liegen in Ihrem Repository neben Ihrem Code. Das bedeutet, Ihre Pipeline-Definition ist versioniert, in Pull Requests überprüfbar und portabel. Es gibt keinen guten Grund, den klassischen Editor für neue Projekte zu verwenden.