.NET CI/CD: Automatisiertes Deployment mit GitHub Actions und Azure DevOps
# .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:
Empfohlener Pipeline-Ablauf
Eine solide .NET CI/CD-Pipeline folgt diesen Phasen in der Reihenfolge:
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:
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: ./publishEin 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.
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.
# 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:
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:
- 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 productionDas 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:
# 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: 10In 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:
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: 5Die 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
- 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=moderateContainer-Image-Scan mit Trivy
- 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
- name: Code auf Secrets scannen
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verifiedIn 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
# Schlecht -- kann ohne Vorwarnung kaputtgehen
uses: actions/checkout@main
# Gut -- auf bestimmte Version gepinnt
uses: actions/checkout@v4Ungepinnte 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:
- 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:
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:
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
RESTful APIs mit ASP.NET Core entwickeln
Lernen Sie die Grundlagen für produktionsreife REST-APIs mit ASP.NET Core. Controller, Routing und Best Practices.
.NET Testing: Unit-, Integrations- und E2E-Teststrategien
Erstellen Sie eine Teststrategie für .NET-Projekte. xUnit, Moq und Testpyramide.
Microservices-Architektur mit .NET: Design und Umsetzung
Entwerfen Sie Microservices-Architektur mit .NET. Service-Kommunikation und Orchestrierung.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen