Pourquoi aucun nouveau projet ne devrait voir le jour sans docker

Oct 16, 2023 min read

Docker, c’est quoi, au juste ?

Replaçons-nous au niveau d’une application.

Son cycle de vie se compose de 3 étapes majeures :

  • Le développement
  • Les tests
  • La production

Dans ces différents environnement, et dans un monde idéal, l’application devrait se comporter de l’exacte même façon. Pourquoi existerait-il des différences là où il ne devrait pas y en avoir ?

Pourquoi quand je vais installer une application sur certains serveurs, celà fonctionne à merveille, et sur d’autres non, alors même que je ne change rien à mon installation ?

La réponse est simple : l’environnement système influe sur l’application. et à moins de tester tous les environnements possibles de la terre, vous ne pourrez jamais être sûr que votre application fonctionnera partout comme elle le fait sur le poste du développeur.

Est-ce une fatalité ? Non, bien sûr. Mais il existe une possibilité autre qu’une batterie de tests aussi coûteuse en temps, qu’en argent :

Et si on créait une boite, qui, elle, serait compatible avec tous les environnements et qui isolerait notre application du reste du système ?

L’idée peut paraître illusoire, mais c’est bien sur ce concept qu’est construit Docker. Docker est “un chroot plus des cgroups”. En résumé, on crée un système de fichiers indépendant de celui du système, on lui donne des accès isolés et privilégiés au kernel (coeur du système d’exploitation) et on y adjoint une virtualisation de réseau pour la communication des applications docker avec le système hôte et le reste du monde.

Celà ressemble énormément à une machine virtuelle. On fait ça depuis des années avec vmware, proxmox, xen, etc… Pourquoi utiliser un nouvel outil alors qu’on a déjà tout sous la main ?

Et bien, s’il y a de nombreuses ressemblances, il y a aussi d’énormes différences. En terme de ressources déjà, car il n’y a pas besoin d’émuler un système d’exploitation complet. Docker est une solution de virtualisation légère qui s’appuie sur le système de l’hôte pour fonctionner. Mais ce qui fait sa principale force fait aussi sa faiblesse. On ne peut pas faire tourner certaines applications si elles ne sont pas compatibles avec le kernel de l’hôte. On pense directement aux applications windows qui ne tourneront pas sur des systèmes linux via docker. L’inverse n’étant pas vrai non plus d’ailleurs. Pour autant, sur les serveurs windows désormais, une couche de virtualisation OS linux permet de faire tourner les applications Linux. Mais elles ne tournent pas sur le kernel windows, mais bien sur un kernel linux.

Une autre différence notable est qu’un conteneur docker ne doit pas être considéré comme une machine virtuelle complète. Trop de droits donnés à vos applications dans le conteneur peuvent créer des failles de sécurité sur le système hôte. Les volumes docker, qui permettent de partager le système de fichier de l’hôte dans un conteneur et des droits root à l’intérieur de ce conteneur peuvent avoir des conséquences inattendues et fâcheuses.

Autre différence, la trace mémoire d’un conteneur docker est réputée quasi-nulle (je n’ai jamais benché une application au point de pouvoir l’affirmer, mais l’expérience va dans ce sens). Ce qui veut dire que votre application y tournera avec les mêmes performances que si elle avait été installée en natif directement sur le système d’exploitation.

Mais revenons à  nos moutons : pourquoi dis-je qu’aucune application ne devrait voir le jour sans Docker aujourd’hui ?

Prenons le cas d’un développeur python. nous savons que python est un langage qui pose de nombreux problème de compatibilité entre ses versions. Le développeur va donc jongler entre ces versions pour ses développements (Il est rare d’avoir une seule app à développer, et de ne pas avoir de “legacy” à maintenir). Il va donc passer un temps coûteux à basculer entre les environnements. Les applications qu’il va produire vont devoir elles aussi être testées et exploitées sur des environnements différents, car incompatibles entre elles.

La complexité va suivre les applications dans tout leur cycle de vie, ce qui va être coûteux à tous les niveaux pour les équipes de dev, de tests et d’exploitation. Ne parlons pas des erreurs, qui sont humaines, et des appli monolythiques insondables qui ne fonctionnent que dans un environnement au petits oignons, avec des librairies obsolètes et introuvables et que personne ne maîtrise vraiment.

Docker apporte des réponses à ces cas de figure.

Comment Docker fonctionne-t-il ?

Docker repose sur un démon (connu sous le nom de service, dans le monde windows), qui sera installé sur une machine, et qui fera le lien entre les conteneur qu’on lui demandera de faire tourner et le système hôte.

A partir d’un Dockerfile, on produit une image. Grâce à ces images, on va démarrer des conteneurs. Une conteneur est une instanciation d’une image.

En gros, c’est comme si vous faisiez fabriquer un tampon encreur à votre nom. Le tampon c’est l’image, et le conteneur c’est votre nom tamponné sur une feuille. Le tampon n’a pas d’autre vocation que de répéter à l’infini et avec exactitude et toujours de la même façon votre nom sur toutes les feuilles qu’on lui présentera.

La construction d’une image docker se fait par “couches”. On prend une image existante, on y ajoute/modifie des fichiers, des applications et on produit une nouvelle image.

Un exemple de Dockerfile (fichier où on décrit la construction d’une image) assez basique

FROM alpine
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

Le “FROM” indique l’image à partir de laquelle je crée ma propre image. Ici il s’agit d’une image minimale disponible sur docker hub, à l’adresse  https://hub.docker.com/_/alpine

Le COPY permet de copier des fichiers depuis le système de fichier de la machine sur laquelle vous construisez l’image (poste de développeur, jenkins, runner gitlab, etc…) vers le système de fichiers de l’image. Ici, je copie un binaire autoportant en Go “helloworld”, présent dans le dossier dans lequel je construis mon image, vers le /run/ de l’image.

Le WORKDIR fixe le dossier à partir duquel le conteneur va lancer la commande

Enfin, le CMD spécifie la commande que va lancer le conteneur en s’éxecutant. Il ne fera rien d’autre.

Si on résume, le fichier télécharge une image existante, y ajoute un binaire, définit le dossier dans lequel il doit s’exécuter et la commande pour le lancer.

La commande suivante crée l’image à partir du Dockerfile et des fichiers présents dans le dossier depuis lequel on l’exécute, le “.” définissant le répertoire courant sous linux.

docker build .

Pour des questions pratiques, on rajoutera l’option -t nomdelimage qui permettra de l’appeler ensuite

docker build -t helloworld .

La sortie console est la suivante

$ docker build -t helloworld .
Sending build context to Docker daemon  3.296MB
Step 1/4 : FROM alpine
 ---> 961769676411
Step 2/4 : COPY helloworld /run/
 ---> 1692ce89a4f5
Step 3/4 : WORKDIR /run/
 ---> Running in 5d81d369798f
Removing intermediate container 5d81d369798f
 ---> 5d26ad1e983d
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in 54115b068924
Removing intermediate container 54115b068924
 ---> 494f734a76da
Successfully built 494f734a76da
Successfully tagged helloworld:latest

Ensuite on peut exécuter le conteneur directement  avec la commande

docker run helloworld

Où “helloworld” est le nom que nous avons défini avec l’option “-t” lors du build. La sortie console donne donc :

$ docker run helloworld
hello world ! 

C’est aussi simple que celà.

Maintenant, admettons que je veuille changer l’image de base pour mon application. J’ai désormais envie qu’elle tourne dans un système ubuntu, mais en version 18.04 . Je modifie donc en conséquence mon Dockerfile, en le renommant Dockerfile.1

FROM ubuntu:18.04
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

Je rebuilde mon image, mais cette fois en spécifiant que le fichier Dockerfile a changé, avec l’option “-f”

docker build -f Dockerfile.1 -t helloworld .

La sortie :

$ docker build -f Dockerfile.1 -t helloworld .
Sending build context to Docker daemon  3.297MB
Step 1/4 : FROM ubuntu:18.04
18.04: Pulling from library/ubuntu
23884877105a: Pull complete 
bc38caa0f5b9: Pull complete 
2910811b6c42: Pull complete 
36505266dcc6: Pull complete 
Digest: sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f
Status: Downloaded newer image for ubuntu:18.04
 ---> c3c304cb4f22
Step 2/4 : COPY helloworld /run/
 ---> 6d55356eab72
Step 3/4 : WORKDIR /run/
 ---> Running in 84bf593deac4
Removing intermediate container 84bf593deac4
 ---> f35025038e86
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in db718029f8db
Removing intermediate container db718029f8db
 ---> bb3d5526bfad
Successfully built bb3d5526bfad
Successfully tagged helloworld:latest 

Et là, échec. Mon application est trop vieille et ne supporte pas ubuntu 18.04. Ce n’est pas grave, nous allons tester avec ubuntu 16.04. je remodifie mon Dockerfile :

FROM ubuntu:16.04
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

La sortie :

$ docker build -f Dockerfile.1 -t helloworld .
Sending build context to Docker daemon  3.297MB
Step 1/4 : FROM ubuntu:16.04
16.04: Pulling from library/ubuntu
e92ed755c008: Pull complete 
b9fd7cb1ff8f: Pull complete 
ee690f2d57a1: Pull complete 
53e3366ec435: Pull complete 
Digest: sha256:db6697a61d5679b7ca69dbde3dad6be0d17064d5b6b0e9f7be8d456ebb337209
Status: Downloaded newer image for ubuntu:16.04
 ---> 005d2078bdfa
Step 2/4 : COPY helloworld /run/
 ---> 6878dcea9a4d
Step 3/4 : WORKDIR /run/
 ---> Running in 79313eb14c81
Removing intermediate container 79313eb14c81
 ---> acdb5b4a6598
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in 92ad4570e082
Removing intermediate container 92ad4570e082
 ---> ca8486b110d1
Successfully built ca8486b110d1
Successfully tagged helloworld:latest 

Et là magie, ça fonctionne ! En changeant une simple ligne, j’ai corrigé un problème d’environnement.

$ docker run helloworld
hello world ! 

Pas besoin de changer d’OS, d’installer de nouvelles librairies, de perdre un temps précieux à débugger le système autant que l’application.

Maintenant, je vais pouvoir diffuser mon application tout en sachant qu’elle restera dans l’environnement que j’ai décidé de lui fournir, et ce sur toutes les machines linux où elle s’exécutera.

Ça paraît simple, trop simple. Mais c’est pourtant le cas. L’écosystème est beaucoup plus riche que ce que je peux vous montrer ici et couvre la grande majorité des cas de figure.

Bon, et mon dev python dans cette affaire ?

Lui aussi va pouvoir bénéficier de la portabilité de l’environnement autour de l’application.

Disons, qu’il développe une application bonjourlemonde.py, compatible python 3.8.3 dont le code est le suivant :

print("Bonjour le monde !")

Et le Dockerfile (Dockerfile.python3) :

FROM python:3.8.3-buster
COPY bonjourlemonde.py /run/
WORKDIR /run/
CMD ["python3","bonjourlemonde.py"]

Le build va non seulement construire un environnement python dans la version précise qu’attend le développeur, mais va aussi lui assurer que les test et la production seront effectués dans les mêmes conditions. C’est le dev qui choisit les conditions de run et fournit l’image de son application.

Cette image pourra avoir pour source une image créé par les équipes système, gérant les problématiques de sécurité et de scalabilité des applications.

De plus, on pourra faire tourner des applications de versions de python différentes sur les mêmes machines, sans qu’il y ait de collisions, vu que les applications sont bloquées dans leur conteneur, contenant leur environnement d’exécution et leur système de fichier propre.

On peut récupérer des environnements directement créés par les éditeurs

Vous avez besoin d’installer une application nécessaire pour un environnement de développement ? Docker est là.

Exemple : pour des besoins de mise en cache de données précalculées, votre application utilise redis. Sur le poste du développeur, il faut également redis pour tester les mises en cache. Le développeur n’a aucune notion d’admin système, ni n’a jamais installé redis.

Il n’aura qu’une commande à lancer

Docker run -p 6379:6379 redis

Docker se chargera de télécharger l’image officielle redis, de la décompresser et de lancer l’application, qui de plus sera isolée du reste du système et n’aura aucun impact sur l’environnement de développement. L’application écoutera sur le port 6379 du localhost comme si on venait de l’installer.

Ce qui vaut pour redis vaudra pour des milliers d’applications. J’ai personnellement toujours trouvé les applications dont j’avais besoin. Vous pouvez même rajouter les vôtres.

En conclusion :

Docker permet de livrer les applications dans un conteneur “déjà installé”, contenant l’application, ainsi que toutes ses dépendances, le tout déjà installé et prêt à tourner.

Vous pouvez également rapidement déployer des applications, internes ou externes à votre entreprise/projet en quelques secondes pour vous créer des environnements de développement ou de production.

L’utilisation de docker-compose peut étendre ce cas de figure ( Voir mon billet sur terraform pour en avoir un aperçu )

On pourrait étendre ce billet sur tous les cas de figure d’applications que l’on peut rencontrer, mais je vous laisse faire vos recherches. C’est en creusant soi-même qu’on apprend. Le but ici n’était que d’attiser votre curiosité en vous présentant le concept et vous permettre de faire vos recherches derrière.

Personnellement, je ne peux plus faire autrement. J’espère qu’il en sera de même pour vous.

-|