Aller au contenu principal

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 :

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) :

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 :

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"]
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

VariableDescriptionExemple
appversionVersion extraite du nom de branche4.1.3 (depuis docker/4.1.3)
versionVersion complète avec build number4.1.3.56
Build.SourceBranchNameNom de la branchedocker/4.1.3
Build.BuildNumberNuméro de build incrémental56
Build.ArtifactStagingDirectoryRépertoire des artifacts/home/azureuser/azagent/_work/9/a

🚀 Workflow de release

1. Créer une branche de version

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 :

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 :

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

🔗 Voir aussi