Consutruction des images Docker: quelles sont les bonnes pratiques ?

Créer ses propres images peut être d'une grand aide lorsque nous avons un besoin spécifique. Mais comment réaliser tout cela en conservant des bonnes pratiques? Voici quelques notions et des règles qu'il me semble essentielles de comprendre et de respecter.

Indiquer les méta-data sur l'images

L’instruction LABEL permet d’ajouter des informations de type clé-valeur. Ces informations permettent de tracer qui est le propriétaire de l’image, les outils utilisés ou encore les licences. Il convient donc de compléter d’ajouter au moins un champs indiquant le responsable de l’image. Cette personne (ou entité) sera en charge de mettre à jour l’image tout au long de la vie du projet :

LABEL equipe=""
LABEL responsable=""
LABEL license=""
LABEL description=""

L’instruction ne va pas modifier la manière dont l’image s’exécute, elle permettra uniquement de faciliter la manière de gérer les images dans le groupe. Les Labels peuvent être ajoutés automatiquement lors de la création de l’image dans une usine logicielle (CI/CD).

Utiliser des images minimales et officielles

L’image de base doit contenir un minimum de données afin de limiter la surface d’exposition lors d’une attaque. Ainsi, il convient d’utiliser tant que possible des images minimales avec uniquement le strict mimimum d’outils installés.

il est recommandé de choisir des images officielles depuis le DockerHub. Pour cela il est possible d’ajouter le tag official lors d’une recherche.

https://hub.docker.com/search?q=nginx&image_filter=official&type=image

Utiliser des images avec des versions spécifiques

Il est recommandé de ne pas utiliser le tag latest lors de la création d’une image. En effet, en cas de présence d’une vulnérabilité (CVE) sur l’image de base il est alors plus difficile d’identifier si l’image de base de vulnérable si elles utilisent le tag latest au lieu d’une version. Il convient alors d’utiliser une version précise pour chaque image. Par exemple ici la version 18.04 d’Ubuntu est utilisée :

FROM ubuntu:18.04

Dans le cas de la création d’une image, alors faudra pointer vers la dernière image publiée.

Exclure des fichiers du context

Il est possible d'exclure des fichiers non pertinents pour la génération de votre image et donc du context à l'aide d'un fichier .dockerignore. Ce fichier est très similaire au fichier .gitignore.

Installer le minimum de dépendances

Avec l’utilisation d’une image minimale, il est souvent nécessaire d’ajouter des dépendances avec yum ou apt. Il est recommandé d’installer uniquement les paquets nécessaires à l’application. De plus, il n’est pas recommandé de mettre à jour l’image; celle-ci doit être réalisée via l’utilisation d’une nouvelle version de l’image de base. Ainsi, les commandes apt-get upgrade et dist-upgrade ne sont pas recommandées.

En outre, il est recommandé de mettre à jour le cache des dépendances et l’installation dans la même commande :

RUN apt-get update && apt-get install -y \
       package-bar \
       package-baz \
       && rm -rf /var/lib/apt/lists/*

Cela va avoir deux avantages :
Eviter la création d’une nouvelle couche pour chaque commande
Forcer la mise à jour du cache et donc installer la dernière version des paquets

Utiliser le multi-stage et les cibles pour chaque environnement

Depuis la version 17.05 de Docker, il est possible de déclarer au sein d’un même fichier Dockerfile plusieurs étapes de construction de l’image, appelé multi-stage.

Cette technique permet de réduire drastiquement la taille de l’image du conteneur en évitant d’inclure les couches de construction intermédiaires. Il est alors possible de déposer dans une image intermédiaire les outils de construction de l’application, comme par exemple une clef SSH. Cette clef ne sera alors plus présente dans l’image finale.

FROM ubuntu:18.04 as intermediate
WORKDIR /app
# Ici la clef est copiée dans l'image intermediate
COPY secret/key /tmp/
# Elle est utilisée pour se connecter à un serveur distant
RUN scp -i /tmp/key build@server.com/files .

FROM ubuntu:18.04
WORKDIR /app
# Les fichiers sont ensuites copiés depuis l'image intermediate sans la clef SSH
COPY --from=intermediate /app .

Dans ce Dockerfile, l’image intermediate contient une clef SSH qui ne sera plus présente dans l’image finale. Cette technique peut être utilisée pour faciliter la construction et déploiement d’images en production sans inclure l’environnement de développement.

Trier vos arguments multi-lignes

Un exemple concret vous permettra de comprendre rapidement :

RUN apt-get update && apt-get install -y \
       bzr \
       cvs \
       git \
       mercurial \
       subversion

Lister les arguments multi-lignes par ordre alphanumérique vous permettra une relecture plus facile du fichier. Cela facilitera votre maintenance du fichier et permet également d'éviter la duplication des packages.

Minimiser le nombre de layers

Dans les anciennes versions de Docker, il était important de minimiser le nombre de couches des images pour s'assurer qu'elles restent performantes. Afin de faciliter notre tâche, Docker a introduit depuis plusieurs versions, des modifications au niveau des layers : Seules les instructions RUN, COPY, ADD créent des couches. Les autres instructions créent des images intermédiaires temporaires mais qui n'augmentent pas la taille de l'image finale.

Dans la mesure du possible, je vous conseille tout de même de limiter le nombre de couches générées afin d'éviter une taille de l'image trop excessive.

Appliquer le principe de moindre privilèges

Comme pour une application sur un serveur, il convient de créer un utilisateur dédié pour l’exécuter au sein du conteneur. La création d’un utilisateur peut dépendre de chaque OS, l’utilisation d’un utilisateur dédié requiert l’instruction USER.

Par exemple, pour une image Ubuntu, le fichier contient le code suivant :

RUN groupadd -r appuser && useradd -r -s /bin/false -g appuser appuser
RUN appuser

Après cette ligne les commandes seront exécutées avec l’utilisateur appuser qui n’est plus root et qui n’a donc plus les privilèges d'un super user

Utiliser l’instruction COPY plutôt que ADD

Les deux commandes, ADD et COPY sont similaires. Mais plus généralement, il est préférable d'utiliser COPY.

COPY permet uniquement la copie de fichiers stockés localement dans le container.

Alors que ADD possède bien plus d'options. Comme par exemple l'extraction de fichier tar ou la copie de fichiers depuis une URL.

L’utilisation de l’instruction ADD peut introduire les faiblesses suivantes :
Le téléchargement du fichier pointé par l’URL n’est pas forcément réalisé de manière sécurisé (interception SSL possible)
Extraction automatique de l’archive ce qui peut rendre possible des attaques de type zip-bomb

Il est donc recommandé d’utiliser uniquement l’instruction COPY et donc d’interdire l’utilisation de l’instruction ADD.