Aller au contenu

Tester une image avec Container Structure Test

Mise à jour :

Comment vérifier qu’une image de conteneur fonctionne comme attendu ? Une image Docker peut sembler correcte après son build, mais contient-elle bien les bons fichiers, les bonnes configurations et les bonnes permissions ? Est-elle sécurisée et optimisée ? C’est ici qu’intervient Container Structure Test, un outil développé par GoogleContainerTools.

Contrairement à Hadolint, qui analyse uniquement le Dockerfile, Container Structure Test effectue des tests post-build directement sur l’image. Il me permet de valider les métadonnées, les fichiers, les variables d’environnement et les commandes exécutées dans un conteneur.

Pourquoi tester une image de conteneur ?

Lorsque je crée une image de conteneur, je veux m’assurer qu’elle est fiable, sécurisée et optimisée. Tester une image de conteneur avec un outil comme Container Structure Test me permet d’éviter des erreurs courantes qui pourraient impacter le bon fonctionnement de mes applications.

Vérifier la conformité aux bonnes pratiques

Une image doit respecter certaines bonnes pratiques pour garantir sa maintenabilité et sa performance :

  • Éviter les images trop volumineuses : une image légère réduit les temps de build et de déploiement.
  • Utiliser un utilisateur non-root : exécuter des conteneurs avec un utilisateur privilégié est une faille de sécurité.
  • Limiter les permissions sur les fichiers sensibles : éviter qu’un fichier critique ne soit modifiable par accident.
  • S’assurer de la présence des fichiers requis : un conteneur doit contenir les fichiers et dépendances nécessaires à son bon fonctionnement.

Avec Container Structure Test, je peux écrire des tests automatisés pour vérifier que mon image respecte certaines de ces règles.

Détecter les erreurs avant le déploiement

Un conteneur mal conçu peut causer des erreurs imprévues en production :

  • Une dépendance manquante empêche l’application de démarrer.
  • Une variable d’environnement critique est absente.
  • Une configuration par défaut est mal définie.
  • Une mise à jour d’un package introduit une régression.

Avec des tests de structure, je peux détecter ces problèmes avant même de lancer un conteneur, évitant ainsi des déploiements défectueux.

Renforcer la sécurité des images Docker

Un conteneur peut contenir des fichiers sensibles (clés API, certificats, variables d’environnement) qui ne devraient jamais être accessibles publiquement. De plus, certaines configurations non sécurisées peuvent exposer mon application à des attaques :

  • Ports inutiles ouverts
  • Permissions trop larges sur les fichiers
  • Exécution en tant que root

En testant mon image avec Container Structure Test, je peux vérifier automatiquement ces points et éviter les mauvaises surprises en production.

Assurer la reproductibilité des builds

Dans un pipeline CI/CD, les builds doivent être reproductibles. Si une modification dans le Dockerfile ou un package cassé change le comportement de mon image, je veux le détecter immédiatement. Un test de structure permet de s’assurer que :

  • L’image contient toujours les bonnes dépendances.
  • Les commandes essentielles fonctionnent correctement.
  • Les variables d’environnement attendues sont présentes.

Ainsi, en intégrant Container Structure Test dans mon workflow, je m’assure que mon image fonctionne de manière identique d’un environnement à l’autre.

Présentation de Container Structure Test

Container Structure Test est un outil développé par GoogleContainerTools permettant de tester la structure des images Docker. Il me permet de m’assurer que mes images sont correctes, sécurisées et conformes aux bonnes pratiques, en validant plusieurs aspects comme :

  • Les métadonnées de l’image (labels, user, entrypoint…)
  • Les fichiers présents dans l’image (permissions, existence, contenu…)
  • Les variables d’environnement
  • L’exécution de commandes à l’intérieur du conteneur

Contrairement à un simple linting de Dockerfile comme Hadolint, Container Structure Test teste l’image une fois qu’elle est construite, ce qui me permet de vérifier réellement son contenu et son comportement.

Fonctionnement

L’outil fonctionne à partir d’un fichier de test écrit en YAML. Ce fichier contient des règles définissant les tests que je veux exécuter. Ensuite, j’utilise Container Structure Test pour :

  1. Charger mon image Docker (locale ou depuis un registre)
  2. Exécuter les tests définis
  3. Fournir un rapport détaillé des résultats

Installation et configuration de Container Structure Test

Avant de pouvoir tester mes images Docker, je dois d’abord installer Container Structure Test sur mon système. Cet outil est disponible sous forme de binaire précompilé, ce qui simplifie son installation.

Prérequis :

Avant d’installer Container Structure Test, je dois m’assurer que :

  • Un runtime de container est installé et fonctionne sur ma machine
  • J’ai un accès à un terminal (Linux/macOS) ou PowerShell (Windows)

Installation de container-structure-test

L’installation est simple : je télécharge le binaire et je le place dans un dossier accessible depuis le terminal.

Installation sous Linux

Terminal window
curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && mkdir -p $HOME/bin && export PATH=$PATH:$HOME/bin && mv container-structure-test-linux-amd64 $HOME/bin/container-structure-test

Installation sous macOS

Pour macOS, on peut utiliser brew ou télécharger le binaire directement :

Terminal window
brew install container-structure-test

Ou

Terminal window
curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-darwin-arm64 && chmod +x container-structure-test-darwin-arm64 && sudo mv container-structure-test-darwin-arm64 /usr/local/bin/container-structure-test

Vérification de l’installation

Je peux ensuite vérifier l’installation avec :

Terminal window
container-structure-test version
1.19.3

Maintenant que Container Structure Test est installé, je peux commencer à rédiger mon premier test pour vérifier la structure d’une image Docker.

Rédiger un test pour une image

Maintenant que Container Structure Test est installé, je vais apprendre à écrire un fichier de test pour analyser la structure de mon image Docker. L’outil utilise un fichier YAML dans lequel je peux définir différents types de tests.

Structure d’un fichier de test

Un fichier Container Structure Test suit une structure simple en YAML. Il est structuré en plusieurs sections, chacune définissant un type de test spécifique. Voici un exemple de fichier de test complet :

schemaVersion: '2.0.0'
metadataTest:
env:
- key: "NODE_ENV"
value: "production"
labels:
- key: "maintainer"
value: "devops@example.com"
exposedPorts: ["8080", "2345"]
volumes: ["/test"]
entrypoint: []
cmd: ["/bin/bash"]
workdir: "/app"
user: "bob"
fileExistenceTests:
- name: "Vérifier la présence du fichier entrypoint"
path: "/app/entrypoint.sh"
shouldExist: true
permissions: "-rwxr-xr-x"
fileContentTests:
- name: "Vérifier la configuration du serveur"
path: "/etc/nginx/nginx.conf"
expectedContents:
- "worker_processes auto;"
- "include /etc/nginx/sites-enabled/*;"
commandTests:
- name: "Vérifier la version de Node.js"
command: "node"
args: ["--version"]
expectedOutput: ["v16.13.0"]
exitCode: 0
setup: [["apt-get", "update"], ["apt-get", "install", "-y", "nodejs"]]
teardown: [["apt-get", "remove", "-y", "nodejs"]]

Je vais maintenant détailler les types de tests disponibles.

Tests de métadonnées

metadataTest:
env:
- key: "NODE_ENV"
value: "production"
labels:
- key: "maintainer"
value: "devops@example.com"
exposedPorts: ["8080", "2345"]
volumes: ["/test"]
entrypoint: []
cmd: ["/bin/bash"]
workdir: "/app"
user: "bob"

Cet exemple définit un test de métadonnées pour une image Docker à l’aide de Container Structure Test. Il vérifie plusieurs aspects clés du conteneur, notamment les variables d’environnement, les labels, les ports exposés, les volumes, le point d’entrée, la commande par défaut, le répertoire de travail et l’utilisateur.

Variables d’environnement

envVars:
- key: foo
value: baz

Ce test vérifie que la variable d’environnement foo est bien définie à baz dans l’image Docker.

Labels de l’image

labels:
- key: 'com.example.vendor'
value: 'ACME Incorporated'
- key: 'build-date'
value: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}$'
isRegex: true
  • Le label com.example.vendor doit être défini avec la valeur ACME Incorporated.
  • Le label build-date doit correspondre à un format de date ISO 8601 précis (AAAA-MM-JJTHH:MM:SS.ssssss). L’option isRegex: true indique que la valeur est une expression régulière, permettant de vérifier un format plutôt qu’une valeur exacte.

Ports exposés

exposedPorts: ["8080", "2345"]

Ce test vérifie que l’image expose bien les ports 8080 et 2345.

Volumes déclarés

volumes: ["/test"]

L’image doit déclarer un volume montable à l’emplacement /test.

Point d’entrée (ENTRYPOINT)

entrypoint: []

Ce test s’assure que l’image n’a pas de point d’entrée défini (ENTRYPOINT est vide dans le Dockerfile).

Commande par défaut (CMD)

cmd: ["/bin/bash"]

L’image doit exécuter /bin/bash par défaut si aucune autre commande n’est spécifiée lors du lancement du conteneur.

Répertoire de travail (WORKDIR)

workdir: "/app"

Ce test vérifie que le répertoire de travail par défaut de l’image est bien /app.

Utilisateur par défaut

user: "bob"

L’image doit être exécutée sous l’utilisateur bob, et non sous root.

Tests de présence de fichiers (fileExistenceTests)

Exemple :

fileExistenceTests:
- name: "Vérifier la présence du fichier entrypoint"
path: "/app/entrypoint.sh"
shouldExist: true
permissions: "-rwxr-xr-x"

Explication :

  • name : Nom du test (facultatif, mais recommandé pour identifier facilement les tests).
  • path : Chemin absolu du fichier ou répertoire à tester dans l’image.
  • shouldExist : Si true, le fichier doit être présent. Si false, il ne doit pas exister.
  • permissions : Vérifie les permissions du fichier sous format Unix (rwx pour lecture/écriture/exécution).

Pourquoi c’est utile ?

Ce type de test garantit que les fichiers critiques sont bien présents dans l’image (comme un script d’entrée ou un fichier de configuration). Il permet aussi de s’assurer que des fichiers sensibles ne sont pas accidentellement inclus.

Tests de contenu de fichiers (fileContentTests)**

Exemple :

fileContentTests:
- name: "Vérifier la configuration du serveur"
path: "/etc/nginx/nginx.conf"
expectedContents:
- "worker_processes auto;"
- "include /etc/nginx/sites-enabled/*;"
excludedContents:
- "server_tokens on;"

Explication :

  • path : Chemin du fichier à analyser.
  • expectedContents : Liste des chaînes de texte qui doivent être présentes dans le fichier.
  • excludedContents : Liste des chaînes qui ne doivent pas apparaître dans le fichier.

Pourquoi c’est utile ?

Je peux vérifier que les bons paramètres sont bien configurés dans mes fichiers de configuration, et qu’aucune directive dangereuse (comme server_tokens on; en Nginx) n’est présente.

Tests d’exécution de commandes (commandTests)

Exemple :

commandTests:
- name: "Vérifier la version de Node.js"
setup: [["apt-get", "update"], ["apt-get", "install", "-y", "nodejs"]]
command: "node"
args: ["--version"]
expectedOutput: ["v16.13.0"]
excludedOutput: ["v14.*"]
exitCode: 0
teardown: [["apt-get", "remove", "-y", "nodejs"]]

Explication :

  • setup : Commandes à exécuter avant le test (exemple : installation de Node.js).
  • command : Commande principale à exécuter.
  • args : Arguments à passer à la commande.
  • expectedOutput : Liste des sorties attendues.
  • excludedOutput : Liste des sorties interdites.
  • exitCode : Code de sortie attendu (0 pour succès, autre valeur pour une erreur).
  • teardown : Commandes exécutées après le test (exemple : suppression de Node.js).

Pourquoi c’est utile ?

Je peux m’assurer que les logiciels installés dans l’image fonctionnent correctement et retournent la bonne version. Ce test est aussi utile pour vérifier qu’une commande critique ne produit pas d’erreurs.

Tests de licences (licenseTests)

Exemple :

licenseTests:
- debian: true
files: ["/foo/bar", "/baz/bat"]

Explication :

  • debian : Si true, vérifie la liste des licences fournies par Debian.
  • files : Liste de fichiers contenant des informations de licence à vérifier.

Pourquoi c’est utile ? Ce test garantit que toutes les licences présentes dans l’image sont autorisées et conformes aux exigences de l’organisation. Il est particulièrement utile pour s’assurer qu’aucune licence non conforme n’est introduite par des paquets tiers.

Variables d’environnement globales (globalEnvVars)

Exemple :

globalEnvVars:
- key: "VIRTUAL_ENV"
value: "/env"
- key: "PATH"
value: "/env/bin:$PATH"

Explication :

  • Définit des variables d’environnement qui seront accessibles à tous les tests.
  • Substitution Unix ($PATH) est supportée, ce qui permet d’étendre les valeurs dynamiquement.

Pourquoi c’est utile ? Ce test est particulièrement utile lorsque certaines commandes ou scripts nécessitent des variables d’environnement spécifiques pour fonctionner correctement.

Options supplémentaires pour l’exécution des tests (containerRunOptions)

Ces options permettent d’ajouter des paramètres spécifiques au conteneur de test, comme le choix de l’utilisateur, le montage de volumes, ou l’ajout de capacités Linux.

Exemple :

containerRunOptions:
user: "root" # Exécuter le conteneur en tant que root
privileged: true # Activer le mode privilégié (désactivé par défaut)
allocateTty: true # Allouer un pseudo-TTY (équivalent à -t)
envFile: path/to/.env # Charger des variables d’environnement depuis un fichier
envVars: # Passer des variables d’environnement spécifiques
- SECRET_KEY_FOO
- OTHER_SECRET_BAR
capabilities: # Ajouter des capacités Linux
- NET_BIND_SERVICE
bindMounts: # Monter des volumes (équivalent à --volume/-v)
- /etc/example/dir:/etc/dir

Explication des options :

  • user : Définit l’utilisateur sous lequel le test sera exécuté (root, nobody, etc.).
  • privileged : Active le mode privilégié (--privileged), ce qui donne plus de permissions au conteneur.
  • allocateTty : Permet d’allouer un pseudo-terminal (-t dans Docker).
  • envFile : Charge des variables d’environnement à partir d’un fichier (--env-file).
  • envVars : Liste de variables d’environnement à transmettre directement.
  • capabilities : Ajoute des capabilités Linux (--cap-add dans Docker).
  • bindMounts : Monte des volumes sur des emplacements spécifiques (-v dans Docker).

Pourquoi c’est utile ?

Ces options sont essentielles lorsque l’image nécessite un environnement spécifique pour être testée correctement. Par exemple, un conteneur qui manipule des fichiers système peut exiger des permissions élevées ou des volumes montés.

Mise en pratique avec une image Docker contenant Bandit

Je vais maintenant illustrer Container Structure Test avec un cas concret : tester une image Docker contenant Bandit, un outil permettant d’analyser du code Python à la recherche de failles de sécurité.

J’utilise cette image dans GitLab CI, ce qui signifie qu’elle doit être optimisée, sécurisée et conforme aux bonnes pratiques. Pour cela, j’ai mis en place un Dockerfile multi-stage, qui permet de limiter la taille de l’image finale en copiant uniquement l’environnement virtuel nécessaire.

Dockerfile utilisé :

FROM alpine:3.14.2 as builder
ENV PYROOT=/venv
ENV PYTHONUSERBASE=$PYROOT
WORKDIR /
COPY Pipfile* ./
RUN apk update && \
apk add --no-cache bc gcc libffi-dev musl-dev openssl-dev python3-dev py3-pip && \
pip3 install --no-cache-dir --no-compile pipenv && \
pipenv lock && \
PIP_USER=1 pipenv sync --system
FROM alpine:3.14.2 as default
RUN adduser -D user && \
apk add --no-cache py3-pip
COPY --from=builder /venv /venv
# Définition de l’environnement
ENV PATH="/venv/bin:$PATH"
ENV PYTHONPATH="/venv/lib/python3.9/site-packages/"
USER user
WORKDIR /src
ENTRYPOINT ["bandit", "-r"]
CMD ["--version"]

Ce Dockerfile :

  • Construit l’environnement virtuel dans un premier conteneur (builder)
  • Copie uniquement le strict nécessaire dans l’image finale (default)
  • Définit les variables d’environnement (PATH, PYTHONPATH)
  • Configure l’utilisateur non-root (user)
  • Spécifie l’ENTRYPOINT et la CMD

Tests à réaliser :

Avec Container Structure Test, je vais vérifier que mon image est conforme aux attentes :

  • Métadonnées :

    • PATH doit contenir /venv/bin
    • PYTHONPATH doit être défini correctement
    • L’utilisateur doit être user
    • WORKDIR doit être /src
    • ENTRYPOINT doit être ["bandit", "-r"]
    • CMD doit être ["--version"]
  • Commandes :

    • La commande bandit --version doit retourner la bonne version
    • La commande which bandit doit pointer vers /venv/bin/bandit
  • Fichiers :

    • /etc/os-release doit contenir la version correcte d’Alpine Linux
    • Un certificat spécifique ne doit pas être présent dans /etc/ssl/certs/my-cert.crt

Fichier de test Container Structure Test :

schemaVersion: '2.0.0'
metadataTest:
envVars:
- key: PATH
value: "/venv/bin.*"
isRegex: true
- key: PYTHONPATH
value: "/venv/lib/python3.9/site-packages/"
entrypoint: ["bandit", "-r"]
cmd: ["--version"]
workdir: "/src"
user: "user"
commandTests:
- name: "bandit version"
command: "bandit"
args: ["--version"]
expectedOutput: ["bandit 1.7.0"]
- name: "bandit path"
command: "which"
args: ["bandit"]
expectedOutput: ["/venv/bin/bandit"]
fileExistenceTests:
- name: "Certificat"
path: "/etc/ssl/certs/my-cert.crt"
shouldExist: false
fileContentTests:
- name: "Linux Version"
path: "/etc/os-release"
expectedContents:
- "VERSION_ID=3.14.2"
- "NAME=\"Alpine Linux\""

Explication des tests :

  • Tests de métadonnées

    • Vérifie que les variables d’environnement sont bien définies.
    • Vérifie le répertoire de travail, l’utilisateur, l’entrée de commande et la commande par défaut.
  • Tests de commandes

    • bandit --version doit afficher bandit 1.7.0.
    • which bandit doit retourner /venv/bin/bandit, garantissant que l’outil est bien installé dans le bon chemin.
  • Tests de fichiers

    • Le certificat /etc/ssl/certs/my-cert.crt ne doit pas exister, assurant qu’aucun certificat sensible n’a été embarqué par erreur.
    • La version du système Alpine Linux doit être correcte, en validant le contenu de /etc/os-release.

Exécution des tests :

Je peux exécuter mes tests avec la commande suivante :

Terminal window
container-structure-test test --image artefacts.robert.local/bandit:1.7.0 --config unit-test.yaml

Résultat des tests :

=======================================
====== Test file: unit-test.yaml ======
=======================================
=== RUN: Command Test: bandit version
--- PASS
duration: 463.749977ms
stdout: bandit 1.7.0
python version = 3.9.5 (default, May 12 2021, 20:44:22) [GCC 10.3.1 20210424]
=== RUN: Command Test: bandit path
--- PASS
duration: 325.957628ms
stdout: /venv/bin/bandit
=== RUN: File Content Test: Linux Version
--- PASS
duration: 0s
=== RUN: File Existence Test: Certificat
--- PASS
duration: 0s
=== RUN: Metadata Test
--- PASS
duration: 0s
=======================================
=============== RESULTS ===============
=======================================
Passes: 5
Failures: 0
Duration: 789.707605ms
Total tests: 5
PASS

Conclusion :

Les tests sont tous passés avec succès, ce qui signifie que l’image Docker est conforme aux attentes.

En intégrant Container Structure Test dans un pipeline CI/CD, je peux valider automatiquement mes images avant leur déploiement. Associé à Trivy (pour l’analyse des vulnérabilités), cet outil permet de garantir que l’image est sécurisée, fiable et optimisée avant d’être utilisée en production.

Conclusion

Tester la structure d’une image de conteneur est une étape essentielle pour garantir sa fiabilité, sa sécurité et sa conformité aux bonnes pratiques. Avec Container Structure Test, j’ai la possibilité d’automatiser ces vérifications et de m’assurer que mes images contiennent les bonnes configurations, les fichiers attendus et les commandes fonctionnelles avant leur déploiement.

Grâce à cet outil, je peux :

  • Valider les métadonnées : utilisateur, variables d’environnement, point d’entrée, répertoire de travail…
  • Vérifier les fichiers critiques : présence des fichiers nécessaires et absence d’éléments sensibles.
  • Tester l’exécution des commandes : s’assurer que l’image exécute correctement les outils installés.

En intégrant Container Structure Test à un pipeline CI/CD, je détecte rapidement les erreurs de configuration avant la mise en production, ce qui permet d’éviter des problèmes en environnement réel. Associé à un scanner de vulnérabilités comme Trivy, cet outil renforce la sécurité et l’intégrité de mes images Docker.

L’automatisation de ces contrôles me permet ainsi de garantir des conteneurs propres, optimisés et prêts à l’emploi, tout en réduisant les risques d’échecs et de failles de sécurité.