Apprendre à déboguer vos conteneurs et vos images Docker

Dans ce chapitre, nous apprendrons à déboguer des conteneurs et images Docker, de manière à ce que vous soyez capable de les réparer mais aussi d'automatiser vos tâches d'administration Docker.

Introduction

Dans ce chapitre, nous allons nous attaquer à la partie Debug dans Docker. Le but de ce chapitre c'est que vous soyez capable de récolter finement des informations sur vos conteneurs afin d'être capable de réparer vos conteneurs mais aussi d'utiliser ces données dans vos scripts dans l'intention d'automatiser vos tâches d'administration Docker.

Les commandes de débogage

la commande stats

Imaginez que vous utilisez un conteneur (un Apache par exemple), mais malheureusement il n'arrive plus à répondre malgré le fait que son statut soit toujours à l'état UP. Que feriez-vous si étiez dans ce cas précis ?

Dans un premier temps, il serait d'abord intéressant de vérifier les statistiques d'utilisation des ressources de votre conteneur. Ceci pour se faire à l'aide de la commande Docker stats.

Dans le but de manipuler cette commande, nous allons premièrement télécharger et ensuite lancer l'image docker httpd :

docker run -tid --name httpdc -p 80:80 httpd

Si vous lancez la commande Docker stats sans argument alors elle vous affichera en temps réel les statistiques de consommation de tous vos conteneurs en cours d'exécution. Exemple :

docker stats

Résultat :

CONTAINER ID    NAME         CPU %        MEM USAGE / LIMIT      MEM %       NET I/O         BLOCK I/O      PIDS
eaa4f4c869a2    ubuntuc      0.00%        1.777MiB / 11.61GiB    0.01%       2.61kB / 0B     0B / 0B        1
24b9fa633549    httpdc       0.00%        7.082MiB / 11.61GiB    0.06%       4.01kB / 0B     0B / 0B        82

Le résultat est sous forme de table, voici ci-dessous une liste d'explication des différentes colonnes de la table de la commande Docker stats :

  • CONTAINER ID et Name : l'identifiant et le nom du conteneur.
  • CPU % et MEM % : le pourcentage de CPU et de mémoire de l'hôte utilisé par le conteneur.
  • MEM USAGE / LIMIT : la mémoire totale utilisée par le conteneur et la quantité totale de mémoire qu'il est autorisé à utiliser.
  • NET I/O : la quantité de données que le conteneur a envoyées et reçues sur son interface réseau.
  • BLOCK I/O : quantité de données lues et écrites par le conteneur à partir de périphériques en mode bloc sur l'hôte.
  • PIDs : le nombre de processus ou de threads créés par le conteneur.

Vous pouvez spécifier le nom ou l'id d'un seul ou plusieurs conteneur(s), pour ne visionner que les statistiques propres à vos conteneurs :

docker stats httpdc

Résultat :

CONTAINER ID    NAME         CPU %        MEM USAGE / LIMIT      MEM %       NET I/O         BLOCK I/O      PIDS
24b9fa633549    httpdc       0.00%        7.082MiB / 11.61GiB    0.06%       4.16kB / 0B     0B / 0B        82

Stressons un peu notre conteneur httpdc avec un script shell en envoyant plusieurs requêtes, en vue de visualiser l'augmentation de la consommation du conteneur :

#!/bin/bash
curl_func () {
    curl -s "http://localhost:80/page{1, 2}.php?[1-1000]" &
}

for i in {1..4}
do
    curl_func
done

wait 
echo "All done"

En lançant le script sur ma machine hôte, j'ai pu constater une augmentation au niveau de la consommation CPU et du flux réseau du conteneur :

CONTAINER ID    NAME       CPU %       MEM USAGE / LIMIT       MEM %      NET I/O             BLOCK I/O      PIDS
24b9fa633549    httpdc     25.24%      17.56MiB / 11.61GiB     0.15%      39.8MB / 73.1MB     0B / 0B        136

Grâce à l'option --format ou -f vous pouvez formater le résultat de la commande Docker stats de manière à limiter l'affichage du résultat en ne représentant que les ressources souhaitées. Dans cet exemple je ne vais afficher que la consommation CPU et le flux réseau du conteneur httpd sous forme de table :

docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.NetIO}}" httpdc

Résultat :

NAME        CPU %         NET I/O
httpdc      19.17%         39.8MB / 73MB

Voici la liste des différents mots résérvés pour l'option --format de la commande Docker stats :

  • .Container : Nom ou ID du conteneur (entrée utilisateur).
  • .Name : Nom du conteneur.
  • .ID : Identifiant du conteneur.
  • .CPUPerc : Pourcentage de CPU.
  • .MemUsage : Utilisation de la mémoire.
  • .NetIO : Utilisation du flux réseau Entrant/Sortant.
  • .BlockIO : Utilisation du disque dur en Lecture/Écriture.
  • .MemPerc : Pourcentage de mémoire (non disponible pour le moment sous Windows).
  • .PIDs : Nombre de PID (non disponible pour le moment sous Windows).

Autre chose, si vous désirez récupérer par exemple que la valeur de la consommation CPU à l'instant T de votre conteneur dans votre script pour la stocker ensuite dans une variable et donc par la même occasion d'éviter le résultat en mode streaming, alors vous pouvez utiliser l'option --no-stream, comme suit :

docker stats --no-stream  --format "{{.CPUPerc}}" httpdc

Résultat :

24.54%

La commande Docker inspect

La commande Docker inspect fournit des informations détaillées sous forme de tableau JSON sur les objets Docker (image Docker, conteneur docker, volume docker, etc ...). Nous avons déjà eu l'occasion d'utiliser cette commande, mais dans ce chapitre nous allons plus nous intéresser à la la partie filtrage de résultat de la commande Docker inspect.

Si vous tentez de lancer cette commande sur un conteneur (par exemple sur le conteneur httpdc créé précédemment), vous allez alors récupérer beaucoup trop d'informations :

docker inspect httpdc

Résultat (je n'affiche pas tout car c'est vraiment long) :

[
    {
        "Id": "24b9fa6335492cb968c000a4221bcb1d503a4befc4cb8770171f5345a350cdca",
        "Created": "2019-07-12T07:29:54.532971612Z",
       ...
            "EndpointID": "290ebd6de9ab4f3139ee93d49462a6ca692a1f18bac7888e14e5f4ebaab26aad",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "5220c685dc3d77ba5547fd853e055a66f6acffd9cc2f57acde61a6e1264ae9db",
                    "EndpointID": "290ebd6de9ab4f3139ee93d49462a6ca692a1f18bac7888e14e5f4ebaab26aad",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]

Pour amincir le résultat, nous allons une nouvelle fois utiliser l'option de formatage --format ou -f. Nous allons commencer par récupérer les sous-éléments de l'élément State de la façon suivante :

docker inspect --format='{{json .State}}' httpdc

Résultat :

{"Status":"running","Running":true,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":18532,"ExitCode":0,"Error":"","StartedAt":"2019-07-12T07:29:55.132385698Z","FinishedAt":"0001-01-01T00:00:00Z"

Je vais utiliser la bibliothèque json de python (installé par défaut sur Linux) afin d'avoir un affichage plus joli :

docker inspect --format='{{json .State}}' httpdc | python3 -m json.tool

Résultat :

{
    "Status": "running",
    "Running": true,
    "Paused": false,
    "Restarting": false,
    "OOMKilled": false,
    "Dead": false,
    "Pid": 18532,
    "ExitCode": 0,
    "Error": "",
    "StartedAt": "2019-07-12T07:29:55.132385698Z",
    "FinishedAt": "0001-01-01T00:00:00Z"
}

Essayons d'aller plus loins en récupérant sous un format texte l'élément Status de l'élément State. De cette manière vous pouvez par exemple stocker le résultat dans une variable de votre script :

docker inspect --format='{{ .State.Status}}' httpdc

Résultat :

running

Allons encore plus en profondeur et tentons de récupérer les mappages de port d'un conteneur. Premièrement, on va créer un conteneur utilisant différents mappages de port :

docker run -tid --name ubuntuc -p 9000:8000 -p 9001:8001 -p 9002:8002 ubuntu

Nous allons ensuite inspecter notre conteneur afin de récupérer les informations concernant les ports :

docker inspect --format='{{json .NetworkSettings.Ports}}' ubuntuc | python3 -m json.too

Nous récupérons ainsi le tableau JSON suivant :

{
    "8000/tcp": [
        {
            "HostIp": "0.0.0.0",
            "HostPort": "9000"
        }
    ],
    "0001/tcp": [
        {
            "HostIp": "0.0.0.0",
            "HostPort": "9001"
        }
    ],
    "8002/tcp": [
        {
            "HostIp": "0.0.0.0",
            "HostPort": "9002"
        }
    ]
}

Pour récupérer des informations d'un tableau JSON depuis l'option --format, il faut au préalable utiliser le mot-clé range. Dans cet exemple nous allons d'abord récupérer la clé de chaque élément du tableau dans une variable nommée $p, cette clé correspondant à tous les ports exposés par votre conteneur , ensuite nous allons aussi récupérer la valeur de chaque clé dans une variable nommée $conf :

docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{println "clé :" $p "=>" " | valeur :" $conf}}{{end}}' ubuntuc

Résultat :

clé : 8000/tcp =>  | valeur : [{0.0.0.0 9000}]
clé : 8001/tcp =>  | valeur : [{0.0.0.0 9001}]
clé : 8002/tcp =>  | valeur : [{0.0.0.0 9002}]

Maintenant gardons la variable $p et essayons de ne récupérer que la deuxième valeur de la variable $conf correspondant au port cible mapper :

docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{println "Port exposé :" $p " | Port cible :" (index $conf 0).HostPort}}{{end}}' ubuntuc

Résultat :

Port exposé : 8000/tcp  | Port cible : 9000
Port exposé : 8001/tcp  | Port cible : 9001
Port exposé : 8002/tcp  | Port cible : 9002

la commande logs

Il y a des risques que votre conteneur soit constamment à l'état RESTART. Dans ce cas il est important de vérifier les logs de votre conteneur.

Pour nos tests, nous allons construire une image ou j'ai rajouté exprès une erreur :

FROM alpine:latest

RUN apk add --no-cache apache2

EXPOSE 80

ENTRYPOINT /usr/sbin/http -DFOREGROUND

Buildons ensuite notre image :

docker build -t alpineerror .

Démarrons subséquemment notre conteneur avec les options suivantes :

docker run -d --restart always --name alpineerrorc -p 80:80 alpineerror

Si on vérifie l'état de notre conteneur, on constatera alors qu'il essaiera toujours de redémarrer mais sans aucun succès :

docker ps

Résultat :

CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                           PORTS               NAMES
31e4baf228c8        alpineerror         "/bin/sh -c '/usr/sb…"   About a minute ago   Restarting (127) 3 seconds ago                       alpineerror

Vérifions ensuite les logs du conteneur afin de trouver la source du problème :

docker logs alpineerrorc

Résultat :

/bin/sh: /usr/sbin/http: not found

Les logs nous indiquent clairement que le chemin de la commande est introuvable. Pour corriger cette erreur il suffit juste de remplacer /usr/sbin/http par /usr/sbin/httpd dans votre Dockerfile.

la commande history

Dans certains cas il est nécessaire de diagnostiquer la construction de votre image en vue de l'optimiser, voire la réparer. Dans ce cas la commande Docker history vous sera d'une grande aide car elle vous indiquera les couches individuelles qui constituent votre image, ainsi que les commandes qui les ont créées, leur taille, et leur temps d'exécution.

Pour utiliser cette commande, nous allons d'abord construire une nouvelle image :

Dockerfile :

FROM alpine:latest
RUN apk add --no-cache git
RUN apk add --no-cache mysql-client

Buildons ensuite notre image :

docker build -t myalpine .

Exécutons maintenant la commande Docker History :

docker history myalpine

Résultat :

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
e0fee090997d        4 seconds ago       /bin/sh -c apk add --no-cache mysql-client      35.5MB              
0c9541183535        7 seconds ago       /bin/sh -c apk add --no-cache git               15.6MB              
b7b28af77ffe        18 hours ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
alt;missing>        18 hours ago        /bin/sh -c #(nop) ADD file:0eb5ea35741d23fe3…   5.58MB 

Par défaut vous n'avez pas d'horodatage de la création de l'image, cette information peut vous être utile si vous souhaitez éxaminer le temps d'éxécution de chaque couche de votre image. Pour afficher cette information, il faut une nouvelle fois utiliser l'option --format avec le mot reservé .CreatedAt :

Avant de l'utiliser, laissez-moi d'abord vous décrire les différents mots résérvés de l'option --format pour la commande Docker history. Par la même occasion ça vous permettra d'en savoir davantage sur chaque colonne retournée par la table de cette commande :

  • .ID : ID de la couche de l'image.
  • .CreatedSince : Temps écoulé depuis la création de la couche de l'image.
  • .CreatedAt : Horodatage de la création de la couche de l'image.
  • .CreatedBy : Commande utilisée pour créer la couche de l'image.
  • .Size : Taille du disque de la couche de l'image.
  • .Comment : Commentaire de la couche de l'image.

Dans l'exemple ci-dessous nous allons dans un premier temps afficher la commande utilisée par nos différentes couches de l'image myalpine, et dans un second temps révéler l'horodatage de création pour chaque couche de cette même image.

docker history -H --format="table {{.ID}}\t{{.CreatedBy}}\t{{.CreatedAt}}" myalpine

Résultat :

IMAGE               CREATED BY                                      CREATED AT
e0fee090997d        /bin/sh -c apk add --no-cache mysql-client      2019-07-12T18:14:46+02:00
0c9541183535        /bin/sh -c apk add --no-cache git               2019-07-12T18:14:43+02:00
b7b28af77ffe        /bin/sh -c #(nop)  CMD ["/bin/sh"]              2019-07-12T00:20:52+02:00
&tl;missing>        /bin/sh -c #(nop) ADD file:0eb5ea35741d23fe3…   2019-07-12T00:20:52+02:00

Exercice

Énoncé

Il est temps de pratiquer un peu ! Je vous ai préparé un petit exercice qui reprend la base de tout ce qu'on a pu étudier depuis le début de ce chapitre.

Le but de cet exercice est de créer un script qui affiche la configuration réseau basique et les ports mappés de tous les conteneurs de votre machine locale (peu importe leur état).

Voici un aperçu du résultat final :

ubuntuc :
    IP : 172.17.0.3/16
    MacAddress : 02:42:ac:11:00:03
    Gateway : 172.17.0.1
    Ports : 
      - 8000:9000
      - 8001:9001
      - 8002:9002

httpdc :
    IP : 172.17.0.2/16
    MacAddress : 02:42:ac:11:00:02
    Gateway : 172.17.0.1
    Ports : 
      - 80:80

Vous pouvez utiliser n'importe quel langage de programmation. En ce qui me concerne, j'ai utilisé un script bash.

Solution

J'espère que vous avez réussi à réaliser ce tp ! Je vous présente ici ma solution. Bien sûr, je ne détiens pas la meilleure solution donc n'hésitez à partager votre code dans les commentaires ou sur le serveur discord.

#!/bin/bash

# Récupération des noms des conteneurs
containers=$(docker ps -a --format={{.Names}})

for container in $containers
do
    # Récupération des informations du conteneur
    IP=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' $container)
    IPp=$(docker inspect --format='{{.NetworkSettings.IPPrefixLen}}' $container)
    MACADDR=$(docker inspect --format='{{.NetworkSettings.MacAddress}}' $container)
    GATEWAY=$(docker inspect --format='{{.NetworkSettings.Gateway}}' $container)
    PORTS=$(docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{println "\t  -" $p ":" (index $conf 0).HostPort}}{{end}}' $container | sed -e 's/ : /:/')


    # Affichage des informations du conteneur
    echo -e "$container :"
    echo -e "\tIP : $IP/$IPp"
    echo -e "\tMacAddress : $MACADDR"
    echo -e "\tGateway : $GATEWAY"
    echo -e "\tPorts : \n${PORTS//\/tcp/}\n"
done

Conclusion

Il est temps de conclure ce chapitre. Dans les chapitres précédents, nous avions vu comment déboguer quelques objets docker comme les volumes et les réseaux Docker, mais cette fois-ci nous nous sommes plus concentré sur le débogage des conteneurs et images Docker.

Comme toujours, je vous partage un pense-bête des commandes que nous avons pu voir dans ce chapitre :

## Récupérer des informations de bas niveau d'un conteneur ou d'une image
docker inspect <CONTAINER_ID ou CONTAINER_NAME ou IMAGE_NAME ou IMAGE_ID>
    -f ou --format : formater le résultat



## Afficher en temps réels les statistiques des différentes
## ressources consommées par votre conteneur en mode streaming  
docker stats <CONTAINER_ID ou CONTAINER_NAME>
    -f ou --format : formater le résultat
    --no-stream : désactiver le mode streaming


## Visualiser des informations sur les différentes couche de votre image
docker history <IMAGE_NAME ou IMAGE_ID>
    -f ou --format : formater le résultat


## Examiner les logs d'un conteneur
docker logs <CONTAINER_ID ou CONTAINER_NAME>
    -f : suivre en permanence les logs du conteneur
    -t : afficher la date et l'heure de la réception de la ligne de log
    --tail <NOMBRE DE LIGNE> = nombre de lignes à afficher à partir de la fin (par défaut "all")

Espace commentaire

Écrire un commentaires

vous devez être connecté pour poster un message !

3 commentaires

utilisateur sans photo de profile

@tubie2b

Merci pour le contenu tu es le best :)

@yuko Merci beaucoup !

utilisateur sans photo de profile

@yuko

Bonjour

il manque un " l " à la fin de la commande :

docker inspect --format='{{json .NetworkSettings.Ports}}' ubuntuc | python3 -m json.too

encore bravo pour les tutos

D'autres articles

Rejoindre la communauté

Recevoir les derniers articles gratuitement en créant un compte !

S'inscrire