Déboguer les conteneurs .NET à distance

Pragmatisme - DevOps - CloudNative - .NET

Déboguer les conteneurs .NET à distance

Vous trouverez plusieurs articles qui parleront du débogage local d’un container .NET en passant par la magie de Visual Studio ou de VS Code, mais il n’est pas facile de trouver la bonne information pour le débogage à distance.

Le dégage local avec les différents IDE est beaucoup plus facile parce que ces derniers installent le débogueur .NET dans le conteneur pour nous à notre insu et le configurent adéquatement. Comparativement au débogage à distance traditionnel pour .NET Framework, celui pour .NET Core ne nécessite pas d’utiliser msvsmon.exe. Il faut plutôt utiliser vsdbg fournit par OmniSharp.

Le débogage local nous oblige à exécuter notre conteneur sur notre machine à partir du code source compilé. C’est pratique pendant la phase de développement où on réalise nos lignes de code, mais ça l’est moins lorsqu’on doit corriger une anomalie qui survient dans une phase subséquente de notre processus de livraison. Dans ce cas, le débogage local d’un conteneur peut s’avérer long et fastidieux étant donné le temps requis pour récupérer la bonne version de l’image à exécuter ainsi que ses dépendances.

Dans cet article, nous verrons comment déboguer un conteneur Linux à distance qui comporte une application ASP.NET Core. Pour référence, voici un Dockerfile généré par Visual Studio pour une application ASP.NET Core :

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["TestRemoteDebugK8s.csproj", "/src"]
RUN dotnet restore "TestRemoteDebugK8s.csproj"
COPY . .
WORKDIR "/src"
RUN dotnet build "TestRemoteDebugK8s.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "TestRemoteDebugK8s.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TestRemoteDebugK8s.dll"]

Build once, deploy anywhere

Une simple recherche vous montrera plusieurs exemples de débogage qui nécessitent de modifier le fichier Dockerfile. L’approche simple vise à inclure le débogueur .NET Core vsdbg directement dans notre image de conteneur. Cette approche est simple à comprendre, mais elle complexifie le travail du développeur parce qu’il doit générer une nouvelle image docker et la redéployer avant de pouvoir déboguer.

En DevOps, on privilégie le principe Build once, deploy everywhere. On vise ici à minimiser les chances d’introduire des anomalies dans notre application en recompilant après les essais. Si tous les essais de notre application sont fait sur une version Debug incluant potentiellement le débogueur et qui faut recompiler en Release avant de livrer en production, on introduit un risque de défaillance ou on se doit de refaire plus de tests. C’est pourquoi on tente au maximum de livrer le binaire qui a été utilisé pour faire les essais et, dans ce cas-ci, l’image du conteneur utilisée pour les tests.

L’approche de modifier le fichier Dockerfile nous éloigne donc de notre approche Build once, deploy everywhere. Il est possible d’utiliser la version Release de notre application/image des les premiers tests, car cette configuration inclut par défaut les symboles de débogage.

Installer le débogueur

La première chose que l’on doit faire pour être en mesure de déboguer notre application est d’installer le débogueur. Comme mentionné précédemment, il n’est pas inclus dans l’image de base du conteneur. On peut ainsi garder la taille de l’image la plus petite possible. Lorsqu’on débogue un conteneur en local, l’IDE associe un volume à notre conteneur qui contient le débogueur .NET Core. En débogage à distance, comme l’image de conteneur est déjà créée, il est préférable de l’installer manuellement.

IMPORTANT : Dépendamment de votre image de base pour votre conteneur, le script suivant pourrait devoir être modifié. L’obtention des packages sous Linux change d’une distribution à l’autre. Pour l’exemple, j’ai utilisé l’image de base aspnet:3.1-buster-slim qui est basée sur Debian.

Pour installer le débogueur, on doit d’abord se connecter en mode interactif à l’intérieur du conteneur :

Docker :

docker exec -it remote /bin/bash

Ici, remote est le nom donné au conteneur lors de son démarrage.

Kubernetes :

kubectl exec -it remote-7ff879f496-5jvdg -- /bin/bash

Ici, remote-7ff879f496-5jvdg est le nom du pod contenant le conteneur à déboguer.

Une fois connecté, il faut exécuter le script suivant :

apt update && \ 
apt install -y unzip procps && \ 
curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l ~/vsdbg

À noter :

  • La première ligne permet au package manager apt de mettre à jour sa liste locale de packages sans les installer.
  • La deuxième ligne installe unzip (requis par le script getvsdbgsh fournit par Microsoft) et procps (requis par les IDEs pour lister les processus en exécution dans le conteneur)
  • La dernière ligne récupère le script d’installation du débogueur et l’installe à l’emplacement /root/vsdbg à l’intérieur du conteneur.

Vous aurez noté qu’il faut avoir accès au conteneur en mode interactif pour installer ce qu’il faut.

S’attacher au processus

Nous avons maintenant une application .NET Core dans un conteneur avec un débogueur installé juste à côté. Comme on ne peut pas simplement appuyer sur F5 pour démarrer le débogage, on doit s’attacher au processus qui s’exécute à l’intérieur du conteneur. Une approche valable serait d’ouvrir un canal SSH vers le conteneur, mais c’est quelque chose qui peut être simplifié avec l’utilisation des fonctionnalités de notre IDE préféré.

Avec VS Code, il faut créer une configuration de lancement du débogage propre au contexte d’exécution :

Docker

Lorsque notre conteneur s’exécute simplement sous Docker, on peut utiliser la configuration suivante :

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": ".NET Core Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickProcess}"
        },
        {
            "name": ".NET Core Docker Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickRemoteProcess}",
            "pipeTransport": {
                "pipeProgram": "docker",
                "pipeArgs": [ "exec", "-i", "remote" ],
                "debuggerPath": "/root/vsdbg/vsdbg",
                "pipeCwd": "${workspaceRoot}",
                "quoteArgs": false
            },
            "justMyCode":false,
            "sourceFileMap": {
                "/src/": "${workspaceRoot}"
            }
        }
    ]
}

Fichier Launch.json sous le répertoire .vscode

La partie intéressante se trouve en évidence. Notons les éléments suivants :

${command:pickRemoteProcess} :
Indique à VS Code d’ouvrir une liste de sélection du processus à déboguer dans le conteneur. C’est pour cette raison qu’il a fallu installé procps précédemment.

« pipeProgram »: « docker » et « pipeArgs »: [ « exec », « -i », « remote » ], :
Indique à VS Code la ligne de commande à exécuter lors du démarrage du débogueur.

« debuggerPath »: « /root/vsdbg/vsdbg », :
Indique où trouver le débogueur dans le système de fichier du conteneur. Ce chemin d’accès est lié à la fin de la ligne vue précédemment : 

curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l ~/vsdbg

« sourceFileMap »: {
               « /src/ »: « ${workspaceRoot} » :
Ce paramètre permet d’indiquer au débogueur où trouver les fichiers sources pour effectuer le débogage. Comme le gabarit par défaut du Dockerfile copie les fichiers sources dans un répertoire src dans le conteneur, il faut indiquer que le répertoire src correspond en fait à notre répertoire de travail dans VS Code.

Les points d’arrêt ne fonctionnent pas

Si vous avez copié intégralement la configuration mentionnée ci-dessus, vous n’aurez sans doute pas de problèmes avec les points d’arrêts. Il est important de mentionné que, puisqu’on utilise une version Release de notre application .NET Core, la débogueur ne sera pas en mesure de charger les fichiers de symbole (pdb) automatiquement. Il suffit de spécifier le paramètre « justMyCode »:false pour que la magie opère. Ce faisant, les points d’arrêt fonctionneront normalement.

Kubernetes

Pour Kubernetes, il est recommandé d’utiliser l’extension Kubernetes pour VS Code. Il facilite grandement la tâche quand vient le temps de se connecter à un pod pour le déboguer. Cependant, vous aurez tout de même à installer le débogueur vous-même auparavant. Une fois que c’est fait, vous pouvez utiliser le menu contextuel d’un pod à partir de l’extension Kubernetes comme suit :

Cependant, la configuration par défaut définit le paramètre justMyCode à vrai. Si on veut demeurer dans le même flux de travail comme mentionné au début de l’article (Build once, deploy anywhere), il faut être en mesure de passer ce paramètre à faux. L’extension ne permet pas encore de changer la configuration utilisée pour attacher le débogueur. Il faut donc utiliser notre propre configuration de débogage comme suit :

{
    "name": ".NET Core K8s Attach",
    "type": "coreclr",
    "request": "attach",
    "processId": "${command:pickRemoteProcess}",
    "pipeTransport": {
        "pipeProgram": "kubectl",
        "pipeArgs": [ "exec", "-i", "remote-75c859fc4c-gcx7d", "--" ],
        "debuggerPath": "/vsdbg/vsdbg",
        "pipeCwd": "${workspaceRoot}",
        "quoteArgs": false
    },
    "sourceFileMap": {
        "/src/TestRemoteDebugK8s/": "${workspaceRoot}"
    },
    "justMyCode":false
}

Fichier Launch.json sous le répertoire .vscode

Il est à noter que le nom du pod à déboguer devra être modifié en fonction de votre contexte.

Conclusion

J’espère avoir réussi à expliquer clairement comment effectuer du débogage à distance d’un conteneur en .NET Core. Il se peut que votre environnement soit un peu différent si vous avez changé d’image de base où si vos privilèges sont restreints dans l’environnement d’exécution. Vous pourrez toujours vous inspirer des exemples ci-dessus pour les adapter à votre contexte.

Une bonne pratique de sécurité applicative est d’utiliser des images de base contenant le stricte nécessaire. Les images distroless publiées par Google ont été travaillées pour éliminer ce qui n’est pas nécessaire à l’exécution de votre application. Toutefois, ces images posent problème pour déboguer, car elle ne contiennent pas de Shell permettant l’installation d’outils après la création de l’image. Dans Kubernetes, l’utilisation des Ephemeral Containers pourra nous venir en aide avec l’arrivée de la version 1.18 de Kubernetes. Un bon sujet pour un prochain article !