Skip to content

Pipelines Azure DevOps pour .NET

Guide pour créer des pipelines Azure DevOps pour build et déploiement d'applications .NET avec Docker.

📋 Prérequis

Avant de créer votre pipeline, assurez-vous que :

  • ✅ La configuration NuGet est en place
  • ✅ Les agents Azure DevOps self-hosted sont configurés
  • ✅ Le service connection vers Azure Container Registry existe
  • ✅ Le repository contient des Dockerfiles pour chaque projet

🏗️ Structure du pipeline

Notre pipeline standard pour .NET suit cette structure :

  1. Trigger : Déclenché sur les branches docker/*
  2. Restore : Restauration des packages NuGet
  3. Build : Compilation de la solution
  4. Publish : Publication de chaque projet
  5. Docker Build & Push : Création et push des images Docker

📝 Pipeline de base

Créez un fichier azure-pipelines.yml à la racine de votre repo :

```yaml title="azure-pipelines.yml" trigger: batch: true branches: include: - docker/*

name: $(Rev:r)

resources: - repo: self

variables: appversion: ${{ replace(variables['Build.SourceBranchName'], 'docker/', '') }} version: $(appversion).$(Build.BuildNumber) dockerRegistryServiceConnection: 'VOTRE_SERVICE_CONNECTION_ID' imageRepository: 'votre-app' containerRegistry: 'votreregistry.azurecr.io'

stages: - stage: Build condition: ${{ contains(variables['Build.SourceBranch'], 'docker/') }} displayName: Build and push stage jobs: - job: Build displayName: Build pool: name: 'jit-azure-devops-agents'

steps:
- task: Bash@3
  displayName: 'Set Short Commit ID'
  inputs:
    targetType: 'inline'
    script: |
      echo "##vso[task.setvariable variable=ShortCommitId]${BUILD_SOURCEVERSION:0:7}"

- task: UseDotNet@2
  displayName: 'Install .NET SDK'
  inputs:
    packageType: 'sdk'
    version: '9.x'
    installationPath: $(Agent.ToolsDirectory)/dotnet

- script: dotnet workload restore
  displayName: 'Restore workloads'

- task: DotNetCoreCLI@2
  displayName: 'Restore NuGet packages'
  inputs:
    command: 'restore'
    projects: '**/*.sln'

- task: DotNetCoreCLI@2
  displayName: 'Build solution'
  inputs:
    command: 'build'
    projects: '**/*.sln'
    arguments: '--configuration Release'

- task: DotNetCoreCLI@2
  displayName: 'Publish VotreProjet'
  inputs:
    command: 'publish'
    publishWebProjects: false
    zipAfterPublish: false
    modifyOutputPath: false
    projects: '**/VotreProjet.csproj'
    arguments: '--self-contained true --configuration Release --output $(Build.ArtifactStagingDirectory)/VotreProjet /p:Version=$(appversion) /p:AssemblyVersion=$(version) /p:IncludeSourceRevisionInInformationalVersion=false'

- task: Docker@2
  displayName: 'Build Docker image - VotreProjet'
  inputs:
    containerRegistry: '$(dockerRegistryServiceConnection)'
    repository: '$(imageRepository)/votreprojet'
    command: 'buildAndPush'
    Dockerfile: 'VotreProjet/Dockerfile'
    buildContext: '$(Build.ArtifactStagingDirectory)/VotreProjet'
    tags: |
      $(version)
      latest

## 🎯 Exemple complet : Multi-projets

Pour une application avec plusieurs projets (Server, API FileManager, API LogClient) :

```yaml title="azure-pipelines.yml"
trigger:
  batch: true
  branches:
    include:
    - docker/*

name: $(Rev:r)

resources:
- repo: self

variables:
  appversion: ${{ replace(variables['Build.SourceBranchName'], 'docker/', '') }}
  version: $(appversion).$(Build.BuildNumber)
  dockerRegistryServiceConnection: '8d9e5dfc-b2fe-4717-b21d-39ada4afe21f'
  imageRepository: 'j-erp'
  containerRegistry: 'jeromeit.azurecr.io'

stages:
- stage: Build
  condition: ${{ contains(variables['Build.SourceBranch'], 'docker/') }}
  displayName: Build and push stage
  jobs:
  - job: Build
    displayName: Build
    pool:
      name: 'jit-azure-devops-agents'

    steps:
    - task: Bash@3
      name: SetShortCommitId
      displayName: 'Set Short Commit ID'
      inputs:
        targetType: 'inline'
        script: |
          echo "##vso[task.setvariable variable=ShortCommitId]${BUILD_SOURCEVERSION:0:7}"

    - task: UseDotNet@2
      name: SetSdkVersion
      displayName: 'Set sdk version'
      inputs:
        packageType: 'sdk'
        version: '9.x'
        installationPath: $(Agent.ToolsDirectory)/dotnet

    - script: |
        dotnet workload restore
      displayName: 'Restore workloads'

    - task: DotNetCoreCLI@2
      displayName: 'Dotnet restore'
      inputs:
        command: 'restore'
        projects: 'j-ERP.sln'

    - task: DotNetCoreCLI@2
      displayName: 'build solution'
      inputs:
        command: 'build'
        projects: 'j-ERP.sln'
        arguments: '--configuration Release'

    # Project 1: Server
    - task: DotNetCoreCLI@2
      displayName: 'Publish j-ERP.Server'
      inputs:
        command: 'publish'
        publishWebProjects: false
        zipAfterPublish: false
        modifyOutputPath: false
        projects: '**/j-ERP.Server.csproj'
        arguments: '--self-contained true --configuration Release --output $(build.artifactstagingdirectory)/Server /p:Version=$(appversion) /p:AssemblyVersion=$(version) /p:IncludeSourceRevisionInInformationalVersion=false'

    - task: Docker@2
      displayName: 'Build Docker image for j-ERP.Server'
      inputs:
        containerRegistry: '$(dockerRegistryServiceConnection)'
        repository: '$(imageRepository)/j-erp.server'
        command: 'buildAndPush'
        Dockerfile: 'j-ERP.Server/Dockerfile'
        buildContext: '$(Build.ArtifactStagingDirectory)/Server'
        tags: |
          $(version)
          latest

    # Project 2: FileManager
    - task: DotNetCoreCLI@2
      displayName: 'Publish j-ERP.Api.FileManager'
      inputs:
        command: 'publish'
        publishWebProjects: false
        zipAfterPublish: false
        modifyOutputPath: false
        projects: '**/j-ERP.Api.FileManager.csproj'
        arguments: '--self-contained true --configuration Release --output $(build.artifactstagingdirectory)/FileManager /p:Version=$(appversion) /p:AssemblyVersion=$(version) /p:IncludeSourceRevisionInInformationalVersion=false'

    - task: Docker@2
      displayName: 'Build Docker image for j-ERP.Api.FileManager'
      inputs:
        containerRegistry: '$(dockerRegistryServiceConnection)'
        repository: '$(imageRepository)/j-erp.api.filemanager'
        command: 'buildAndPush'
        Dockerfile: 'j-ERP.Api.FileManager/Dockerfile'
        buildContext: '$(Build.ArtifactStagingDirectory)/FileManager'
        tags: |
          $(version)
          latest

    # Project 3: LogClient
    - task: DotNetCoreCLI@2
      displayName: 'Publish j-ERP.Api.LogClient'
      inputs:
        command: 'publish'
        publishWebProjects: false
        zipAfterPublish: false
        modifyOutputPath: false
        projects: '**/j-ERP.Api.LogClient.csproj'
        arguments: '--self-contained true --configuration Release --output $(build.artifactstagingdirectory)/LogClient /p:Version=$(appversion) /p:AssemblyVersion=$(version) /p:IncludeSourceRevisionInInformationalVersion=false'

    - task: Docker@2
      displayName: 'Build Docker image for j-ERP.Api.LogClient'
      inputs:
        containerRegistry: '$(dockerRegistryServiceConnection)'
        repository: '$(imageRepository)/j-erp.api.logclient'
        command: 'buildAndPush'
        Dockerfile: 'j-ERP.Api.LogClient/Dockerfile'
        buildContext: '$(Build.ArtifactStagingDirectory)/LogClient'
        tags: |
          $(version)
          latest

🔧 Configuration du pool d'agents

Utiliser un pool self-hosted

pool:
  name: 'jit-azure-devops-agents'

Utiliser un agent Microsoft-hosted

pool:
  vmImage: 'ubuntu-latest'

🐳 Dockerfiles

Chaque projet doit avoir son Dockerfile. Exemple pour une API ASP.NET :

```dockerfile title="VotreProjet/Dockerfile" FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443

FROM base AS final WORKDIR /app COPY . . ENTRYPOINT ["dotnet", "VotreProjet.dll"]


:::tip Note importante
Le Dockerfile utilise le contexte de build `$(Build.ArtifactStagingDirectory)/VotreProjet` qui contient déjà les fichiers publiés, donc un simple COPY suffit.
:::

## 📊 Variables importantes

| Variable | Description | Exemple |
|----------|-------------|---------|
| `appversion` | Version extraite du nom de branche | `4.1.3` (depuis `docker/4.1.3`) |
| `version` | Version complète avec build number | `4.1.3.56` |
| `Build.SourceBranchName` | Nom de la branche | `docker/4.1.3` |
| `Build.BuildNumber` | Numéro de build incrémental | `56` |
| `Build.ArtifactStagingDirectory` | Répertoire des artifacts | `/home/azureuser/azagent/_work/9/a` |

## 🚀 Workflow de release

### 1. Créer une branche de version

```bash
git checkout -b docker/4.1.3
git push origin docker/4.1.3

2. Le pipeline se déclenche automatiquement

Le pipeline : - Build la solution - Publie chaque projet - Crée les images Docker - Push vers Azure Container Registry avec les tags : - 4.1.3.56 (version + build number) - latest

3. Déployer l'image

docker pull jeromeit.azurecr.io/j-erp/j-erp.server:4.1.3.56
# ou
docker pull jeromeit.azurecr.io/j-erp/j-erp.server:latest

🐛 Troubleshooting

Erreur : No image label found to route agent pool

Symptôme :

##[error]No image label found to route agent pool Azure Pipelines

Cause : Vous utilisez vmImage au lieu de name pour un pool self-hosted.

Solution :

# ❌ Incorrect pour self-hosted
pool:
  vmImage: 'jit-azure-devops-agents'

# ✅ Correct pour self-hosted
pool:
  name: 'jit-azure-devops-agents'

Erreur NuGet 401/404

Voir la section troubleshooting de la documentation NuGet.

Les images Docker ne se buildent pas

Vérifications : 1. Le Dockerfile existe dans le bon répertoire 2. Le buildContext pointe vers le bon dossier 3. Docker est installé et accessible sur l'agent

# Sur l'agent
docker --version
docker images

📝 Template réutilisable

Vous pouvez créer un template réutilisable pour éviter la duplication :

```yaml title="templates/dotnet-publish-docker.yml" parameters: - name: projectName type: string - name: projectFile type: string - name: dockerRepository type: string

steps: - task: DotNetCoreCLI@2 displayName: 'Publish ${{ parameters.projectName }}' inputs: command: 'publish' publishWebProjects: false zipAfterPublish: false projects: '**/${{ parameters.projectFile }}' arguments: '--self-contained true --configuration Release --output $(Build.ArtifactStagingDirectory)/${{ parameters.projectName }}'

  • task: Docker@2 displayName: 'Build Docker - ${{ parameters.projectName }}' inputs: containerRegistry: '$(dockerRegistryServiceConnection)' repository: '${{ parameters.dockerRepository }}' command: 'buildAndPush' Dockerfile: '${{ parameters.projectName }}/Dockerfile' buildContext: '$(Build.ArtifactStagingDirectory)/${{ parameters.projectName }}' tags: | $(version) latest

Utilisation :

```yaml
- template: templates/dotnet-publish-docker.yml
  parameters:
    projectName: 'j-ERP.Server'
    projectFile: 'j-ERP.Server.csproj'
    dockerRepository: '$(imageRepository)/j-erp.server'

🔗 Voir aussi