13 septembre 2025
Packager Crystal Reports dans un conteneur Windows pour Azure App Service
La migration d’applications legacy vers le cloud est pleine de surprises. En général, on s’attend à rencontrer des problèmes dans le code, l’infrastructure ou le CI/CD, mais en réalité presque tout peut surgir. Cette fois, tout a buté sur Crystal Reports… Voyons comment nous avons réussi à migrer vers Azure App Service une application legacy en ASP.NET classique, qui utilise Crystal Reports pour générer des rapports.
Introduction

La tâche était plutôt classique : migrer l’application vers le cloud. Azure App Service a été choisi comme plateforme cible : moins de gestion de serveurs web, un déploiement plus simple, des logs et une maintenance plus faciles. Ensuite, configuration habituelle de l’infrastructure et du déploiement. À première vue, rien de compliqué.Mais il est rapidement apparu qu’il existait une contrainte forte. L’application utilise Crystal Reports, et dans l’univers du .NET Framework classique, ce composant est très fortement lié à Windows et à son runtime natif.Concrètement, le résultat était assez désagréable : le site fonctionnait, la logique métier fonctionnait, mais les rapports levaient des exceptions.Au final, il ne s’agissait donc plus simplement de « lancer le site » dans App Service, mais de reproduire autour de lui l’environnement Windows dans lequel Crystal Reports peut réellement fonctionner correctement. À partir de là, l’histoire a cessé d’être une migration App Service standard pour devenir une question d’environnement d’exécution.

Solution retenue

Dans le cadre de cette migration, pour des raisons évidentes, personne ne voulait réécrire la couche de reporting ni chercher en urgence une alternative à Crystal Reports. La décision a donc été prise de packager l’application dans un conteneur Windows et de l’exécuter dans Azure App Service for Containers.Cette approche présentait plusieurs avantages. Elle permettait de conserver la stack existante en .NET Framework 4.8, d’obtenir un contrôle complet sur l’environnement et de figer explicitement tout ce qui, auparavant, « vivait simplement sur le serveur » : la version de Windows, Crystal Reports Runtime, mais aussi, comme nous l’avons découvert, certaines DLL système, les polices, les paramètres régionaux et les logs.En pratique, le conteneur n’est donc pas seulement devenu un emballage pour l’application, mais une manière de décrire tout l’environnement d’exécution.
La solution retenue
FROM mcr.microsoft.com/windows:1809 AS system_files# Final image: ASP.NET 4.8 on Windows Server Core 2022FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022SHELL ["cmd", "/S", "/C"]# Crystal Reports prerequisitesCOPY --from=system_files C:/Windows/System32/oledlg.dll C:/Windows/System32/oledlg.dllCOPY --from=system_files C:/Windows/SysWOW64/oledlg.dll C:/Windows/SysWOW64/oledlg.dll# Install Crystal Reports RuntimeCOPY ./CrystalReportsInstaller/CR13SP35MSI64_0-80007712.MSI C:/install/RUN msiexec.exe /i C:\install\CR13SP35MSI64_0-80007712.MSI /qn /norestart /L*V C:\install\cr_install.log || exit /b 0# Deploy applicationCOPY ./WebApplication C:/inetpub/wwwroot# Permissions and persistent log storageRUN powershell -NoProfile -Command "New-Item -Type Directory -Path C:\home\LogFiles\Application -Force | Out-Null"RUN icacls "C:\inetpub\wwwroot" /grant "IIS_IUSRS:(OI)(CI)M" /T && icacls "C:\inetpub\wwwroot" /grant "IUSR:(OI)(CI)R" /T && icacls "C:\home\LogFiles" /grant "IIS_IUSRS:(OI)(CI)M" /T
Compatibilité des versions Windows

Lorsque l’on travaille avec des conteneurs Linux, on prend vite l’habitude de considérer le choix de l’image de base comme une question de composants internes plutôt que de compatibilité. Avec Windows, c’est plus strict. La version de l’image du conteneur doit être compatible avec l’hôte sur lequel ce conteneur sera ensuite exécuté.Dans ce cas, l’application web fonctionne à partir de l’image mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022. Cela exige une famille Windows correspondante côté App Service. Sinon, le conteneur ne démarre tout simplement pas.Un stage intermédiaire basé sur mcr.microsoft.com/windows:1809 est également utilisé pour récupérer certains fichiers système. Il s’est avéré que windowsservercore ne contenait pas certaines bibliothèques et polices utilisées lors de la génération des rapports.

Installation de Crystal Reports Runtime

La partie principale de toute cette construction concerne bien sûr l’installation de Crystal Reports Runtime dans le conteneur. Pour cela, le package MSI est copié dans l’image puis installé silencieusement via msiexec.Mais une simple installation ne s’est pas révélée suffisante. Pendant les tests, il est apparu qu’il fallait aussi copier oledlg.dll dans System32 et SysWOW64 depuis le Dockerfile. C’est un point important, car certaines dépendances de Crystal Reports s’attendent à trouver ces bibliothèques dans les deux architectures.

Polices

Les rapports ont commencé à fonctionner, mais leur rendu était incorrect. Le problème venait des polices. Pour Crystal Reports, c’est un sujet particulièrement sensible. Même si le rapport est formellement généré, l’absence des bonnes polices peut facilement faire bouger la mise en page, modifier les retours à la ligne, déplacer les marges et casser la largeur des colonnes.C’est pourquoi les polices Arial sont copiées séparément dans le conteneur et enregistrées dans le registre système.
# Fonts required by reportsCOPY --from=system_files C:/Windows/Fonts/arial.ttf C:/Windows/Fonts/arial.ttfCOPY --from=system_files C:/Windows/Fonts/arialbd.ttf C:/Windows/Fonts/arialbd.ttfCOPY --from=system_files C:/Windows/Fonts/ariali.ttf C:/Windows/Fonts/ariali.ttfCOPY --from=system_files C:/Windows/Fonts/arialbi.ttf C:/Windows/Fonts/arialbi.ttfRUN reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial (TrueType)" /t REG_SZ /d arial.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Bold (TrueType)" /t REG_SZ /d arialbd.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Italic (TrueType)" /t REG_SZ /d ariali.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Bold Italic (TrueType)" /t REG_SZ /d arialbi.ttf /f# Locale and time zoneRUN powershell -NoProfile -Command "Set-Culture fr-FR; Set-WinSystemLocale fr-FR; Set-WinUserLanguageList fr-FR -Force"RUN powershell -NoProfile -Command "tzutil /s 'Romance Standard Time'"
Locale, fuseau horaire et devises

Un autre élément qui a réservé une surprise concerne les paramètres régionaux du conteneur.Dans le Web.config, l’application avait déjà une globalisation fixée à fr-FR. Mais il s’est avéré que cela ne suffisait pas. Pour que les rapports et l’application dans son ensemble fonctionnent correctement, il a aussi fallu configurer explicitement dans le conteneur la culture, la locale système, la liste des langues, le fuseau horaire et les formats monétaires.Notre application génère des documents, travaille avec des dates, des montants, des devises et des représentations textuelles de nombres. Et si le conteneur fonctionne, par exemple, avec les paramètres par défaut, on obtient à nouveau des erreurs dans les rapports, même si tout est correct côté polices :les dates ne s’affichent pas dans le bon format ;les valeurs monétaires utilisent le mauvais séparateur ;le symbole de devise diffère de celui attendu ;le fuseau horaire n’est pas celui attendu.C’est pourquoi le conteneur définit explicitement fr-FR, le fuseau horaire Romance Standard Time et les paramètres de devise via HKEY_USERS\.DEFAULT\Control Panel\International.Il y a aussi une nuance supplémentaire. Comme l’application fonctionne dans IIS, le chargement du profil utilisateur est désactivé pour l’application pool, et les paramètres régionaux nécessaires sont définis pour le profil par défaut.

Droits d’accès et stockage persistant

Viennent ensuite des sujets moins spécifiques à Crystal Reports, liés à l’utilisation du disque pour les logs de l’application.Dans le Dockerfile, le répertoire C:\home\LogFiles\Application est créé séparément, et les droits sont accordés à IIS_IUSRS et IUSR à la fois sur le répertoire de l’application et sur le répertoire des logs.Il est particulièrement important de se rappeler ici que, dans App Service, le chemin C:\home à l’intérieur du conteneur est mappé vers le système de fichiers d’App Service. Autrement dit, les données qui y sont écrites seront accessibles dans le système de fichiers du service et, d’une part, ne disparaîtront pas lors d’un redéploiement du conteneur, et d’autre part, pourront être consultées via les developer tools.

Particularités du logging

Parlons maintenant des logs séparément.Dans le projet, le logging est configuré dans Web.config via log4net. Nous avons configuré deux canaux de sortie.Le premier est un RollingFileAppender, qui écrit dans C:\home\LogFiles\Application\app.log.Le second est un ConsoleAppender, qui écrit dans la sortie standard du conteneur.Ainsi, les logs sont visibles à la fois dans la sortie standard des logs applicatifs d’App Service et sous forme de fichiers dans le système de fichiers du service.L’appender fichier est pratique parce qu’il fournit un vrai rolling log. Dans notre configuration, le log est ajouté au fichier existant, il est rotaté par taille, limité à 10 MB par fichier et conserve jusqu’à 10 archives. De plus, MinimalLock est utilisé, ce qui réduit le risque de problèmes de verrouillage lors de l’écriture dans le fichier.
# Currency settings for the default profile used by IISRUN powershell -NoProfile -Command "New-Item -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Force | Out-Null; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sCurrency -Value ([char]0x20AC); \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name iCurrency -Value 3; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sMonDecimalSep -Value ','; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sMonThousandSep -Value ' '"RUN %WINDIR%\System32\inetsrv\appcmd set apppool /apppool.name:DefaultAppPool /processModel.loadUserProfile:falseEXPOSE 80ENTRYPOINT ["C:\\ServiceMonitor.exe", "w3svc"]window.setTimeout(function() { window.scrollTo(0, 1); }, 0)
Conclusion

Au final, nous avons réussi à exécuter l’application avec Crystal Reports dans Azure App Service sans réécrire la couche de reporting et sans abandonner .NET Framework. Mais ce qui a fonctionné ici n’est pas une seule astuce bien trouvée : c’est une longue chaîne de petits détails qui, pris séparément, sont faciles à sous-estimer.Il ne suffisait pas d’installer Crystal Reports Runtime dans le conteneur. Il a fallu construire autour de l’application un véritable environnement Windows : choisir l’image de base, vérifier la compatibilité des versions Windows, ajouter les DLL système manquantes, les polices, la culture, le fuseau horaire, les formats monétaires et un logging correct.Voici ci-dessous un exemple anonymisé de Dockerfile, inspiré de la version utilisée dans le projet.