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 :
- Charger mon image Docker (locale ou depuis un registre)
- Exécuter les tests définis
- 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
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 :
brew install container-structure-test
Ou
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 :
container-structure-test version1.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. Sifalse
, 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 builderENV PYROOT=/venvENV PYTHONUSERBASE=$PYROOTWORKDIR /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-pipCOPY --from=builder /venv /venv
# Définition de l’environnementENV PATH="/venv/bin:$PATH"ENV PYTHONPATH="/venv/lib/python3.9/site-packages/"
USER userWORKDIR /srcENTRYPOINT ["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/binPYTHONPATH
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
- La commande
-
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
.
- Le certificat
Exécution des tests :
Je peux exécuter mes tests avec la commande suivante :
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--- PASSduration: 463.749977msstdout: 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--- PASSduration: 325.957628msstdout: /venv/bin/bandit
=== RUN: File Content Test: Linux Version--- PASSduration: 0s
=== RUN: File Existence Test: Certificat--- PASSduration: 0s
=== RUN: Metadata Test--- PASSduration: 0s
====================================================== RESULTS ======================================================Passes: 5Failures: 0Duration: 789.707605msTotal 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é.