Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentes Révision précédente
Prochaine révision
Révision précédente
technique:docker:general:tips [2020/09/30 20:30] qduchemitechnique:docker:general:tips [2020/10/13 18:53] (Version actuelle) – [Spécifier un utilisateur lors de l'exécution de commandes] qduchemi
Ligne 1: Ligne 1:
-===== Bonnes pratiques pour les Dockerfiles =====+{{indexmenu_n>30}} 
 +Bonnes pratiques pour les Dockerfiles
  
 On pourra se référer à la [documentation officielle sur les bonnes pratiques](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/). On pourra se référer à la [documentation officielle sur les bonnes pratiques](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/).
-==== Générales ==== 
-=== Import d'images stables === 
-Lors de l'import d'une image dans un Dockerfile à l'aide de la directive ''FROM'', il est important de spécifier la version de l'image importée ; sinon la dernière version (//latest//) est importée, ce qui peut poser des problèmes de stabilité. 
  
-=== Exécution de commandes dans un conteneur === +## Import d'images stables 
-Auparavant, l'exécution de commandes dans un conteneur était réalisée par l'utilisation de [[http://supervisord.org/|supervisord]] ; cette pratique ne correspond plus aux pratiques employées actuellement par l'association. L'exécution de commandes dans un conteneur est désormais réalisée par l'utilisation de l'instruction ''CMD'' associé à un ''ENTRYPOINT'' si nécessaire dans le Dockerfile du conteneur.+ 
 +Lors de l'import d'une image dans un `Dockerfile` à l'aide de la directive ''FROM''il est important de spécifier la version de l'image importée ; sinon la dernière version (//latest//) est importée, ce qui peut poser des problèmes de stabilité. 
 + 
 +## Réaliser des images minimales 
 + 
 +Chaque élément d'une image peut être un facteur de vulnérabilité ; même un paquet en apparence inoffensif peut dépendre d'une librairie contenant une vulnérabilité. Pour cette raison, il convient de réaliser des images aussi légères que possibles en évitant d'installer des paquets superflus ou en supprimant les paquets installés pour un besoin temporaire (par exemple pour compiler un programme) une fois qu'ils sont obsolètes. 
 + 
 +<bootnote> 
 +Préférez vous baser sur une image Alpine (peu de paquets, donc peu de vulnérabilités) ou Debian. 
 +Pour Debian, préférez la version stable la plus récente possible (Buster plutôt que Stretch) pour mitiger d'emblée un grand nombre de vulnérabilités. 
 +</bootnote> 
 + 
 +Préférez également les versions `slim`, qui contiennent moins de paquets et suffisent souvent. 
 + 
 +Pour les services basés sur un langage interprété comme Python, utilisez l'image officielle en appliquant les principes précédents. 
 + 
 +<bootnote> 
 +Il peut être intéressant d'avoir recours à des [[https://docs.docker.com/develop/develop-images/multistage-build/|multi-stage builds]]. L'idée est par exemple d'utiliser une image pour la compilation, et une image qui recevra uniquement l'exécutable et les dépendances nécessaires à l'exécutionCette dernière sera utilisée en production. 
 +</bootnote> 
 + 
 +## Rendre les build reproductibles 
 + 
 +L'idée derrière un build "reproductible", c'est que si je lance le build d'un même `Dockerfile` à deux moments différents, l'image finale doit être la même quel que soit le temps écoulé entre les deux.  
 + 
 +<bootnote warning>Ce ne sont pas de **vrais** builds reproductibles, car la version des paquets installés peut avoir changé, par exemple. Mais l'idée est d'essayer de s'en rapprocher.</bootnote> 
 + 
 +<bootnote question>Pourquoi des builds reproductibles ?  
 + 
 +Pour pouvoir relancer le build d'une ancienne version et la remettre en production en cas de problème. Si on a l'ancien `Dockerfile`, alors on devrait pouvoir avoir quasiment la même ancienne image. 
 +</bootnote> 
 + 
 +Souvent, un `Dockerfile` va récupérer du code sur un dépôt Git, ou encore un binaire sur un site de téléchargement de releases. 
 +Il est fortement déconseillé de faire un `git clone` ou un `wget` de la dernière version (`latest`, `master`...), ce qui rend le build non reproductible et dépendant de cette dernière version. 
 + 
 +Il est donc important de gérer la version du service en question, par exemple avec une variable `ARG SERVICE_VERSION=1.0.1` qui sera ré-utilisée dans l'URL de téléchargement. 
 + 
 +En outre il existe deux solutions pour récupérer du code existant, versionné sur un dépôt Git distant : 
 + 
 +* Utiliser un `wget` sur une release particulière (Gitlab, Github), permettant de récupérer une archive contenant le code source d'une version spécifique. 
 +* Installer Git dans le `Dockerfile`, utiliser un `git clone` puis un `git checkout <tag>` sur la version souhaitée et copier le code dans l'image. 
 + 
 +<bootnote>En général, on préférera télécharger une release du code que d'utiliser Git.</bootnote> 
 + 
 +## Exécution de commandes au démarrage 
 + 
 +Pour lancer directement le binaire d'un service au démarrage, on utilisera la directive [CMD](https://docs.docker.com/engine/reference/builder/#cmd) avec la forme tableau (`[ "commande", "arg1", "arg2" ]`). En effet, cette forme permet de démarrer le processus directement, tandis que la forme sans tableau est exécutée par `/bin/sh`, ce qui pose des problèmes ([explications ici](https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html)). 
 + 
 +Lorsque l'on a besoin d'exécuter des instructions préalables (injection de secrets, test du contact avec la base de données, etc...), on utilisera la directive [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) en conjonction avec `CMD`, toujours sous la forme tableau. 
 + 
 +En général, l'`entrypoint` sera un script shell, qui recevra en argument le tableau passé à `CMD`. 
 + 
 +## Installation de paquets par ordre alphabétique
  
-=== Installation de paquets par ordre alphabétique === 
 Pour éviter d'installer deux fois un paquet, lors de l'installation de plusieurs paquets en une instruction, il convient de les installer dans l'ordre alphabétique. Pour éviter d'installer deux fois un paquet, lors de l'installation de plusieurs paquets en une instruction, il convient de les installer dans l'ordre alphabétique.
  
-=== Réduction du nombre de couches === +## Réduction du nombre de couches 
-Un Dockerfile est composé de différentes couches. Une couche correspond à une instruction exécutée par le Dockerfile. Lorsqu'un conteneur est lancé, il va accéder aux commandes du conteneur en remontant les différents niveaux. À la manière de l'héritage en programmation, si un conteneur cherche a appeler une commande qu'il ne connaît pas, il va remonter à sa classe mère et ainsi de suite. Or, ces accès réduisent la vitesse d'exécution de la commande. Il faut donc veiller à réduire au maximum le nombre de couches d'un conteneur et privilégier les instructions avec plusieurs commandes: \\ + 
-<code bash+Un `Dockerfileest composé de différentes couches (*layers*). Une couche correspond à une instruction exécutée par le `Dockerfile` 
-# Exemple:+ 
 +Lorsqu'un conteneur est lancé, il va accéder aux commandes du conteneur en remontant les différents niveaux. À la manière de l'héritage en programmation, si un conteneur cherche a appeler une commande qu'il ne connaît pas, il va remonter à sa classe mère et ainsi de suite. Or, ces accès réduisent la vitesse d'exécution de la commande. Il faut donc veiller à réduire au maximum le nombre de couches d'un conteneur et privilégier les instructions avec plusieurs commandes
 + 
 +<bootnote>Les couches ont pour intérêt de pouvoir être partagées entre les conteneurs et utilisent un système de cache. Ainsi, la modification d'une instruction n'influencera pas les couches précédentes, qui n'auront pas besoin d'être reconstruites. Il faut donc trouver le juste milieu entre trop de couches et pas assez de couches.</bootnote
 + 
 +En général, une couche est une sorte d'unité sémantique. L'exemple qui suit décrit l'installation de dépendances, qui n'ont pas lieu d'être séparées en trois instructions : 
 + 
 +```bash
 RUN echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list && \ RUN echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list && \
 wget -O- https://www.dotdeb.org/dotdeb.gpg | apt-key add - && \ wget -O- https://www.dotdeb.org/dotdeb.gpg | apt-key add - && \
 apt-get update -y && apt-get install -y php7.0 php7.0-fpm php7.0-gd php7.0-xml nginx supervisor curl tar apt-get update -y && apt-get install -y php7.0 php7.0-fpm php7.0-gd php7.0-xml nginx supervisor curl tar
-</code> +```
-\\+
  
-=== Passage de paramètres à un conteneur === +<bootnote>Dans cet exemple, il n'y a qu'une seule instruction `RUN`, donc une seule couche est créée. Par contre, la moindre modification de cette instruction entraînera son ré-exécution complète lors de la reconstruction de l'image.</bootnote>
-Les images Docker de l'association comportent une instruction ''ENTRYPOINT'' qui permet de lancer un script à l'instanciation de l'image ; le script ''entrypoint.sh'' associé à chaque conteneur initialise dans son conteneur les variables d'environnement qui sont nécessaires.+
  
-=== Contrôle de la santé d'un conteneur === +## Contrôle de la santé d'un conteneur
-Afin de faciliter la supervision de l'état d'un conteneur, il convient d'inclure dans les images Docker une instruction ''HEALTHCHECK'' contenant une commande permettant de vérifier la santé du conteneur.+
  
-==== Sécurité ==== +Les `HEALTHCHECK`, au sens Docker, permettent de vérifier qu'un conteneur est en "bonne santé", par exemple en lançant une commande de type `curl`
-=== Options lors de l'instanciation d'images === +
-Certaines options permettent de désactiver des fonctionnalités importantes d'isolation fournies par Docker au lancement d'un conteneur. Ces options ne doivent pas être utilisées. Elles comportent :  +
-  * ''--privileged'' : donne toutes les //capabilities// et lui permet de dépasser les limites qui lui seraient normalement imposées par son //cgroup// ; cela expose l'hôte à des attaques de type déni de service par l'absence de limitation sur la consommation de ressources et cela donne accès au conteneur à des capabilities dont il n'a pas nécessairement besoin et qui sont vecteurs de risques +
-  * ''--security-opt'' : désactive l'utilisation de seccompseccomp est une fonctionnalité du noyau Linux permettant de limiter le nombre d'appels systèmes disponibles à un conteneur ; la désactivation de ces limiations met donc l'hôte à risque+
  
-=== Montage de volumes === +<bootnote warning>Souventles commandes comme `curl` et `wget` ne sont pas installées, il faut y penser.</bootnote>
-Lors de la rédaction d'imageson peut être tenté d'exposer des volumes sur des images "temporaires". Par exemple, un conteneur nginx auquel on va exposer le volume ''/var/www/html''. Le problème de cette pratique est que si l'on veut créer une image dokuwiki basée sur cette image nginx, il ne sera pas possible d'écrire par dessus le volume ''/var/www/html''. Même si au sein du Dockerfile on spécifie des commandes pour remplacer le contenu de l'image, au final, le contenu du dossier restera celui de l'image mère.  +
-Il convient également de ne pas monter de volume sensible (par exemple ''/etc'') dans un conteneur, parce que les conteneurs peuvent par défaut modifier les volumes montés avec des privilèges ''root''. Ainsi, il ne faut surtout pas monter la socket Docker (''/var/run/docker/sock'') dans un conteneur.+
  
-=== Présence d'informations confidentielles dans un Dockerfile ou un docker-compose.yml === +<bootnote web>Référence pour les `HEALTHCHECK` : https://docs.docker.com/engine/reference/builder/#healthcheck</bootnote>
-Il convient de ne pas saisir d'informations confidentielles, telles que des mots de passe ou des clés SSH, dans des fichiers tels que des Dockerfiles ou ''docker-compose.yml'' ; ces fichiers sont souvent versionnés et parfois moins protégés. Pour ces raisons, il convient pour passer des paramètres tels que des mots de passe à un conteneur d'utiliser des variables d'environnement comme mentionné plus haut.+
  
-=== Spécifier un utilisateur lors de l'exécution de commandes === +## Ne pas utiliser les directives VOLUME
-Dans un Dockerfile, une commande exécutée par une directive ''RUN'' est exécutée par le compte root du conteneur par défaut. Il convient d'exécuter aussi peu de commandes que possible avec le compte root ; pour se faire, il est possible d'utiliser la directive ''USER''.+
  
-=== Réaliser des images minimales === +Lors de la rédaction d'images, on peut être tenté d'exposer des volumes sur des images "temporaires". Par exemple, une image `nginx` dans laquelle on va indiquer que `/var/www/html` doit être persistent grâce à la directive `VOLUME`. 
-Chaque élément d'une image peut être un facteur de vulnérabilité ; même un paquet en apparence inoffensif peut dépendre d'une librairie contenant une vulnérabilité. Pour cette raison, il convient de réaliser des images aussi légères que possibles en évitant d'installer des paquets superflus ou en supprimant les paquets installés pour un besoin temporaire (par exemple pour compiler un programme) une fois qu'ils sont obsolètes. + 
-Pour ce dernier besoin, il peut être intéressant d'avoir recours à des [[https://docs.docker.com/develop/develop-images/multistage-build/|multi-stage builds]].+Le problème de cette pratique est que si l'on veut créer une image Dokuwiki basée sur cette image nginx, il ne sera pas possible d'écrire par dessus le volume ''/var/www/html''Même si au sein du `Dockerfile` on spécifie des commandes pour remplacer le contenu de l'image, au final, le contenu du dossier restera celui de l'image mère 
 + 
 +On évitera donc les directives `VOLUME` au sein des `Dockerfile`, et on spécifiera les volumes au lancement du conteneur.
  
 +## Spécifier un utilisateur lors de l'exécution de commandes
  
-=== Documenter les paramètres nécessaires === +Dans un `Dockerfile`, une commande exécutée par une directive ''RUN'' est exécutée par le compte root du conteneur par défaut. Il convient d'exécuter aussi peu de commandes que possible avec le compte root ; pour se faire, il est possible d'utiliser la directive ''USER''.
-Chaque Dockerfile peut nécessiter le passage de paramètres tels que des mots de passe ; lorsque c'est le cas, il convient de les documenter dans le point d'entrée du conteneur et de leur y assigner une valeur par défaut.+
  
-==== Références ==== +<bootnote important>Un conteneur lancé en tant que `root` pose des risques de sécurité en cas de compromission, bien plus que quand il est lancé en tant que simple utilisateurPour certains services, c'est difficile, mais il faut essayer de s'en rapprocher le plus possible.</bootnote>
-[[https://docs.docker.com/develop/develop-images/dockerfile_best-practices/|Best practices for writing Dockerfiles]] +
-[[https://www.sans.org/reading-room/whitepapers/auditing/checklist-audit-docker-containers-37437|Recommandations de SANS]] +
-[[https://docs.docker.com/engine/reference/builder/|Dockerfile reference]] +
-[[https://www.miscmag.com/misc-n95-references-de-larticle-docker-les-bons-reflexes-a-adopter/|Références du MISC 95]]+
  
 +La dernière directive `USER` utilisée correspondra à l'utilisateur qui exécutera les commandes quand le conteneur sera lancé. Il est préférable de démarrer le conteneur avec un utilisateur non-privilégié. On s'assurera qu'il a bien les droits sur les fichiers qu'il doit utiliser, par exemple avec des instructions `chown` lors de la construction de l'image.
  • technique/docker/general/tips.1601490621.txt.gz
  • de qduchemi