Remote Debugging .NET Containers

DotnetDockerBanner

You will find several articles that will talk about local debugging .NET container through the magic of Visual Studio or VS Code, but it’s not easy to find the right information for remote debugging.

Local debugging with the different IDEs is much easier because they install the .NET debugger in the container for us without telling us and configure it properly. Compared to traditional remote debugging for .NET Framework, debugging for .NET Core does not require the use of msvsmon.exe. Instead, you need to use vsdbg provided by OmniSharp.

Local debugging requires us to run our container on our machine from the compiled source code. This is convenient during the development phase when we produce our code, but it is less convenient when we need to fix an anomaly that occurs in a subsequent phase of our delivery process. In this case, local debugging of a container can be long and tedious given the time required to retrieve the correct version of the image to be executed and its dependencies.

In this article, we will see how to debug a remote Linux Container that contains an ASP.NET Core application. For reference, here is a Dockerfile generated by Visual Studio for an ASP.NET Core application:

#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 everywhere

A simple search will show you several debugging examples that require you to modify the Dockerfile. The simplest approach is to include the .NET Core vsdbg debugger directly in our container image. It is simple to understand, but it complicates the developer’s job because he has to generate a new docker image and redeploy it before he can debug.

In DevOps, we favor the Build once, deploy everywhere principle. The goal here is to minimize the chances of introducing anomalies in our application by recompiling after testing. If all the tests of our application are done on a Debug version that potentially includes the debugger and that must be recompiled in Release before delivering to production, we introduce a risk of failure or we have to redo more tests. This is why we try as much as possible to deliver the binary that was used during the testing phase and, in this case, the image of the container used for the tests.

The approach of modifying the Dockerfile therefore takes us away from our Build once, deploy everywhere approach. Instead, it is possible to use the Release version of our application/image for the first tests, as this configuration includes the debugging symbols by default.

Installing the debugger

The first thing we need to do to be able to debug our application is to install the debugger. As mentioned before, it is not included in the basic container image. This allows us to keep the image size as small as possible. When we debug a Container locally, the IDE associates a volume to our container that contains the .NET Core debugger. When debugging remotely, since the container image is already created, it is preferable to install it manually.

IMPORTANT: Depending on your container base image, the following script may need to be modified. Getting packages under Linux varies from one distribution to another. For the example, I used the aspnet:3.1-buster-slim base image which is based on Debian.

To install the debugger, we must first connect in interactive mode inside the container :

Docker:

docker exec -it remote /bin/bash

Here, remote is the name given to the container at startup.

Kubernetes :

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

Here, remote-7ff879f496-5jvdg is the name of the pod containing the container to be debugged.

Once connected, you need to run the following script:

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

Note the following:

  • The first line allows the apt manager package to update its local list of packages without installing them.
  • The second line installs unzip (required by the getvsdbgsh script provided by Microsoft) and procps (required by the IDEs to list the processes running in the container).
  • The last line fetches the debugger installation script and installs it to the /root/vsdbg location inside the container.

You will have noticed that you need to access the container in interactive mode to install what you need.

Attaching to the process

We now have a .NET Core application in a container with a debugger installed right next to it. Since you can’t just press F5 to start debugging, you have to attach to the process running inside the container. A valid approach would be to open an SSH channel to the container, but this is something that can be simplified by using the features of our favorite IDE.

With VS Code, you need to create a debugging launch configuration specific to the runtime context:

Docker

When our container simply runs under Docker, the following configuration can be used:

{
    // 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}"
             }
         }
    ]
}

Launch.json file under .vscode folder

 

The interesting part is in blue. Note the following:

${command:pickRemoteProcess}:
Tells VS Code to open a pick list of the process to be debugged in the container. This is why procps had to be installed previously.

AttachProcess

« pipeProgram »: « docker » and « pipeArgs »: [ « exec », « -i », « remote » ], :
Tells VS Code the command line to execute when starting the debugger.

« debuggerPath »: « /root/vsdbg/vsdbg », :
Indicates where to find the debugger in the container file system. This path is linked to the end of the line seen above :

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

« sourceFileMap »: {
               « /src/ »: « ${workspaceRoot} »:
This parameter tells the debugger where to find the source files for debugging. Since the default Dockerfile template copies the source files to a src directory in the container, we must indicate that the src directory actually corresponds to our working directory in VS Code.

Breakpoints do not work

If you have copied the configuration mentioned above entirely, you probably won’t have any problems with breakpoints. It is important to mention that, since we are using a Release version of our .NET Core application, the debugger will not be able to load the symbol (pdb) files automatically. You just need to specify the parameter « justMyCode »:false for the magic to work. By doing so, breakpoints will work normally.

Kubernetes

For Kubernetes, it is recommended to use the Kubernetes extension for VS Code. It makes it much easier to connect to a pod to debug it. However, you will still have to install the debugger yourself beforehand. Once this is done, you can use the context menu of a pod from the Kubernetes extension as follows :

VSCodeK8sExtensionDebugAttach
However, the default configuration sets the justMyCode parameter to true. If you want to stay in the same workflow as mentioned at the beginning of the article (Build once, deploy anywhere), you must be able to set this parameter to false. The extension does not yet allow to change the configuration used to attach the debugger. So we have to use our own debug configuration as follows :

{
    "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
}

Launch.json file under the .vscode directory

Note that the name of the pod to be debugged will have to be modified according to your context.

Conclusion

I hope I’ve been able to clearly explain how to remotely debug a .NET Core container. Your environment may be a bit different if you have changed your base image or if your privileges are restricted in the runtime environment. You can always use the above examples to adapt them to your context.

A good application security practice is to use base images that contain only the bare essentials. The distroless images published by Google have been worked to eliminate what is not necessary to run your application. However, these images are problematic for debugging, as they do not contain a Shell that allows tools to be installed after the image is created. In Kubernetes, the use of Ephemeral Containers may help us with the arrival of Kubernetes version 1.18. A good subject for a next article!

 

Prefer reading in French? You can find a french version here : Déboguer les conteneurs .NET à distance

Déboguer les conteneurs .NET à distance

DotnetDockerBanner

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 bleu. 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.

AttachProcess

« 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 :
VSCodeK8sExtensionDebugAttach

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 !