Conteneurs et Docker
La technique de « conteneurisation » a de nombreux avantages. Elle est notamment souvent utilisée dans le cadre de l’intégration continue pour sa capacité à définir un environnement reproductible d’un run à l’autre, quelle que soit la machine sur laquelle elle est exécutée. Cet article se propose d’exposer les intérêts et limites de cette technique dans le cas de Docker et de donner des bonnes pratiques de création d’images de conteneurs.
Conteneur ou machine virtuelle ?¶
La technologie de conteneurisation s’appuie sur les fonctionnalités du noyau linux (principalement les cgroups et les espaces de nom) pour fournir une exécution dans un espace isolé. Le conteneur est plus léger que la machine virtuelle : il repose sur les services offerts par l’OS de la machine physique et n’inclut pas de noyau. En général, il n’inclut qu’une application, avec les fichiers nécessaires à son exécution. Sa taille se compte plutôt en mégaoctets là où les machines virtuelles pèsent fréquemment plusieurs gigaoctets. En effet, ces dernières intègrent un système d’exploitation complet et exécutent souvent plusieurs fonctions.
Les conteneurs sont aussi beaucoup plus légers et rapides à démarrer qu’une machine virtuelle 1.
Ils sont aussi utilisables dans des contextes d’orchestration de conteneurs tels que Kubernetes, qui permettent de gérer, déployer et faire monter en capacité des applications en conteneurs.
Terminologie¶
On parlera de :
- conteneur (container) : un groupe de processus isolé des autres processus créé à partir d’une image de conteneur.
- image de conteneur ou container image : le fichier contenant une image du système de fichiers utilisé pour démarrer le conteneur
Images de conteneurs¶
Une image de conteneur est un fichier qui contient l’ensemble des éléments permettant de créer un conteneur une fois exécuté sur le runtime. Il existe plusieurs logiciels permettant d’exécuter des images de conteneurs, avec des objectifs et fonctionnalités différentes : le plus utilisé est docker engine, mais il y a aussi podman, containerd, kubernetes, rancher,…
Le format des images est défini par une spécification ouverte gérée par l’Open Container Initiative. Une image est essentiellement constituée de métadonnées et d’une ou plusieurs couches de système de fichiers (layers) qui une fois empilées viennent constituer le système de fichier visible des applications qui s’exécutent dans le conteneur.
Une image est construite à partir d’un fichier de description. À chaque ligne du fichier correspond une couche de système de fichiers.
La figure suivante montre une image construite à partir d’une image de base
ubuntu 15.04
à laquelle on ajoute le répertoire courant dans /app
, puis on
lance la commande make
et enfin on positionne la commande par défaut du
conteneur comme étant app.py
.
L’outil Dive permet de visualiser le contenu d’une image, de parcourir les différentes layers et de mesurer l’éventuel espace gâché.
Fabrication d’une image docker¶
Pour fabriquer une image docker, on fournit à l’outil un fichier de description Dockerfile
et un contexte composé :
- de l’ensemble des fichiers contenu dans un répertoire (certains fichiers
pouvant être ignorés au moyen d’un fichier
.dockerignore
, à l’image du.gitignore
) - ou d’une URL pointant vers un dépot git2,
- ou d’une archive tar.
Les fichiers présents dans le contexte pourront être inclus dans l’image au
moyen des commandes COPY
ou ADD
.
Le format du Dockerfile
est
documenté. Les
principales directives sont :
-
FROM
pour indiquer l’image de base, -
COPY
ouADD
pour ajouter des fichiers , -
RUN
pour lancer une commande dans un shell, -
ENV
pour positionner des variables d’environnement, -
ARG
pour définir des variables qui seront positionnables sur la ligne de commande dudocker build
-
ENTRYPOINT
qui définit la commande qui sera exécutée au démarrage, -
CMD
qui définit les paramètres de la commande exécutée au démarrage, -
EXPOSE
qui expose un port à l’extérieur du conteneur.
Un exemple simple de Dockerfile
pour dockeriser un script python :
# ./Dockerfile FROM python:3 RUN pip install pydependency ADD my_script.py / CMD [ "python", "./my_script.py" ]
L’image nommée script:latest
se construit dans le répertoire contenant le
Dockerfile
et le script my_script.py
avec :
docker build -t script:latest .
Pour pousser cette image sur un
dépôt d’images, il faut
la tagguer avec l’URL de ce dépôt et la téléverser avec la commande docker
push
:
docker tag script:latest docker.aix.systerel.fr/cXXX/script:latest docker tag script:latest docker.aix.systerel.fr/cXXX/script:1.3 docker push docker.aix.systerel.fr/cXXX/script:latest docker push docker.aix.systerel.fr/cXXX/script:1.3
Cache docker¶
Chaque ligne du Dockerfile
est associée à une couche (layer) dans le cache
de l’outil Docker qui permet d’accélérer les reconstructions d’images, ce qui
est bien pratique notamment lors de la mise au point des images.
Lors d’une nouvelle construction de l’image à l’aide de la commande docker
build
, si la ligne est identique à celle qui a produit la couche associée dans
le cache, alors la couche du cache est réutilisée telle quelle. Pour les commandes COPY
et ADD
le checksum des fichiers copiés est comparé au
checksum des fichiers utilisés précédemment pour décider s’il faut utiliser le
cache ou non.
Dans l’exemple ci-dessus, ça veut dire que la couche correspondant à RUN pip
install pydependency
ne sera pas reconstruite (et qu’une nouvelle version de
pydependency
ne sera pas installée sans invalidation du cache). Il est
possible d’utiliser l’option --no-cache=true
pour forcer docker build
à ne
pas utiliser le cache.
Dockerfile Multi-stages¶
Une des difficultés pour la construction des images docker est de limiter leur taille. Si on ne prend pas garde, on peut vite arriver à une taille de plusieurs centaines de Mo en partant d’une image de base et en ajoutant les chaînes de compilation nécessaires à l’obtention du binaire que l’on souhaite exploiter dans l’image.
Une solution est d’utiliser une fabrication en deux étapes afin de ne conserver que le strict nécessaire dans l’image finale :
- une première image docker contient tout ce qui est nécessaire à la compilation et est instanciée en un conteneur produisant le binaire à partir des sources,
- l’image finale inclut les dépendances indispensables au runtime et le binaire copié depuis le conteneur précédent.
Concrètement cela se fait de la manière suivante, en utilisant l’option
--from
de la directive COPY
pour récupérer un fichier depuis le conteneur
de fabrication :
# ./Dockerfile FROM golang:1.16 AS builder WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go ./ RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./ CMD ["./app"]
Dépôt d’image (container registry)¶
Plusieurs dépôts d’image de conteneurs sont disponibles :
- Docker Hub, le dépôt officiel de la société Docker (avec une limite au nombre d’images lues ! Ça peut être source de mauvaises surprises). C’est le dépôt utilisé par défaut quand on ne spécifie pas l’URL complète d’une image.
- Quay Container Registry hébergé par RedHat.
- Systerel Docker Registry, le dépôt interne à Systerel.
- Les dépôts spécifiques à chaque projet sur le gitlab interne ou externe (par exemple : le dépôt pour le projet S2OPC)
Image de base¶
L’image de base est celle qui est référencée par la directive FROM
du
Dockerfile
. Plusieurs images sont généralement utilisées :
- debian slim qui pèse 28 Mo (50 Mo pour la version normale),
- alpine qui pèse moins de 3 Mo, mais qui utilise musl comme librairie C, ce qui peut parfois compliquer l’installation de certains programmes,
- distroless qui est destinée à contenir le binaire de l’application et ses dépendances à l’exclusion de tout autre binaire.
- python avec des déclinaisons à base d’image debian ou alpine,
- golang pour la compilation des langages en Go avec des déclinaisons à base d’image debian ou alpine,
Multi-Architectures¶
Une image docker peut contenir des variants pour différentes architectures.
Par exemple, l’image busybox
sur le docker hub
supporte les architectures suivantes amd64
, arm32v5
, arm32v6
, arm32v7
,
arm64v8
, i386
, ppc64le
, et s390x
.
Référez vous à la documentation pour construire une image multi-plateformes.
Fabrication en CI¶
Il est parfois intéressant d’automatiser la création d’une image docker en intégration continue, mais cela nécessite l’utilisation de docker dans docker ce qui n’est pas toujours possible en fonction de la manière dont les runners sont configurés. L’outil kaniko permet de contourner ce problème puisqu’il n’a pas besoin dun démon docker pour construire des images. Voici un exemple simplifié d’utilisation (voir l’exemple complet en fin d’article) :
# .gitlab-ci.yml variables: IMAGE: gitlab.aix.systerel.fr:5000/produit/s3/blast docker: stage: docker image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - /kaniko/executor --context $CI_PROJECT_DIR/docker/blast --build-arg IMAGE="${DEV_IMAGE}" --dockerfile $CI_PROJECT_DIR/docker/blast/Dockerfile --destination "${IMAGE}:${CI_COMMIT_TAG}" --destination "${IMAGE}:latest" only: - /^\d+\.\d+.*$/ - /^testdocker$/
Bonnes pratiques¶
Voici une liste de pratiques recommandées pour la création des images docker :
- Ajouter seulement les fichiers nécessaires (attention aux applications qui utilisent des caches, qui pourraient être embarqués dans l’image).
- Utiliser
.dockerignore
pour limiter le contexte au minimum requis. - Forcer la mise à jour de l’image de base pour s’assurer d’avoir la dernière
version (avec les derniers correctifs de sécurité) :
docker build --pull .
- Éviter de dupliquer les fichiers, par exemple en utilisant l’option
--chmod
de la directiveCOPY
pour copier un fichier avec les bonnes permissions en une seule étape. - Configurer le gestionnaire de paquet en non interactif :
ENV DEBIAN_FRONTEND=noninteractive
RUN pip install --yes --no-input
- Mettre à jour les dépôts de paquets et l’OS de base (au détriment de la
capacité à avoir une image reproductible, mais avec un gain de sécurité) :
RUN apt-get update && apt-get -y upgrade
RUN apk update && apk upgrade
- Configurer les gestionnaires de paquets pour ne pas utiliser le cache et
installer le minimum de paquets :
-
RUN apk add --no-cache PACKAGE
pour ne pas utiliser de cache sous alpine, -
RUN apt-get -y --no-install-recommends install PACKAGE && rm -rf /var/lib/apt/lists/*
pour ne pas installer les paquets optionnels et pour effacer le cache sous debian ou ubuntu, -
RUN npm install --production
pour ne pas installer les dépendances spécifiques au développement, -
RUN pip install --no-cache-dir
pour ne pas utiliser de cache.
-
- Configurer votre application hébergée dans le conteneur pour qu’elle écoute
sur l’adresse générique
0.0.0.0
et non pas sur127.0.0.1
oulocalhost
. Dans ce dernier cas, le port ne serait pas visible de l’extérieur du conteneur. - Utiliser dive pour analyser les images.
- Utiliser un outil permettant de détecter les problèmes ou failles de sécurité, comme trivy
À explorer¶
Une alternative que nous n’avons pas encore mise en œuvre serait de créer et maintenir des images à jour en utilisant l’outil apko pour construire rapidement des petites images reproductibles uniquement à partir d’une liste de paquets apk (ce qui implique de construire les apk spécifiques à notre application).
Leur dépôt contient le résultat d’une analyse comparative et régulière des failles contenues dans les images publiques qui montre qu’il est possible de maintenir des images avec très peu de failles.
Volumes¶
Il est possible de déclarer dans le Dockerfile
un ou plusieurs volumes :
FROM ubuntu RUN mkdir /data VOLUME /data
Le démon docker s’occupe alors de créer un espace dans le système de fichier
hôte, d’y copier les données du répertoire de l’image docker et de monter cet
espace sur le chemin spécifié (ici /data
). Il est également possible lors de
l’appel de docker run
de spécifier un point de montage différent du volume
sur le système hôte (bind mount).
docker run --rm -it -v /srv/app/data:/data:rw myubuntu:latest
Utiliser des volumes géré par le démon docker peut-être plus intéressant que des montages sur le système de fichier (bind mounts) notamment pour des raisons de performance sur Windows ou MacOs.
Installation et utilisation de docker¶
Ce sujet fera l’objet du billet sur l’Utilisation de Docker.
Interrogation d’un dépôt d’images¶
L’outil crane permet d’interroger ou de modifier des images distantes. Par exemple, pour connaître les images disponibles sur le dépôt interne3 :
$ docker run --rm -it gcr.io/go-containerregistry/crane:debug --insecure catalog docker.aix.systerel.fr argos/gss bullseye-min buster-min c462/sysdoc c541/build c541/pyinstaller c672/mass-s3-build …
Et pour lister les tags d’une image :
$ docker run --rm -it gcr.io/go-containerregistry/crane:debug --insecure ls docker.aix.systerel.fr/c939/reviewverif
0.5
latest
Exemples¶
Kaniko pour CI Gitlab¶
Exemple complet de configuration de la CI gitlab pour construire un conteneur.
variables: IMAGE: gitlab.aix.systerel.fr:5000/produit/s3/blast docker: stage: docker image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_JOB_TOKEN\"}}}" > /kaniko/.docker/config.json # Add systerel certificate - | echo "-----BEGIN CERTIFICATE----- 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 -----END CERTIFICATE-----" >> /kaniko/ssl/certs/ca-certificates.crt - /kaniko/executor --context $CI_PROJECT_DIR/docker/blast --build-arg IMAGE="${DEV_IMAGE}" --dockerfile $CI_PROJECT_DIR/docker/blast/Dockerfile --destination "${IMAGE}:${CI_COMMIT_TAG}" --destination "${IMAGE}:latest" only: - /^\d+\.\d+.*$/ - /^testdocker$/
-
Bien qu’il existe maintenant des solutions de Micro-VM rapides, modulo quelques contraintes. ↩
-
Il est recommandé d’éviter les liens git en production, sauf à spécifier explicitement la version ou le tag utilisé (en ajoutant
#nom_du_tag
à l’URL engit://
). ↩ -
Dans le cas de ce dépôt, une interface web est disponible qui permet d’en afficher le contenu : https://docker.aix.systerel.fr/, mais ce n’est pas tout le temps le cas. ↩
Commentaires