Retour aux articles

Déploiement d’une Web App .NET sur SmarterASP avec Azure DevOps (CI/CD pas à pas)

Déploiement d’une Web App .NET sur SmarterASP avec Azure DevOps (CI/CD pas à pas)

Vous hébergez une app ASP.NET Core ou Blazor sur SmarterASP.NET et vous en avez assez de copier des fichiers à la main via FileZilla ? Ce guide vous explique comment mettre en place une pipeline CI/CD complète avec Azure DevOps qui build, publie et déploie automatiquement votre application à chaque push.

TL;DR

  • SmarterASP.NET expose un endpoint WebDeploy (MSDeploy) sur le port 8172.
  • Azure DevOps build votre projet avec dotnet publish, publie un artefact, puis invoque msdeploy.exe depuis l'agent Windows.
  • Les secrets (credentials, URL) sont stockés dans les variables secrètes du pipeline, jamais en clair dans le YAML.
  • Des règles -skip protègent vos fichiers précieux côté serveur (sitemaps, certificats Let's Encrypt…).

Pourquoi WebDeploy plutôt que FTP ?

FTP, c'est bien pour un premier déploiement en développement. Mais pour un projet qui évolue, plusieurs développeurs, branches, environnements, c'est une source de bugs silencieux : fichier oublié, ancienne DLL qui traîne, déploiement partiel après un timeout. WebDeploy (aka MSDeploy) règle ça en faisant une synchronisation différentielle entre votre dossier de publication et le serveur. Il ne transfère que ce qui a changé, et peut supprimer les fichiers obsolètes.

SmarterASP.NET supporte WebDeploy nativement. Leur endpoint ressemble à https://winXXXX.site4now.net:8172/msdeploy.axd. On va s'en servir directement depuis Azure DevOps.

ℹ Pré-requis
Vous avez besoin d'un compte Azure DevOps (gratuit pour les petits projets), d'un projet Git, et de vos identifiants SmarterASP.NET (username / password de votre panneau de contrôle). WebDeploy doit être activé dans les paramètres de votre site.
Pas encore client ? Mon lien partenaire vous donne accès à une offre promotionnelle.


Anatomie de la pipeline

La pipeline se découpe en deux stages : Build et Deploy. Cette séparation est importante : elle vous permet par exemple d'ajouter un stage de tests entre les deux, ou de déployer le même artefact vers plusieurs environnements.

stages:
  - stage: Build          # dotnet restore → build → publish → artefact
  - stage: Deploy         # download artefact → msdeploy → warm-up
    dependsOn: Build
    condition: succeeded()

La pipeline complète et commentée

pool:
  vmImage: 'windows-latest'   # MSDeploy n'existe que sur Windows

variables:
  BuildConfiguration: 'Release'
  DotNetVersion: '10.x'       # Centralisé ici pour faciliter les mises à jour
  ProjectPath: 'src\{lien-vers-votre-csproj}.csproj'
  PreserveRemoteFiles: 'false' # true = ne supprime pas les fichiers inconnus

# -------------------------------------------------------
# STAGE 1 - Build
# -------------------------------------------------------
stages:
- stage: Build
  displayName: 'Build & Publish'
  jobs:
  - job: Build
    steps:
    - task: UseDotNet@2
      displayName: 'SDK .NET $(DotNetVersion)'
      inputs:
        packageType: 'sdk'
        version: '$(DotNetVersion)'

    - task: DotNetCoreCLI@2
      displayName: 'Restore'
      inputs:
        command: 'restore'
        projects: '$(ProjectPath)'

    - task: DotNetCoreCLI@2
      displayName: 'Build'
      inputs:
        command: 'build'
        projects: '$(ProjectPath)'
        arguments: '--configuration $(BuildConfiguration) --no-restore'

    - task: DotNetCoreCLI@2
      displayName: 'Publish'
      inputs:
        command: 'publish'
        publishWebProjects: false
        projects: '$(ProjectPath)'
        # --no-build évite de recompiler ce qui vient d'être compilé
        arguments: >-
          --configuration $(BuildConfiguration)
          --output $(Build.ArtifactStagingDirectory)
          --no-build
        zipAfterPublish: false

    # Publie le dossier généré comme artefact nommé "webapp"
    - publish: '$(Build.ArtifactStagingDirectory)/webapp'
      displayName: 'Publie l''artefact webapp'
      artifact: 'webapp'

# -------------------------------------------------------
# STAGE 2 - Deploy
# -------------------------------------------------------
- stage: Deploy
  displayName: 'Déploiement WebDeploy → SmarterASP.NET'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: DeployToSmarterASP
    environment: prod
    strategy:
      runOnce:
        deploy:
          steps:
          - download: current
            artifact: 'webapp'

          - powershell: |
              $sourcePath = "$(Pipeline.Workspace)\webapp"

              # Recherche msdeploy aux deux emplacements standards des agents MS
              $msdeploy = @(
                "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe",
                "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe"
              ) | Where-Object { Test-Path $_ } | Select-Object -First 1

              if (-not $msdeploy) { throw "msdeploy.exe introuvable sur cet agent" }
              Write-Host "msdeploy trouvé : $msdeploy"

              $msdeployArgs = @(
                '-verb:sync',
                "-source:contentPath=""$sourcePath""",
                "-dest:contentPath=""$(WEBSITE_NAME)"",computerName=""$(SERVER_COMPUTER_NAME)"",userName=""$(SERVER_USERNAME)"",password=""$(SERVER_PASSWORD)"",authtype=""Basic"",includeAcls=""False""",
                '-allowUntrusted',
                '-disableLink:AppPoolExtension',
                '-disableLink:ContentExtension',
                '-disableLink:CertificateExtension',
                '-enableRule:AppOffline'   # Place un app_offline.htm pendant le déploiement
              )

              # Option : ne pas supprimer les fichiers inconnus côté serveur
              if ("$(PreserveRemoteFiles)".ToLower() -eq "true") {
                $msdeployArgs += '-enableRule:DoNotDeleteRule'
              }

              # --- Exclusions personnalisées (via variable PARAM_EXCLUDED_FILES) ---
              "$(PARAM_EXCLUDED_FILES)".Split(";") |
                Where-Object { $_.Trim() -ne "" } |
                ForEach-Object {
                  $regex = "(?i).*$([Regex]::Escape($_.Trim()))$"
                  Write-Host "Exclusion fichier : $regex"
                  $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=`"$regex`""
                }

              # --- Protéger les dossiers critiques côté serveur ---

              # Certificats Let's Encrypt (ACME challenge)
              $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=`"(?i).*\\\.well-known.*`""

              # Sitemaps générés dynamiquement
              $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=`"(?i).*\\wwwroot\\sitemaps(`$|\\.*)`""
              $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=`"(?i).*\\wwwroot\\sitemaps\\.*`""

              Write-Host "Lancement de msdeploy..."
              & $msdeploy @msdeployArgs

              if ($LASTEXITCODE -ne 0) {
                Write-Error "msdeploy a échoué avec le code $LASTEXITCODE"
                exit $LASTEXITCODE
              }
            displayName: 'WebDeploy → SmarterASP.NET'
            env:
              WEBSITE_NAME:          '$(WEBSITE_NAME)'
              SERVER_COMPUTER_NAME:  '$(SERVER_COMPUTER_NAME)'
              SERVER_USERNAME:       '$(SERVER_USERNAME)'
              SERVER_PASSWORD:       '$(SERVER_PASSWORD)'

          # Warm-up : réveille l'app après le déploiement
          - powershell: |
              $url = "$(WEBSITE_URL)"
              $maxRetries = 3
              $retryDelay = 10
              Write-Host "Warm-up de $url..."
              for ($i = 1; $i -le $maxRetries; $i++) {
                try {
                  $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 45
                  Write-Host "Site répond : $($r.StatusCode) - OK"
                  break
                } catch {
                  Write-Host "Tentative $i/$maxRetries échouée : $_"
                  if ($i -lt $maxRetries) { Start-Sleep -Seconds $retryDelay }
                }
              }
            displayName: 'Warm-up application'
            continueOnError: true   # Un warm-up raté ne doit pas faire échouer le déploiement

Les variables à configurer dans Azure DevOps

Rendez-vous dans Pipelines → votre pipeline → Edit → Variables. Certaines sont publiques, d'autres doivent être marquées comme secret pour ne jamais apparaître dans les logs.

Variable Exemple de valeur Secret ?
WEBSITE_NAME monsite.com Non
SERVER_COMPUTER_NAME https://win1234.site4now.net:8172/msdeploy.axd?site=monsite.com Non
SERVER_USERNAME votre-login-smarterasp Oui
SERVER_PASSWORD ••••••••• Oui
WEBSITE_URL https://monsite.com Non
PARAM_EXCLUDED_FILES appsettings.Production.json;Web.config Non

✓ Conseil
Retrouvez votre SERVER_COMPUTER_NAME dans le panneau de contrôle SmarterASP.NET → Publish your website → onglet Connection. Il commence toujours par https://winXXXX.site4now.net:8172.


Étape par étape : mettre ça en place

  1. Activez WebDeploy dans le panneau SmarterASP.NET. Par défaut c'est activé, mais vérifiez que le port 8172 n'est pas bloqué par un firewall de votre côté.

  2. Créez le fichier azure-pipelines.yml à la racine de votre dépôt Git. Copiez le YAML ci-dessus et adaptez ProjectPath et DotNetVersion à votre projet.

  3. Dans Azure DevOps, allez dans Pipelines → New pipeline, pointez sur votre dépôt, sélectionnez Existing Azure Pipelines YAML file.

  4. Ajoutez les variables dans l'interface Azure DevOps (pas dans le YAML !). Cochez Keep this value secret pour le mot de passe et le username.

  5. Créez l'environnement prod dans Pipelines → Environments. Vous pouvez y ajouter une approbation manuelle avant déploiement si vous travaillez en équipe.

  6. Lancez un premier run et observez les logs. Si msdeploy échoue, le message d'erreur est généralement explicite (mauvais credentials, port fermé, certificat non approuvé).


Les pièges courants

« msdeploy introuvable »

Les agents windows-latest de Microsoft incluent Web Deploy V3. Le script cherche aux deux emplacements classiques. Si ça échoue malgré tout, ajoutez une étape qui installe WebDeploy via Chocolatey :

choco install webdeploy -y

Certificat SSL non approuvé sur l'agent

Le flag -allowUntrusted dans la commande msdeploy règle ce problème. SmarterASP.NET utilise des certificats valides, mais les agents hébergés peuvent parfois avoir des problèmes de chaîne de confiance.

Mon appsettings.Production.json est écrasé

C'est le problème numéro un en production. Ajoutez ce fichier dans la variable PARAM_EXCLUDED_FILES : le script PowerShell le transforme automatiquement en règle -skip qui empêche msdeploy de le supprimer ou le remplacer.

⚠ Attention
Ne mettez jamais vos secrets de production (ConnectionStrings, clés API…) dans appsettings.json versionné. Utilisez appsettings.Production.json hors dépôt Git, ou mieux : Azure Key Vault si votre offre SmarterASP.NET le permet.

Le warm-up échoue avec une 302

Normal. Si votre site redirige HTTP → HTTPS, Invoke-WebRequest peut signaler une erreur sur la redirection. Le flag continueOnError: true fait que le pipeline ne considère pas ça comme un échec bloquant. Le site tourne très bien malgré tout.

Mes sitemaps générés dynamiquement disparaissent

Si votre app Blazor écrit des sitemaps dans wwwroot/sitemaps/ au runtime, ces fichiers n'existent pas dans l'artefact de publication. Sans précaution, msdeploy les efface à chaque déploiement. Les deux règles -skip dédiées dans le script protègent ce dossier.


Aller plus loin

Ajouter des tests avant le déploiement

Insérez un job Test entre Build et Deploy, ou ajoutez une étape DotNetCoreCLI@2 avec command: 'test' dans le stage Build. Azure DevOps intègre les résultats de tests directement dans l'interface.

Déployer sur plusieurs environnements

Dupliquez le stage Deploy et nommez-les staging et prod. Utilisez des variable groups différents pour chaque environnement (Library → Variable groups). Ajoutez une approbation manuelle sur l'environnement prod dans Pipelines → Environments.

Notifier en cas d'échec

Azure DevOps peut envoyer des emails ou des messages Teams/Slack en cas d'échec de pipeline. Configurez ça dans Project Settings → Notifications ou via une tâche InvokeRestAPI qui appelle un webhook.

✓ Bonne pratique finale
Déclenchez la pipeline uniquement sur les commits vers main en ajoutant un trigger en haut du YAML :

trigger:
  - main

Les branches de développement peuvent ainsi être poussées librement sans déclencher un déploiement en production.


Conclusion

Mettre en place ce pipeline demande une heure la première fois, mais c'est du temps récupéré à chaque déploiement ensuite. Plus de FileZilla, plus de risque d'oublier un fichier, plus de déploiements à 23h en croisant les doigts. Juste un git push sur main et votre app est en ligne deux minutes plus tard.

La combinaison Azure DevOps + MSDeploy + SmarterASP.NET est un peu moins connue que les grands clouds managés, mais elle fonctionne très bien pour des projets .NET qui n'ont pas besoin (ou pas le budget) d'Azure App Service ou AWS Elastic Beanstalk. Ce guide devrait vous permettre de la mettre en place sans tâtonner.

Si vous avez des questions sur un point spécifique, gestion des migrations EF Core au déploiement, multi-environnements, ou intégration avec Azure Key Vault, les commentaires sont là pour ça.

Hébergement ASP.NET - SmarterASP.NET

Commentaires (0)

Aucun commentaire pour le moment. Soyez le premier à commenter !

Laisser un commentaire

Votre email ne sera pas publié.
An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.