RYDIR

CI/CD voor .NET op Azure: Van nul tot productie

15 feb 2026

CI/CD opzetten voor een .NET-applicatie op Azure betekent het hele pad van code commit tot productie-deployment automatiseren — build, test en release — met Azure DevOps Pipelines of GitHub Actions. Na vijf jaar bouwen en onderhouden van deze pipelines bij HUSS B.V. kan ik je vertellen dat het verschil tussen een tutorial-pipeline en een productiewaardige pipeline aanzienlijk is. Deze gids laat zien hoe dat verschil eruitziet en hoe je het overbrugt.

Waarom CI/CD belangrijk is voor .NET-projecten

Handmatige deployments zijn een risico. Elke keer dat iemand met de rechtermuisknop op “Publish” klikt in Visual Studio, gok je op consistentie. CI/CD elimineert dat risico door elke deployment identiek, traceerbaar en omkeerbaar te maken. Het dwingt je ook om testbare code te schrijven — want als je pipeline bij elke commit tests draait, moet je ze ook daadwerkelijk schrijven.

Voor .NET op Azure specifiek is de tooling volwassen. Microsoft heeft flink geinvesteerd om dit pad soepel te maken, of je nu kiest voor Azure DevOps of GitHub Actions. De vraag is niet of je moet automatiseren, maar hoe je het goed doet.

Azure DevOps vs GitHub Actions voor .NET

Beide platformen kunnen de klus klaren. De keuze komt meestal neer op de bestaande tooling en voorkeuren van je organisatie, niet op een fundamenteel technisch verschil. Zo vergelijken ze in de praktijk:

Eigenschap Azure DevOps Pipelines GitHub Actions
YAML pipeline-ondersteuning Ja Ja
Ingebouwde .NET SDK-taken Uitgebreide takenbibliotheek (DotNetCoreCLI@2) Community actions + dotnet CLI direct
Azure-integratie Native service connections Azure Login action + service principals
Deployment slots Eersteklas swap-taak Ondersteund via azure/webapps-deploy
Artefactbeheer Azure Artifacts ingebouwd GitHub Packages of externe feeds
Self-hosted agents Ondersteund Ondersteund (self-hosted runners)
Omgevingen & goedkeuringen Environments met gates en checks Environments met protection rules
Prijs voor private repos 1 gratis parallelle job (1800 min/maand) 2000 min/maand gratis
Leercurve Steiler — meer concepten om te leren Lager — eenvoudigere YAML-structuur

Mijn aanbeveling: als je organisatie al Azure DevOps gebruikt voor boards en repos, blijf dan bij Azure DevOps Pipelines. Als je code op GitHub staat, gebruik dan GitHub Actions. Migreren tussen de twee is eenvoudig — de pipeline-logica is vrijwel identiek, alleen de YAML-syntax verschilt.

Hoe je CI/CD opzet voor .NET op Azure

Een productiewaardige pipeline volgt dezelfde kernflow ongeacht het platform: restore, build, test, publish, deploy. Hieronder vind je werkende voorbeelden voor zowel Azure DevOps als GitHub Actions die een .NET 8 webapplicatie deployen naar Azure App Service.

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 en zero-downtime releases

Deployment slots zijn de allerbelangrijkste feature voor productie-.NET-deployments op Azure App Service. De strategie is simpel: deploy naar een staging slot, controleer of het werkt en swap staging naar productie. De swap is vrijwel direct omdat Azure alleen de virtuele IP-routering omschakelt — je app is al opgewarmd in de staging slot.

Een paar dingen die ik op de harde manier heb geleerd:

  • Configureer altijd slot-sticky instellingen. Connection strings en app-instellingen die verschillen tussen staging en productie — zoals databaseverbindingen — moeten als “slot sticky” worden gemarkeerd, zodat ze niet meeverhuizen bij de swap.
  • Gebruik health checks. Configureer het App Service health check-endpoint zodat Azure kan verifieren dat je app gezond is voordat de swap wordt voltooid.
  • Test het swap-pad, niet alleen de deployment. Ik heb pipelines gezien waar de deploy-naar-slot stap perfect werkte, maar de swap faalde door configuratieverschillen. Automatiseer de volledige flow.

Infrastructure as Code: Provisioning voor deployment

Je pipeline moet er niet vanuit gaan dat de infrastructuur al bestaat. Gebruik Bicep of ARM templates om je Azure-resources te provisioneren als onderdeel van de pipeline — of op z’n minst in een aparte infrastructuur-pipeline die eerst draait.

Bij HUSS gebruikte ik Bicep templates om het App Service Plan, de App Service, deployment slots, Application Insights en Key Vault-referenties te definieren. Dat betekende dat het opzetten van een nieuwe omgeving een enkele pipeline-run was, niet een dag klikken door de Azure-portal.

Een minimaal Bicep-fragment voor een App Service met een 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
  }
}

Veelgemaakte fouten om te vermijden

Je .NET SDK-versie niet vastpinnen. Als je vertrouwt op welke versie dan ook die voorgeinstalleerd is op de build agent, gaan je builds willekeurig kapot als de agent-image wordt bijgewerkt. Specificeer altijd de exacte SDK-versie.

Tests overslaan in de pipeline. Ik snap het — tests vertragen de boel. Maar een pipeline zonder tests is gewoon geautomatiseerd risico. Draai op z’n minst je unit tests. Voeg integratietests tegen een testdatabase toe als je kunt.

Dezelfde service principal voor alles gebruiken. Maak aparte service principals voor staging en productie met passend afgebakende rechten. Least privilege is niet optioneel.

NuGet-packages niet cachen. Beide platformen ondersteunen dependency caching. Gebruik het. Het verkort buildtijden aanzienlijk bij grotere solutions.

Direct naar productie deployen. Zelfs als je een solo-developer bent, gebruik een staging slot. De vijf minuten die het toevoegt aan je pipeline besparen uren aan het debuggen van productie-incidenten.

Veelgestelde vragen

Heb ik Azure DevOps nodig als mijn code op GitHub staat?

Nee. GitHub Actions heeft uitstekende Azure-integratie via officiele actions zoals azure/login en azure/webapps-deploy. Je hebt Azure DevOps alleen nodig als je de andere features wilt, zoals Boards, Test Plans of Azure Artifacts. Voor pure CI/CD is GitHub Actions prima geschikt.

Kan ik .NET Framework (niet .NET Core) deployen met deze pipelines?

Ja, maar met kanttekeningen. .NET Framework vereist Windows build agents (windows-latest in plaats van ubuntu-latest), en je gebruikt MSBuild-taken in plaats van dotnet CLI-commando’s. De algehele pipeline-structuur blijft hetzelfde — build, test, publish, deploy — maar de specifieke commando’s verschillen.

Hoe ga ik om met databasemigraties in de pipeline?

Voer EF Core-migraties uit als een stap tussen deployment en slot swap. Deploy je code naar de staging slot, voer dotnet ef database update uit tegen de staging-database, controleer of alles werkt en swap dan. Voer migraties nooit rechtstreeks uit tegen productie tijdens de swap — doe ze ervoor.

Wat kosten deployment slots op Azure App Service?

Deployment slots zijn beschikbaar vanaf de Standard-tier zonder extra kosten voor de slot zelf. De staging slot verbruikt echter resources van je App Service Plan wanneer deze draait. Je kunt de staging slot stoppen na de swap om die resources vrij te maken als kosten een zorg zijn.

Moet ik YAML pipelines of de klassieke editor gebruiken in Azure DevOps?

Gebruik altijd YAML pipelines. De klassieke editor wordt uitgefaseerd, en YAML pipelines staan in je repository naast je code. Dat betekent dat je pipeline-definitie onder versiebeheer staat, reviewbaar is in pull requests en overdraagbaar is. Er is geen goede reden om de klassieke editor te gebruiken voor nieuwe projecten.