Aller au contenu
Conteneurs & Orchestration medium

Tester une image avec Container Structure Test

23 min de lecture

Container Structure Test valide automatiquement la structure de vos images Docker : présence des fichiers, permissions, variables d’environnement et commandes. Développé par GoogleContainerTools, cet outil s’intègre dans vos pipelines CI/CD pour détecter les erreurs de configuration avant le déploiement. Contrairement à Hadolint qui analyse le Dockerfile, Container Structure Test teste l’image après sa construction pour vérifier son contenu réel.

  • Installer Container Structure Test (v1.22.1) sur Linux ou macOS
  • Écrire des tests YAML pour valider métadonnées, fichiers et commandes
  • Exécuter les tests et interpréter les résultats
  • Intégrer dans CI/CD (GitHub Actions, GitLab CI)
  • Générer des rapports JUnit pour vos pipelines

Une image de conteneur peut sembler correcte après son build, mais contient-elle les bons fichiers ? Les bonnes permissions ? Les bonnes configurations ?

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.

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.

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.

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.

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.

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

Section intitulée « 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)

Téléchargez le binaire v1.22.1 :

Fenêtre de terminal
curl -sLO https://github.com/GoogleContainerTools/container-structure-test/releases/download/v1.22.1/container-structure-test-linux-amd64
chmod +x container-structure-test-linux-amd64
sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test
Fenêtre de terminal
container-structure-test version
v1.22.1

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.

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.

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.

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:
- 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.
exposedPorts: ["8080", "2345"]

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

volumes: ["/test"]

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

entrypoint: []

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

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.

workdir: "/app"

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

user: "bob"

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

Tests de présence de fichiers (fileExistenceTests)

Section intitulée « 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.

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.

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.

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)

Section intitulée « 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)

Section intitulée « 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

Section intitulée « 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 :

Fenêtre de terminal
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.

Container Structure Test s’intègre facilement dans vos pipelines CI/CD. Depuis la version 1.22.0, l’option --output junit génère des rapports au format JUnit XML, compatibles avec la plupart des plateformes CI.

.github/workflows/test-image.yml
name: Test Container Image
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:test .
- name: Install container-structure-test
run: |
curl -sLO https://github.com/GoogleContainerTools/container-structure-test/releases/download/v1.22.1/container-structure-test-linux-amd64
chmod +x container-structure-test-linux-amd64
sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test
- name: Run structure tests
run: |
container-structure-test test \
--image my-app:test \
--config tests/structure-test.yaml \
--output junit > test-results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results.xml
OptionDescription
--output jsonSortie JSON (machine-readable)
--output junitSortie JUnit XML pour CI/CD
--test-report FILEÉcrit les résultats dans un fichier
--saveSauvegarde le tar de l’image localement
--pullForce le pull de l’image depuis le registre
  • Container Structure Test valide la structure des images Docker après leur construction, contrairement à Hadolint qui analyse le Dockerfile
  • Quatre types de tests : métadonnées, fichiers, contenu de fichiers et commandes
  • Le format de sortie JUnit XML (--output junit) facilite l’intégration CI/CD
  • Combinez avec Trivy pour une validation complète (structure + vulnérabilités)
  • Utilisez un fichier YAML par image ou par famille d’images pour une maintenance facile