13 min read

GdT-PyR 17: Introduction sur les conteneurs

Session du 11/01/2021: Introduction aux conteneurs. Ce post est un extrait de la fiche complète disponible à l’adresse suivante:

https://forgemia.inra.fr/gdtpyr/gdt_pyr/-/tree/main/GDT_PyR_17_Conteneurs

Concept

Un container est un processus ou un ensemble de processus isolés du reste du système. Tous les fichiers nécessaires à leur exécution sont fournis par une image distincte, ce qui signifie que les conteneurs Linux sont portables et fonctionnent de la même manière dans les environnements de développement, de test et de production. (source: redhat.com).

Les conteneurs ont deux états : repos et fonctionnement. Au repos, un conteneur est un fichier (ou un ensemble de fichiers) enregistré sur le disque. C’est ce qu’on appelle une image de conteneur ou un dépôt de conteneur. Lorsqu’on lance un conteneur, le moteur de conteneur décompresse les fichiers et les méta-données nécessaires, puis les transmet au noyau Linux. Le démarrage d’un conteneur est très similaire au démarrage d’un processus Linux normal et nécessite un appel d’API au noyau Linux. Une fois lancés, les conteneurs ne sont plus qu’un processus Linux.

Architecture des conteneurs - source: http://gb-virtualk.fr

Les conteneurs partagent le même noyau “hôte”. L’isolation entre les différents conteneurs s’appuie sur des fonctionnalités propres au Kernel linux, les namespaces et les cgroups. Les namespaces permettent de limiter ce qui est vu par le conteneur. Les cgroups permettent de limiter les ressources utilisées (RAM, CPU, réseau, disque). La runtime ou moteur de container (containerd, containerd-shim, runC pour Docker; liblxc pour lixc) permet de créer et de faire fonctionner le conteneur.

runtime runtime

VM & Conteneurs

virtualisation

La virtualisation et les containerisation sont complémentaires:

  • La virtualisation permet d’exécuter simultanément plusieurs machines virtuelles (invités) avec différents systèmes d’exploitation (Windows ou Linux) sur une même machine (hôte). Un hyperviseur gère et exécute des machines virtuelles (VM). Les ressources matérielles (RAM, CPU, disque) utilisées par les VMs sont isolées.
  • Les conteneurs partagent le même noyau de système d’exploitation et isolent les processus de l’application du reste du système. Les systèmes Linux ARM exécutent des conteneurs Linux ARM, les systèmes Linux x86 exécutent des conteneurs Linux x86 et les systèmes Windows x86 exécutent des conteneurs Windows x86, …. Les conteneurs sont extrêmement portables, mais ils doivent être compatibles avec l’OS du système sous-jacent.

Focus sur Docker

Introduction

Docker engine (source: docs.docker.com)

Le docker engine est une application client-serveur qui se compose:

  • d’un processus daemon (commande dockerd);
  • d’une API REST qui définit une interface permet d’interroger le daemon;
  • d’un client (commande docker) qui interroge cette API pour interagir avec le daemon docker.

Docker architecture (source: docs.docker.com)

Le daemon docker (dockerd) écoute les requêtes via l’API et gère les objets Docker (images, containers, réseau et stockage).

Le client docker (commande docker) permet d’envoyer des commandes au daemon (build, pull, run, …). La commande docker run permet par exemple de récupérer l’image (Ubuntu, Nginx, …) et instancier un container.

Le docker registry (Docker Hub) stocke les images Docker. C’est un registre public que tout le monde peut utiliser, et Docker est configuré pour rechercher des images sur Docker Hub par défaut. On peut aussi installer un registre privé.

Docker architecture (source: docs.docker.com)

Image

Une image Docker représente le système de fichiers, sans les processus. Elle contient tout ce que vous avez décidé d’y installer (Java, une base de donnée, un script que vous allez lancer, etc…), mais qui est dans un état inerte. Les images sont créées à partir de fichiers de configuration, nommés Dockerfile, qui décrivent exactement ce qui doit être installé sur le système. Une image de conteneur est compilée à partir de différentes couches sur une image de base ou image parente. Chaque commande dans le DockerFile crée une nouvelle couche dans l’image.

Containers

Un conteneur Linux est :

  • un RootFS;
  • une configuration Cgroups;
  • un ensemble de namespaces

Pour plus de sécurité et d’isolation les conteneurs :

  • s’appuient sur les mécanismes de sécurité existant sous Linux (Selinux, apparmor, secomp, capabilities, iptables, …)
  • utilisent une configuration réseau dédié (via bridge/overlay et veth)

source : https://indico.in2p3.fr/event/17124/contributions/61042/attachments/48554/61402/20180604-IN2P3-intro-conteneurs.pdf

Repository privé & publique

Les images de conteneurs sont stockées dans un registre privé ou public (comme le Docker Hub/ForgeMIA).

L’utilisation d’une image de qualité est un gage de sécurité. Docker propose d’utiliser uniquement des images certifiées. Pour être certifiée, une image doit passer différents tests sur l’API de Docker grâce à l’outil inspectDockerImage.

Source : https://docs.docker.com/docker-hub/publish/certify-images/

Sur le Docker Hub il existe également des images portant la mention “éditeur vérifié” ou “image officielle”. Cela ne garantit pas le contenu de l’image mais donne des garanties sur la provenance de l’image.

Docker permet également de signer une image grâce à l’outil notary.

Source: https://blog.octo.com/la-signature-dimages-docker-sur-une-registry-avec-notary/

Utilisation de Docker

Docker Ligne de commande

La commande docker permet d’interagir avec l’API Docker.

Commandes de base

docker search : rechercher une image
docker run : lancer un conteneur
docker stop : arrêter un conteneur
docker rm : supprimer un conteneur
docker ps : lister les conteneurs
docker pull : récupérer une image
docker push: déposer une image
docker info : infos générales
docker kill : tuer un container
docker rmi : supprimer une image

DockerFile

Les Dockerfiles sont des fichiers qui permettent de construire une image Docker adaptée à ses besoins, étape par étape.

Voici un exemple de DockerFile

Source: https://putaindecode.io/articles/les-dockerfiles/

Volumes

Les données d’un conteneur sont éphémères. Lorsque qu’un conteneur est supprimé son contenu l’est aussi. Afin de pouvoir sauvegarder (persister) les données et également partager des données entre conteneurs, Docker a développé le concept de volumes. Les volumes sont des répertoires (ou des fichiers) qui ne font pas partie du système de fichiers du container mais qui existent sur le système de fichiers hôte.

Commandes utiles:

docker volume create <volumename> : créer un volume
docker volume ls : lister les volumes
docker volume rm <volumename> : supprimer un volume

Docker compose

Une infrastructure informatique fait souvent appel à plusieurs composants. Par exemple, un site sous Wordpress va nécessiter 2 composants : un conteneur pour wordpress et un pour la base de données MySQL. Docker Compose permet d’orchestrer la gestion de plusieurs containers au sein d’un ensemble cohérent (stack). C’est un outil écrit en Python qui permet de décrire, dans un fichier YAML, plusieurs conteneurs comme un ensemble de services.

Voici un exemple de fichier YAML

Commandes utiles:

docker-compose up -d: permet de lancer l’ensemble des conteneurs en arrière plan
docker-compose ps : permet de voir l’état de l’ensemble de la stack
docker-compose stop : permet d’arrêter les conteneurs
docker-compose down : permet de supprimer les conteneurs
docker-compose config : permet de valider la config du fichier yaml
docker-compose scale <service name> = <no of instances> : permet de scaler un service (si compatible)

Sécurité

Les failles de sécurité peuvent affecter :

  • l’hôte (OS Linux en général);
  • le conteneur
  • le réseau

Voici un article très intéressant qui couvre le sujet : A Checklist for Audit ofDocker Containers

Vous trouverez ci-dessous quelques bonnes pratiques forcément non exhaustives.

Il existe des outils comme Docker Bench for Security qui permettent de tester sa configuration. Il existe un container disponible:

docker run --rm --net host --pid host --userns host --cap-add audit_control     -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST     -v /etc:/etc:ro     -v /usr/bin/containerd:/usr/bin/containerd:ro     -v /usr/bin/runc:/usr/bin/runc:ro     -v /usr/lib/systemd:/usr/lib/systemd:ro     -v /var/lib:/var/lib:ro     -v /var/run/docker.sock:/var/run/docker.sock:ro     --label docker_bench_security     docker/docker-bench-security

Voici quelques pistes pour sécuriser ces éléments.

Sécurisation de l’hôte

  1. appliquer les mises à jour de sécurité

Exemple sous CentOS:

sudo yum updateinfo list updates security
sudo yum update --security
  1. Il existe plusieurs outils open source comme Lynis ou OpenVAS qui permettent d’analyser le noyau Linux
git clone https://github.com/CISOfy/lynis.git
cd lynis; ./lynis audit system
  1. Installer Docker dans une VM afin de limiter l’accès au noyau de la machine hôte.

  2. Limiter les privilèges d’un conteneur

4.1 Par défaut, Docker nécessite des privilèges root pour créer et gérer des conteneurs. On peut démarrer les conteneurs avec un utilisateur qui dispose de moins de droits.

4.2 On peut aussi limiter les capacités qu’utilise Docker

C’est un mécanisme intégré au noyau Linux qui permet de scinder les privilèges traditionnellement associés au superutilisateur en unités distinctes que l’on peut activer ou désactiver individuellement. Les capacités sont des attributs individuels à chaque thread.

Par défaut Docker utilise les capabilities suivantes: chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap

Les options --cap-drop et --cap-add permettent de supprimer ou rajouter des capacités.

Par exemple la commande :

docker run -d --cap-drop SETGID --cap-drop SETUID apache

Permet de supprimer les capabilities SETGID et SETUID afin d’empêcher le container apache de changer son GID et son UID.

On peut aussi spécifier ces options dans le Docker file Source : https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick

  1. Limiter les ressources matérielles utilisées par un conteneur

On peut les ressources (cpus, gpus, mémoire) allouées à un conteneur.

Par exemple la commande:

docker run -d --name prodnginx --cpuset-cpus=0 --cpu-shares=2 nginx

Permet de lancer la conteneur nginx sur le premier cpu (option –cpuset-cpus)

La liste complète des ressources paramétrables est disponible dans la doc.

  1. monitorer consommation de ressources

Il existe énormément d’outils de monitoring compatibles avec Docker (datadog, portainer, …). Beaucoup sont payants mais il existe aussi des outils open source gratuits (Prometheus, Nagios, …)

Sécurisation du conteneur

La plupart des failles de sécurité sont liées à des images corrompues. Pour s’en prémunir on peut :

  1. regarder le contenu de l’image

Inspecter le dockerfile, les scripts et fichiers associés

docker inspect nginx

On peut aussi aller voir directement le contenu du Dockerfile depuis le DockerHub. Exemple pour l’image nginx 1.19.6 :

  1. Vérifier que l’image soit à jour et maintenue (voir les tags sur DockerHub et leurs dates de publication)

  2. Gérer les versions des images utilisées

Par défaut Docker télécharge la dernière image d’un conteneur mais on peut fixer la version que l’on souhaite récupérer en précisant le tag.

sudo docker run -t ubuntu:18.04

Ou dans le DockerFile : FROM ubuntu:18.04

  1. utiliser un registre privé

  2. n’utiliser que les images officielles

docker search --filter is-official=true nginx
  1. utiliser des images signées (DTC - Docker Trust Content) Positionner la variable d’environnement DOCKER_CONTENT_TRUST à 1 permet de n’utiliser que des images signées. Les commandes Docker compatibles avec DTC sont : push, build, create, pull, run.

Notary est l’outil permettant de gérer les signatures des images Docker. Vous pouvez installer un serveur Notary ou utiliser celui de Docker.

  1. utiliser un scanner d’images

Il existe des outils comme anchore-engine qui permettent de scanner les images Docker. Cet outil peut être combiné à un IDE comme Jenkins. Voir ce tuto.

  1. ne pas stocker les infos sensibles dans les images

Il est préférable de ne pas mettre ces informations sensibles comme les mots de passe dans le Dockerfile.

Pour cela privilégier on peut utiliser un fichier contenant les variables d’environnements et exclure ce fichier lors du dépôt ou bien utiliser les secrets Docker. Exemple de fichier docker-compose.yml à éviter :

version: '3.7'
services:
  mariadb:
    image: mariadb:10
    environment:
      MYSQL_ROOT_PASSWORD: PasswordRoot
      MYSQL_USER: MyUSER
      MYSQL_PASSWORD: MyUserPass

Pour cela on peut utiliser un fichier d’environnement :

version: '3.7'
services:
  mariadb:
    image: mariadb:10
    env_file:
        - .env_mariadb

Afin de ne pas stocker ce fichier dans le repository Docker on peut l’exclure en rajoutat un fichier .gitignore

cat .gitignore
.env
.env_mariadb

Cependant l’utilisation des variables d’environnement peut avoir des limites (partagées entre tous les conteneurs de l’hôte, difficile à échanger entre plusieurs machines).

Les secrets docker offrent un moyen sécurisé de stocker des informations sensibles (nom d’utilisateur, mots de passe, fichiers comme des certificats auto-signés ou clés SSH). Voir ces différentes exemples :

  1. limiter au maximum les paquets installés dans les containers

Exemple : supprimer le compilateur gcc une fois que l’application est compilée.

  1. vérifier les logs des conteneurs
docker logs <container-id>

Sécuriser le réseau

  1. restreindre les communications entre les containers sur l’interface bridge par défaut créé par Docker.

Par défaut, la communication réseau entre les conteneurs est autorisée. On peut la désactiver avec l’option icc=false. Il est alors nécessaire de rendre explicite la communication entre containers avec l’option –links (links dans DockerCompose).

  1. n’ouvrir que les ports utiles

L’option -p permet d’exposer un seul port contrairement à l’option -P qui expose tous les ports.

docker run -p 80:80 apache

On peut aussi filtrer sur un réseau :

docker run -p 192.168.0.1:9200:9200 elasticsearch
  1. créer un réseau dédié

Docker propose différents drivers réseaux. Avec le réseau de type bridge on peut faire communiquer les conteneurs entre eux tout en les rendant inaccessibles depuis l’extérieur.

Voir ce tuto : Fonctionnement et manipulation du réseau dans Docker.

Cas d’utilisation pour Docker

Développement

  • Travail en équipe

Je démarre un nouveau projet au sein d’une équipe qui utilise Docker ? J’installe Docker, je démarre le container avec la dernière version du code en cours de développement. Je suis sûr d’avoir les mêmes versions des outils que mes collègues, quel que soit l’OS sur mon poste de travail ! De plus, les montées de version sont facilitées pour toute l’équipe, et on est sûrs que tout le monde est à jour.

  • Veille technologique - Développement sur plusieurs projets

Je ne veux pas polluer mon poste de dev avec tous les outils que je télécharge pour plusieurs de mes projets. Problème : j’ai plusieurs versions des mêmes outils installées sur ma machine. J’ai par exemple deux versions de PHP et Apache d’installées. Si je change de projet, je dois en désactiver une et activer l’autre. Cela me fait perdre du temps et je ne suis jamais sûr d’avoir tout bien désinstallé si je dois en supprimer une. Solution: avoir des conteneurs qui possèdent mon environnement de travail pour chaque projet.

  • Multiplateforme

Je veux utiliser un outil sur Mac qui est développé uniquement pour Linux. Je peux directement l’utiliser sur mon Mac grâce à Docker.

  • Outil qui facilite l’intégration continue

J’ai un projet dans lequel j’intègre progressivement des fonctionnalités en équipe ou seul à un programme. Les utilisateurs de ce programme ont des environnements différents mais identifiés (version du langage, librairies, etc ..). Je peux créer une routine qui me permet de tester ces différents environnements. Je peux aussi effectuer des tests sur des futures versions de langages de programmation afin de prévoir les éventuels conflits.

Tester un package dans une image Docker

Description de l’image (Docker file)

ARG  R_VERSION=latest
 
FROM rocker/tidyverse:${R_VERSION}
 
ENV R_REPOS http://cloud.R-project.org/
 
COPY . /tmp/package
 
WORKDIR /tmp/package
 
RUN cd /tmp/package \
   && R -e 'options(repos =c(CRAN=Sys.getenv("R_REPOS")));install.packages("devtools");devtools::install_deps(build_vignettes=TRUE, dependencies = TRUE, upgrade = TRUE)'
Construction de l'image
docker build -t testr:4.0 --build-arg R_VERSION=4.0.2 https://github.com/nomProjet/nomPackage.git 

Successfully tagged testr4:4.0 Lancement du conteneur à partir de l’image

docker run -i -t --rm testr:4.0 /bin/bash  -c "R --version"
  • -t assigne un pseudo-tty ou un terminal à l’intérieur du nouveau conteneur
  • -i vous permet d’établir une connexion interactive en saisissant l’entrée standard (STDIN) du conteneur.
  • -rm supprime automatiquement le conteneur lorsque le processus se termine. Par défaut, les conteneurs ne sont pas supprimés. Ce conteneur existe jusqu’à ce que nous conservions la session shell et se termine lorsque nous quittons la session (comme une session SSH avec un serveur distant).

Exemple de test d’un package dans un container

docker run -i -t --rm testr /bin/bash  -c "R -e 'devtools::check(document = FALSE, args = \"--no-tests\", error_on = c(\"note\"))'"

Intégration continue : Mise en place de tests en utilisant des images Docker dans gitlab (ou github)

Il est possible à partir de la fonctionnalité Gitlab CI/CD d’exécuter du code en fonction d’actions qui seront réalisées automatiquement (commits, pull request) ou manuellement.

Dans l’exemple suivant ce package est testé avec le package devtools pour vérifier qu’il passe toutes les étapes de tests dans un ensemble d’environnements donnés.

Liens utiles