.NET CI/CD: Automatisiertes Deployment mit GitHub Actions und Azure DevOps

14 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
.NET CI/CDGitHub Actions .NETAzure DevOps .NET.NET deploymentPipeline .NETAutomated deployment C#Docker .NET CIRelease automation

# .NET CI/CD: Automatisiertes Deployment mit GitHub Actions und Azure DevOps

Eine ausgereifte CI/CD-Pipeline ist die Grundlage fuer zuverlaessige .NET-Releases. Sie reduziert das Release-Risiko, verkuerzt Feedback-Zyklen fuer Entwickler und standardisiert die Deployment-Qualitaet. In diesem Artikel gehe ich die komplette Anatomie einer produktionsreifen Pipeline durch -- vom ersten Commit bis zum erfolgreichen Deployment, mit echten Konfigurationsbeispielen und Erfahrungswerten aus der Praxis.

Warum CI/CD fuer .NET-Projekte so wichtig ist

Manuelle Deployments sind fehleranfaellig. Ich habe Teams erlebt, die ganze Nachmittage damit verbracht haben, einem Deployment-Problem auf den Grund zu gehen -- und am Ende war es ein vergessener Build-Schritt oder eine Umgebungsvariable, die nur auf dem Rechner eines Entwicklers korrekt gesetzt war. CI/CD eliminiert diese gesamte Fehlerkategorie.

Eine gut gestaltete Pipeline bietet:

  • **Reproduzierbarkeit** -- jeder Build laeuft in derselben sauberen Umgebung
  • **Geschwindigkeit** -- automatisierte Schritte laufen parallel und reduzieren die Release-Zeit von Stunden auf Minuten
  • **Vertrauen** -- Tests und Scans laufen bei jedem Commit und fangen Probleme ab, bevor sie die Produktion erreichen
  • **Nachvollziehbarkeit** -- jedes Deployment ist auf einen bestimmten Commit und Pipeline-Lauf zurueckverfolgbar
  • Empfohlener Pipeline-Ablauf

    Eine solide .NET CI/CD-Pipeline folgt diesen Phasen in der Reihenfolge:

  • Abhaengigkeiten wiederherstellen und deterministisch bauen
  • Unit- und Integrationstests ausfuehren
  • Statische Analyse und Sicherheitsscans durchfuehren
  • Signierte, versionierte Artefakte erzeugen
  • Kontrolliert deployen mit Rollback-Strategie
  • Schauen wir uns an, wie jede Phase in der Praxis aussieht.

    GitHub Actions Workflow fuer .NET

    GitHub Actions ist mein bevorzugtes Werkzeug fuer die meisten .NET-Projekte. Die YAML-basierte Konfiguration lebt im Repository und ist damit versionskontrolliert und reviewbar wie jeder andere Code.

    Hier ein vollstaendiger Workflow, der Build, Test, Publish und Deployment abdeckt:

    yaml
    name: .NET CI/CD Pipeline
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
    
    env:
      DOTNET_VERSION: '8.0.x'
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}
    
    jobs:
      build-and-test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: .NET SDK einrichten
            uses: actions/setup-dotnet@v4
            with:
              dotnet-version: ${{ env.DOTNET_VERSION }}
    
          - name: Abhaengigkeiten wiederherstellen
            run: dotnet restore --locked-mode
    
          - name: Build
            run: dotnet build --no-restore --configuration Release
    
          - name: Unit-Tests ausfuehren
            run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults/
    
          - name: Testergebnisse hochladen
            uses: actions/upload-artifact@v4
            if: always()
            with:
              name: test-results
              path: TestResults/
    
          - name: Publish
            run: dotnet publish src/MyApp/MyApp.csproj --no-build --configuration Release --output ./publish
    
          - name: Build-Artefakt hochladen
            uses: actions/upload-artifact@v4
            with:
              name: app-artifact
              path: ./publish
    
      deploy-staging:
        needs: build-and-test
        runs-on: ubuntu-latest
        if: github.ref == 'refs/heads/develop'
        environment: staging
        steps:
          - name: Artefakt herunterladen
            uses: actions/download-artifact@v4
            with:
              name: app-artifact
              path: ./publish
    
          - name: Auf Azure Web App deployen (Staging)
            uses: azure/webapps-deploy@v3
            with:
              app-name: myapp-staging
              publish-profile: ${{ secrets.AZURE_STAGING_PUBLISH_PROFILE }}
              package: ./publish
    
      deploy-production:
        needs: build-and-test
        runs-on: ubuntu-latest
        if: github.ref == 'refs/heads/main'
        environment: production
        steps:
          - name: Artefakt herunterladen
            uses: actions/download-artifact@v4
            with:
              name: app-artifact
              path: ./publish
    
          - name: Auf Azure Web App deployen (Production)
            uses: azure/webapps-deploy@v3
            with:
              app-name: myapp-production
              publish-profile: ${{ secrets.AZURE_PROD_PUBLISH_PROFILE }}
              package: ./publish

    Ein paar wichtige Details. Das `--locked-mode`-Flag beim Restore stellt sicher, dass die `packages.lock.json` beachtet wird -- keine unerwarteten Paketversionsaenderungen waehrend der Pipeline. Das `if: always()` beim Upload der Testergebnisse bedeutet, dass Berichte auch bei fehlgeschlagenen Tests verfuegbar sind, was fuer die Fehlersuche unverzichtbar ist.

    Azure DevOps Pipelines

    Fuer Organisationen, die bereits in das Microsoft-Oekosystem investiert haben, ist Azure DevOps die natuerliche Wahl. Es bietet tiefere Integration mit Azure-Diensten, eingebaute Freigabe-Gates und feingranulare Zugriffskontrollen.

    yaml
    trigger:
      branches:
        include:
          - main
          - develop
    
    pool:
      vmImage: 'ubuntu-latest'
    
    variables:
      buildConfiguration: 'Release'
      dotnetVersion: '8.0.x'
    
    stages:
      - stage: Build
        jobs:
          - job: BuildAndTest
            steps:
              - task: UseDotNet@2
                inputs:
                  packageType: 'sdk'
                  version: $(dotnetVersion)
    
              - script: dotnet restore --locked-mode
                displayName: 'Abhaengigkeiten wiederherstellen'
    
              - script: dotnet build --configuration $(buildConfiguration) --no-restore
                displayName: 'Loesung bauen'
    
              - script: dotnet test --configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"
                displayName: 'Tests mit Coverage ausfuehren'
    
              - task: PublishCodeCoverageResults@2
                inputs:
                  summaryFileLocation: '**/coverage.cobertura.xml'
    
              - script: dotnet publish src/MyApp/MyApp.csproj --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)
                displayName: 'Anwendung publizieren'
    
              - task: PublishBuildArtifacts@1
                inputs:
                  pathToPublish: $(Build.ArtifactStagingDirectory)
                  artifactName: 'app-drop'
    
      - stage: DeployStaging
        dependsOn: Build
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
        jobs:
          - deployment: DeployToStaging
            environment: 'staging'
            strategy:
              runOnce:
                deploy:
                  steps:
                    - task: AzureWebApp@1
                      inputs:
                        azureSubscription: 'MyAzureServiceConnection'
                        appName: 'myapp-staging'
                        package: '$(Pipeline.Workspace)/app-drop'

    In meinen Deployment-Pipelines richte ich zwischen Staging und Production in Azure DevOps immer Freigabe-Gates ein. Die eingebaute Environment-Approval-Funktion ist weitaus zuverlaessiger als eigene Skripte. Ausserdem liefert sie eine saubere Nachvollziehbarkeit, wer was wann freigegeben hat.

    Docker Multi-Stage Build fuer .NET

    Containerisierung ist heute der Standard fuer .NET-Deployments. Ein Multi-Stage Dockerfile haelt Images klein und den Build-Prozess sauber, indem es das SDK (Build-Werkzeuge) von der Runtime trennt.

    dockerfile
    # Phase 1: Build
    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    WORKDIR /src
    
    # Projektdateien zuerst kopieren fuer besseres Layer-Caching
    COPY *.sln .
    COPY src/MyApp/MyApp.csproj src/MyApp/
    COPY src/MyApp.Domain/MyApp.Domain.csproj src/MyApp.Domain/
    COPY tests/MyApp.Tests/MyApp.Tests.csproj tests/MyApp.Tests/
    RUN dotnet restore
    
    # Rest kopieren und bauen
    COPY . .
    RUN dotnet build --configuration Release --no-restore
    
    # Phase 2: Test
    FROM build AS test
    RUN dotnet test --configuration Release --no-build --logger trx --results-directory /TestResults
    # Bei fehlgeschlagenen Tests stoppt der Build hier
    
    # Phase 3: Publish
    FROM build AS publish
    RUN dotnet publish src/MyApp/MyApp.csproj --configuration Release --no-build --output /app/publish /p:UseAppHost=false
    
    # Phase 4: Runtime
    FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
    WORKDIR /app
    
    # Nicht-Root-Benutzer erstellen
    RUN adduser --disabled-password --gecos "" appuser
    USER appuser
    
    COPY --from=publish /app/publish .
    
    EXPOSE 8080
    ENV ASPNETCORE_URLS=http:"code-comment">//+:8080
    
    ENTRYPOINT ["dotnet", "MyApp.dll"]

    Die entscheidende Optimierung besteht darin, `.csproj`-Dateien zu kopieren und `dotnet restore` auszufuehren, bevor der restliche Quellcode kopiert wird. Docker cached jede Schicht, sodass der Restore-Schritt komplett uebersprungen wird, wenn sich die Projektdateien nicht geaendert haben. In meinen Deployment-Pipelines hat dieser einzelne Trick die Build-Zeiten durchschnittlich um 40-60% verkuerzt.

    Die Ausfuehrung als Nicht-Root-Benutzer in der letzten Phase ist eine bewaeahrte Sicherheitspraxis. Sie begrenzt den Wirkungsradius, falls die Anwendung kompromittiert wird.

    Docker-Builds in CI integrieren

    Fuegen Sie den Docker-Build-und-Push-Schritt Ihrem GitHub Actions Workflow hinzu:

    yaml
      build-and-push-image:
        needs: build-and-test
        runs-on: ubuntu-latest
        if: github.ref == 'refs/heads/main'
        permissions:
          contents: read
          packages: write
        steps:
          - uses: actions/checkout@v4
    
          - name: Bei Container Registry anmelden
            uses: docker/login-action@v3
            with:
              registry: ${{ env.REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}
    
          - name: Docker Image bauen und pushen
            uses: docker/build-push-action@v5
            with:
              context: .
              push: true
              tags: |
                ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
                ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

    Taggen Sie Ihre Images neben `latest` immer auch mit dem Commit-SHA. Das macht Rollbacks trivial -- Sie deployen einfach den vorherigen SHA-Tag erneut.

    Deployment-Strategien

    Die Wahl der richtigen Deployment-Strategie haengt von der Ausfalltoleranz Ihrer Anwendung und dem Risikoniveau des Releases ab.

    Blue/Green Deployment

    Blue/Green verwendet zwei identische Umgebungen. Eine (Blue) bedient den Live-Traffic, waehrend die andere (Green) das neue Deployment erhaelt. Nach der Verifizierung wird der Traffic auf Green umgeschaltet.

    Im Azure App Service bekommen Sie das mit Deployment Slots kostenlos:

    yaml
      - name: In Staging-Slot deployen
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-production
          slot-name: staging
          package: ./publish
    
      - name: Smoke-Tests gegen Staging-Slot ausfuehren
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" https:"code-comment">//myapp-production-staging.azurewebsites.net/health)
          if [ "$response" != "200" ]; then
            echo "Smoke-Test fehlgeschlagen mit Status $response"
            exit 1
          fi
    
      - name: Slots tauschen
        uses: azure/cli@v2
        with:
          inlineScript: |
            az webapp deployment slot swap \
              --resource-group myapp-rg \
              --name myapp-production \
              --slot staging \
              --target-slot production

    Das Schoene am Slot-Swap: Wenn etwas schiefgeht, tauschen Sie einfach zurueck. Die vorherige Version laeuft noch im Staging-Slot.

    Canary Deployment

    Canary-Releases leiten einen kleinen Prozentsatz des Traffics auf die neue Version und erhoehen diesen Anteil schrittweise, wenn das Vertrauen waechst. Ideal fuer hochfrequentierte Dienste, bei denen selbst ein kurzer Ausfall inakzeptabel ist.

    Mit Azure Front Door oder einem Service Mesh wie Istio koennen Sie Traffic-Splitting konfigurieren:

    yaml
    # Kubernetes Canary mit Istio VirtualService
    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: myapp
    spec:
      hosts:
        - myapp.example.com
      http:
        - route:
            - destination:
                host: myapp-stable
                port:
                  number: 80
              weight: 90
            - destination:
                host: myapp-canary
                port:
                  number: 80
              weight: 10

    In meinen Deployment-Pipelines starte ich Canary-Rollouts mit 5% Traffic und erhoehe erst, nachdem ich Fehlerraten und Latenzen mindestens 15 Minuten beobachtet habe. Wenn sich eine Schluesselmetrik verschlechtert, rollt die Pipeline automatisch zurueck.

    Rolling Deployment

    Rolling Deployments aktualisieren Instanzen einzeln nacheinander. In Kubernetes ist dies die Standardstrategie:

    yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: myapp
    spec:
      replicas: 4
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
      template:
        spec:
          containers:
            - name: myapp
              image: ghcr.io/myorg/myapp:latest
              readinessProbe:
                httpGet:
                  path: /health
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 5

    Die Readiness Probe ist hier entscheidend. Ohne sie koennte Kubernetes Traffic an einen Pod weiterleiten, der noch hochfaehrt.

    Sicherheitsscans in der CI

    Sicherheit darf kein nachtraeglicher Gedanke sein. Die Integration von Scans in die Pipeline faengt Schwachstellen ab, bevor sie die Produktion erreichen.

    Abhaengigkeits-Schwachstellenscan

    yaml
      - name: Auf bekannte Schwachstellen pruefen
        run: dotnet list package --vulnerable --include-transitive
    
      - name: NuGet Audit ausfuehren
        run: dotnet restore --force-evaluate /p:NuGetAudit=true /p:NuGetAuditLevel=moderate

    Container-Image-Scan mit Trivy

    yaml
      - name: Docker Image auf Schwachstellen scannen
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
    
      - name: Trivy-Scanergebnisse hochladen
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

    Secret-Scanning

    yaml
      - name: Code auf Secrets scannen
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

    In meinen Deployment-Pipelines behandle ich Sicherheitsscan-Fehler als harte Blocker. Eine Pipeline, die bekannte kritische Schwachstellen in die Produktion laesst, ist schlimmer als keine Pipeline -- sie vermittelt ein falsches Sicherheitsgefuehl.

    Haeufige CI/CD-Fehler

    Im Laufe der Jahre habe ich immer wieder dieselben Fehler bei verschiedenen Teams beobachtet. Die folgenden richten den groessten Schaden an:

    1. Action- und Image-Versionen nicht pinnen

    yaml
    # Schlecht -- kann ohne Vorwarnung kaputtgehen
    uses: actions/checkout@main
    
    # Gut -- auf bestimmte Version gepinnt
    uses: actions/checkout@v4

    Ungepinnte Versionen bedeuten, dass Ihre Pipeline an einem beliebigen Dienstag kaputtgehen kann, weil eine Upstream-Action ein Breaking Change veroeffentlicht hat. Pinnen Sie immer Versionen.

    2. Secrets im Repository speichern

    Es klingt offensichtlich, aber ich stosse immer noch auf Connection Strings und API-Keys, die ins Repository committed wurden. Nutzen Sie das Secret-Management Ihrer CI-Plattform. Bei GitHub Actions sind das Repository- oder Environment-Secrets. Bei Azure DevOps die Azure Key Vault-Integration.

    3. Tests ueberspringen, um "Zeit zu sparen"

    Manche Teams deaktivieren Tests in der Pipeline, weil sie langsam sind. Die richtige Loesung ist, die Tests schneller zu machen -- parallelisieren, Unit-Tests von Integrationstests trennen oder Test Impact Analysis verwenden. Niemals ueberspringen.

    4. Kein Rollback-Plan

    Jedes Deployment sollte einen getesteten Rollback-Mechanismus haben. Ob Slot-Swap, Kubernetes-Rollback oder erneutes Deployment des vorherigen Artefakts -- der Rollback-Pfad sollte automatisiert und regelmaessig erprobt sein.

    5. Pipeline-Performance ignorieren

    Eine Pipeline, die 45 Minuten dauert, toetet die Entwicklerproduktivitaet. Verfolgen Sie Ihre Pipeline-Ausfuehrungszeit und optimieren Sie konsequent. Haeufige Verbesserungen:

  • NuGet-Pakete zwischen Laeufen cachen
  • Unabhaengige Jobs parallel ausfuehren
  • Wo moeglich inkrementelle Builds nutzen
  • Die richtige Runner-Groesse waehlen
  • yaml
      - name: NuGet-Pakete cachen
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

    6. Dieselbe Pipeline fuer jeden Branch

    Feature-Branches muessen nicht auf Staging deployt werden. Verwenden Sie bedingte Logik, um nur die relevanten Phasen auszufuehren:

    yaml
    deploy-staging:
      if: github.ref == 'refs/heads/develop'
    
    deploy-production:
      if: github.ref == 'refs/heads/main'

    Betriebliche Leitplanken

    Ueber die Pipeline hinaus brauchen Sie Beobachtbarkeit in Ihrem Release-Prozess:

  • **Deployment-Haeufigkeit** -- wie oft deployen Sie. Hoeher ist normalerweise besser, aber nur wenn die Fehlerrate niedrig bleibt.
  • **Change Failure Rate** -- welcher Prozentsatz der Deployments Vorfaelle verursacht. Zielwert unter 5%.
  • **Mean Time to Recovery (MTTR)** -- wie schnell erholen Sie sich von einem fehlgeschlagenen Deployment. Automatische Rollbacks reduzieren dies drastisch.
  • **Lead Time for Changes** -- die Zeit vom Commit bis zur Produktion. Eine gesunde Pipeline haelt dies unter einer Stunde.
  • Verfolgen Sie diese Metriken und ueberpruefen Sie sie monatlich. Sie sagen mehr ueber die Liefergesundheit Ihres Teams aus als jedes einzelne Tool oder jede einzelne Konfiguration.

    Fazit

    Effektive CI/CD ist sowohl technische Automatisierung als auch Release-Governance. Eine gut aufgebaute Pipeline fuehrt nicht nur Befehle aus -- sie erzwingt Qualitaets-Gates, ermoeglicht sichere Deployments und gibt dem gesamten Team das Vertrauen, dass jedes Release produktionsreif ist. Die Investition in eine saubere Einrichtung zahlt sich bei jedem einzelnen Deployment aus.

    Ich unterstuetze gern beim Entwurf einer produktionsreifen .NET-CI/CD-Pipeline.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen