RYDIR

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.