La conteneurisation
En tant qu’administrateur nous faisons face à l’émergence de nouveau outil favorisant le maintien en condition opérationnel (MCO) des systèmes d’information. Parmi ces outils, la conteneurisation, et plus spécifiquement Docker, peut s’avérée être un complément judicieux à la virtualisation, offrant une flexibilité accrue aux systèmes d’information.
Les architectures informatiques
Pour vraiment saisir les avantages de la conteneurisation, il est essentiel de prendre en compte l’évolution au fil des années des architectures logicielles.
Application monolithique
Cela nous amène à revisiter le concept des applications monolithiques. Dans ce modèle d’architecture, tous les éléments logiciels sont conçus au sein d’une même plateforme unifiée.
Cependant, ce schéma ne favorise pas la modularité, c’est-à-dire la capacité d’apporter de la souplesse au sein du système d’information. Par conséquent, ces applications fonctionnent avec leurs propres bases de données, qui ne sont pas partagées avec d’autres applications. De plus, leur méthode d’authentification interne n’est pas compatible avec les technologies de type SSO (Single Sign-On).
Au fil du temps, les entreprises ont progressivement adopté un nombre croissant d’applications pour automatiser leurs processus métiers. Chaque service au sein de l’entreprise disposait de sa propre application pour gérer ses processus spécifiques. Toutefois, le besoin d’harmoniser certaines données s’est rapidement fait sentir. Par exemple, les informations relatives à l’identité d’un employé étaient pertinentes à la fois pour le service de paie et le service des ressources humaines en charge de la gestion de carrière. Dans le cadre d’une architecture monolithique, chaque service devait créer sa propre fiche employée dans son propre logiciel, entraînant non seulement des risques d’erreurs, mais également des pertes de temps considérables.
Les architectures SOA
Ainsi, les architectures monolithiques ont cédé la place aux architectures SOA (Service-Oriented Architecture). Comme son nom l’indique, l’architecture SOA repose sur une orientation vers les services, où les applications et les bases de données au sein d’un système d’information sont considérées comme des services. Reprenons l’exemple précédent impliquant les services de paie et de ressources humaines. Lorsque les données relatives à l’identité d’un employé sont saisies dans le logiciel des ressources humaines, elles deviennent accessibles à l’application de gestion des paies. En d’autres termes, dans cette architecture, le code n’est plus constitué d’un seul bloc, mais de plusieurs blocs plus petits que l’on appelle des services. Ainsi, une application devient un ensemble de services interconnectés.
Ce type d’architecture nous permet de gérer les aspects applicatifs. Cependant, l’inconvénient majeur de ce modèle réside dans la nature des services. Si une application est une composition de services, ces services forment un bloc solide et immuable.
Architecture microservice
L’étape suivante pour accroître la flexibilité consiste à transformer ce bloc solide en un bloc malléable. En d’autres termes, chaque service constituant l’application pourra être modifié. Si une modification a un impact sur la fonctionnalité gérée par le service en question, cela n’entraînera pas nécessairement de répercussions sur les autres services, à condition que ces derniers ne dépendent pas du service modifié.
Dans ce contexte, lorsque nous mettons à jour la gestion de l’authentification, les données nécessitant une authentification ne seront pas accessibles. Cependant, les données ne nécessitant pas d’authentification resteront disponibles. Cependant, si nous modifions le système de gestion des impressions, cela n’aura aucune incidence sur l’utilisation globale de l’application.
Isolation des processus sous Linux
Pour créer une telle architecture, nous aurons besoin d’un environnement capable de générer des services autonomes et de les regrouper.
Les namespaces
En 2002, une fonctionnalité permettant l’isolation des processus a été introduite dans le noyau Linux (version 2.4.19). Cette fonctionnalité, appelée « Namespaces », autorise des processus à utiliser des ressources qui sont déjà utilisées par d’autres ressources.
Il devient possible de partager une ressource telle qu’une carte réseau, permettant à la fois son utilisation par la machine physique et par des hyperviseurs (dans le contexte de la virtualisation). Tout cela s’effectue tout en maintenant une isolation adéquate entre les processus qui utilisent cette ressource.
Les Cgroups
En plus de la fonctionnalité des Namespaces, une autre capacité a été introduite : celle de regrouper et contrôler des fonctionnalités du noyau, connue sous le nom de cgroups (control group). À l’intérieur d’un groupe sous notre contrôle, il est possible d’agréger une portion de nos ressources. Par exemple, cela peut consister en l’allocation de 25 % de notre CPU, 25 % de notre mémoire RAM et 10 % de notre espace disque.
Les ressources peuvent être gérées collectivement. Les fonctionnalités des Namespaces et des Cgroups peuvent collaborer de concert. Le projet qui a fusionné ces deux capacités au sein du noyau est le projet LinuX Container (LXC), lancé en 2007. Comme son nom l’indique, ce projet vise à créer des conteneurs.
La difficulté rencontrée avec ce projet réside dans la question de la portabilité. Comme nous l’avons expliqué précédemment, les fonctionnalités des cgroups et des Namespaces sont intégrées au sein du noyau. Ainsi, si nous transférions le conteneur vers une autre machine, des effets indésirables pourraient apparaître. Ces défis peuvent être résolus en ajoutant une couche supplémentaire à LXC, afin de gérer les disparités entre la machine qui a créé le conteneur et celle qui l’exécutera. Cette surcouche a été introduite en tant que projet open source en 2013 sous le nom de Docker.
Docker
Docker est multiplateforme, c’est-à-dire qu’il peut être installer sur Windows, Linux ou MacOS. Pour illustrer nos exemples, nous installerons docker sur une distribution Alamlinux. Docker se décline en deux versions :
- Docker-CE (Community Edition).
- Docker-EE (Enterprise Edition)
Les fonctionnalités proposées par les deux éditions sont identiques en tout point. La différence se base au niveau du support proposé par Docker. La version Docker-CE qui est une version gratuite propose un support via la communauté des utilisateurs alors que la version Docker EE avec un support payant et proposé d’autres services. Il existe trois offres payantes pour Docker-EE :
- De base: vous obtenez la plate-forme Docker pour une infrastructure certifiée, ainsi que le support de Docker Inc. Vous avez également accès aux conteneurs Docker certifiés et aux plugins Docker depuis Docker Store.
- Standard: Comprend les mêmes fonctionnalités que le niveau de base, mais avec une gestion avancée des images et des conteneurs, une intégration utilisateur LDAP / AD et un contrôle d’accès basé sur les rôles.
- Avancé: Comprend une analyse de sécurité Docker supplémentaire et une surveillance continue des vulnérabilités.
La tarification générale de chaque niveau est généralement basée sur le coût par nœud et par an. Pour ceux qui nous concerne, nous installerons l’édition Docker-CE.
Installation de Docker
Pour cet article, nous utiliserons la distribution Almalinux en version 9. Après avoir installé Almalinux en version minimal, nous ajouterons les dépôts de docker. Pour rappel, la distribution Almalinux est un fork de CentOS, par conséquent, nous devons ajouter les repository destinés à CentOS.
Ajout repos Docker dans Almalinux 9
Le repository est disponible sur les serveurs WEB de Docker à l’adresse :
Nous exécuterons la commande suivante pour ajouter le repository.
[root@docker ~]# dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
Nous mettrons ensuite à jours notre système qui intégrera les mises à jour lié au référentiel Docker que nous venons d’ajouter.
[root@docker ~]# dnf upgrade -y
Pour vérifier que le référentiel a été ajouté dans notre système il est possible d’exécuter la commande suivante :
[root@docker ~]# dnf repolist -v
Maintenant, nous avons tout le nécessaire pour installer Docker et ses outils d’administration et de gestion.
[root@docker ~]# dnf install docker-ce docker-ce-cli containerd.io -y
Pour vérifier que l’installation c’est bien passé, nous exécuterons une commande avec l’utilitaire « Docker ».
[root@docker ~]# docker --version
Après avoir installé le service Docker, nous paramétrons se service afin qu’il démarre à chaque démarrage du système et nous démarrerons Docker.
[root@docker ~]# systemctl enable --now docker
Nous vérifierons que la configuration du démarrage automatique a bien été pris en compte.
[root@docker ~]# systemctl is-enabled docker
Nous vérifierons également le status de notre service qui devrait être en « active ».
[root@docker ~]# systemctl status docker
Les images Docker
Avant d’explorer plus en détail le cœur du sujet, à savoir la conteneurisation, il est essentiel de comprendre que les conteneurs reposent sur des images. Ces images jouent le rôle de modèles (ou de gabarits) qui serviront de base pour la création des conteneurs.
Les images Docker sont constituées d’une série de couches qui, lorsqu’elles sont assemblées, renferment tous les éléments nécessaires au bon fonctionnement de l’application. Ces couches sont en lecture seule, et chaque modification effectuée donne naissance à une nouvelle couche distincte. Pour réaliser cela, les images Docker font usage d’un système de fichiers en couches superposées, comparable à un modèle en oignon, également connu sous le nom d’UnionFS.
Télécharger une image docker
Des images prêtes à l’emploi sont disponibles sur des dépôts, il existe des dépôts privés et public. Commençons par nous rendre sur le dépôt officiel de Docker à l’adresse https://hub.docker.com.
La récupération d’une image se fera à l’aide de la commande « docker pull » suivi de nom de l’image. Par exemple si nous souhaitons récupérer l’image de Ubuntu, sur la plateforme « Dockerhub », dans la barre de recherche, nous allons renseigner « Ubuntu ».
Le résultat de la recherche, nous avons plusieurs propositions. Il est préconisé d’utiliser les images officielles.
Cliquons sur l’image, ainsi nous aurons accès à plus d’information. Parmi ses informations nous avons la commande à exécuter pour récupérer l’image.
Récupération d’une image Docker depuis le Hub officiel
Sur notre machine, nous exécuterons la commande donnée sur la plateforme « DockerHub ».
[root@docker ~]# docker pull ubuntu
Exécutons la commande « docker images », nous vérifierons ainsi que l’image a bien été téléchargée.
Le message renvoyé après avoir exécuter la commande « docker pull ubuntu » indique que nous avons obtenu la version marquée (tag) « latest ». Cette information est accessible lorsque nous listons les images présentes sur le système. En effet, nous avons la possibilité de sélectionner une version spécifique, dans le cas, où nous avons plusieurs version d’une même images.
Lorsque nous téléchargeons une Image Docker et que nous ne spécifions pas de version, l’image portant l’étiquette « latest » sera utilisée par défaut. Pour lister les étiquettes liées à une image, il suffit de consulter la description de l’image sur la plateforme DockerHub où nous avons un onglet « tag » qui répertorie toutes les étiquettes associées à cette image.
Cette image a pour identifiant une valeur exprimée en sha256, par défaut, nous avons la valeur tr onquée de cette ID, le « vrai » identifiant, cette à dire la valeur qui n’est pas tronqué pourras être obtenu en ajoutant le commutateur « no-trunc » à la commande précédemment exécutée.
[root@docker ~]# docker images --no-trunc
Recherche d’image Docker depuis le CLI
Pour rechercher une image, nous pouvons le faire sans passer par un navigateur WEB. En effet, la commande « docker », nous permettra de rechercher un conteneur en se basant sur un mot clé. Par exemple, si nous souhaitons récupérer une image d’un serveur web, nous pouvons exécuter la commande « docker search » suivi d’un mot clé. Par exemple, si nous voulons récupérer une image Docker qui exécutera un serveur web, le mot clé sera « http » par exemple.
[root@docker ~]# docker search http
La réponse de la commande « docker search », nous renvoie les mêmes informations que nous avions avec le moteur de recherche du portail Docker Hub. Dans la mesure du possible, nous prioriserons les images officielles ou celle avec un nombre d’étoile important.
Fonctionnement d’une image
Il faut voir une image Docker comme à LiveCD. Lorsque vous lancez une image, elle reste en lecture seule, et toutes les modifications que vous apportez sont enregistrées séparément. L’image d’origine, combinée à toutes les couches de modifications, forme le conteneur. C’est cette combinaison qui permet aux conteneurs d’être légers, reproductibles et efficaces, car ils partagent des parties communes d’images de base tout en ajoutant des couches spécifiques pour chaque conteneur individuel.
Lorsque nous exécutons une image, nous créons un conteneur à partir de cette image. Ce conteneur constitue un environnement isolé et autonome où l’application ou le service spécifié par l’image peut s’exécuter. L’image de base demeure inchangée, quelle que soit l’action que nous effectuons dans le conteneur. Chaque modification que nous apportons au conteneur est enregistrée dans une couche de modification distincte (overlay), ce qui permet à l’image d’origine de demeurer inchangée. Cette approche favorise la reproductibilité et facilite la gestion des applications, car nous pouvons toujours lancer un nouveau conteneur à partir de la même image d’origine, sans craindre les effets des modifications précédentes. En somme, les conteneurs offrent une isolation et une portabilité tout en maintenant la cohérence de l’image initiale, ce qui garantit un environnement reproductible et fiable.
Manipulation des images
Lorsque vous lancez une image Docker, elle génère un conteneur qui sera un réplica de cette image au niveau des fichiers mais elle intégrera son propre espace de stockage, ses processus distincts et un environnement d’exécution isolé. Prenons l’image que nous avons précédemment récupérer du portail « Docker Hub », pour l’exécuter nous exécuterons la commande docker run <nom_image> soit pour l’image Ubuntu
[root@docker ~]# docker run ubuntu
Cette commande recherche l’image Ubuntu sur la machine locale. Si cette image n’est pas déjà présente, elle sera télécharger depuis le dépôt par défaut, qui est généralement le portail Docker Hub. Ensuite, elle crée un conteneur basé sur cette image. Le conteneur est une instance de l’image Docker et exécute généralement la commande par défaut, qui est souvent /bin/bash pour permettre une interaction avec le conteneur. Pour lister les conteneurs en cours d’éxecution sur une machine, nous exécuterons la commande
[root@docker ~]# docker ps
Comme nous pouvons le constater que la capture d’écran ci-dessus, il n’y a aucun conteneur, cela s’explique par le fait qu’après avoir exécuté la commande « /bin/bash », il n’éxecute plus aucune autre commande, ce qui provoque son extinction automatique soit un status « Exited ». Ce container même si il est éteints est tout de même présent dans la machine, pour voir tout les conteneurs actif ou inactif (status exited), nous exécuterons la commande
[root@docker ~]# docker ps -a
Afin de pouvoir interagir avec une instance dès sa création, nous spécifierons lors de l’exécution de l’image l’ajout du mode interactif et d’un terminal. Cela nous permettra d’engager une interaction avec le conteneur via notre propre terminal.
[root@docker ~]# docker run -it ubuntu
Dans notre invite de commande, nous constatons un changement sur le nom de la machine, dans la capture d’écran, nous sommes passé du nom de l’hôte « docker » à l’identifiant de l’instance « 7c85ed7d6261 ».
Dans ce conteneur, comme c’est souvent le cas dans les conteneurs basés sur une distribution Linux, le seul processus en cours d’exécution est généralement le shell Bash, auquel nous avons lié une interface interactive en utilisant les options -i (interactive) et -t (terminal).
Pour revenir à l’invite de commande de notre machine tout en maintenant le conteneur actif, nous pouvons utiliser la combinaison de touches [Ctrl]+[P] suivie de [Ctrl]+[Q].
Nous exécuterons la commande permettant de répertorier tous les conteneurs, et nous constaterons que notre conteneur affiche un statut « UP ».
Lorsque nous avons utilisé la combinaison de touches [Ctrl]+[P] et [Ctrl]+[Q] dans le contexte de Docker, nous nous sommes détachés de l’invite de commande du terminal de notre conteneur. Pour revenir au terminal de notre conteneur, nous devons le réattacher en utilisant son identifiant unique, soit « 7c85ed7d6261 » dans notre exemple, ou même en tronquant l’identifiant en indiquant « 7c » tant qu’il reste unique parmi les conteneurs actifs.
[root@docker ~]# docker attach 7c
Comme mentionné précédemment dans cette documentation, les modifications apportées à un conteneur n’affectent ni l’image à partir de laquelle il a été créé ni la machine où le moteur Docker est exécuté. Pour illustrer ce concept, prenons un exemple : dans notre conteneur, nous allons créer un fichier nommé « panda » dans le répertoire /home/.
Après avoir créé et vérifier la présence du fichier « panda », nous détacherons l’invite de commande à l’aide de la combinaison de touche clavier [ctrl]+[P] et [ctrl]+[Q] de notre conteneur et vérifier que le répertoire /home/ de notre machine n’héberge pas ce fichier.
Après avoir créé et vérifié la présence du fichier « panda », nous détacherons l’invite de commande de notre conteneur à l’aide de la combinaison de touches [Ctrl]+[P] et [Ctrl]+[Q], puis nous vérifierons que le répertoire /home/ de notre machine ne contient pas ce fichier. Pour récapituler ce qui a été expliqué précédemment, Docker crée une couche de données supplémentaire pour stocker ces nouvelles données. Cela s’appelle la couche de données différentielle. La couche de données différentielle est fusionnée avec les autres données de notre conteneur pour former un seul objet, qui est le conteneur lui-même. Les informations de cette couche de données différentielle sont stockées dans les bibliothèques de Docker. Il nous sera possible de lister les données différentielles et les données fusionner, c’est-à-dire notre fichier panda en le recherchant dans notre système.
Nous retournerons dans notre conteneur, puis nous ajouterons des données dans le fichier « panda ».
[root@docker ~]# docker attach 7c
root@7c85ed7d6261:/home# echo "Bonjour" > panda
root@7c85ed7d6261:/home# cat panda
Retournons sur l’invite de commande de notre machine et éditons le fichier de différencier.
[root@docker ~]# vi /var/lib/docker/overlay2/14401c86fa9a626c1d8b39a7c6ba94968a3d4c6e8a54fe745db8f97efb307e8e/diff/home/panda
Nous ajouterons des données dans ce fichier.
Après avoir enregistrer les modifications nous retournerons dans notre conteneur, puis nous éditerons le fichier que nous avons créée.
[root@docker ~]# docker attach 7c
root@7c85ed7d6261:/home# cat panda
Les données n’auront pas été altérer par la modification que nous avons apporté à notre fichier vu qu’elles n’ont pas été fusionner avec les autres éléments de notre conteneur, c’est dire les données stockées dans le fichier : « var/lib/docker/overlay2/14401c86f../merged/home/panda ».
Sortons une nouvelle fois de notre conteneur à l’aide des combinaisons de touche [ctrl]+[P] et [ctrl]+[Q], nous ajouterons des données dans le fichier :
« var/lib/docker/overlay2/14401c86f…/merged/home/panda».
👍🏻 Les … sont à remplacer par la suite alphanumérique complet.
[root@docker ~]# vi /var/lib/docker/overlay2/14401c.../merged/home/panda
Après avoir enregistrer cette modification, nous attacherons une nouvelle fois le conteneur à notre invite de commande puis nous afficherons le contenu du fichier « panda ».
Docker se concentre principalement sur la séparation des programmes, appelés processus, pour éviter qu’ils ne se perturbent mutuellement et sur la gestion équitable des ressources telles que la puissance de calcul (CPU) et la mémoire. Cependant, en ce qui concerne les données, Docker n’impose pas une séparation aussi stricte par défaut. Cela signifie que, si nous ne prenons pas de précautions, les programmes dans les conteneurs pourraient partager ou même accéder à des données que nous souhaitons garder privées. Il est donc essentiel d’être attentif à la manière dont nous gérons et partageons les données dans Docker pour garantir la sécurité et la confidentialité des informations sensibles.
Isolation des containers
Un des avantages clés de la conteneurisation réside dans sa capacité à isoler des applications les unes des autres grâce à l’utilisation de conteneurs individuels. Cette isolation permet de résoudre les problèmes de conflits de dépendances entre les applications. Lorsqu’une application nécessite une version spécifique d’une bibliothèque ou d’un composant, cela peut parfois entrer en conflit avec les besoins d’une autre application déjà installée sur la même machine. En utilisant des conteneurs, chaque application peut être emballée avec ses propres dépendances, garantissant ainsi qu’elles ne se perturbent pas mutuellement. En conséquence, vous pouvez exécuter plusieurs applications sur la même machine sans craindre de conflits, ce qui optimise l’utilisation des ressources matérielles disponibles et réduit les coûts opérationnels liés à la gestion des serveurs.
Prenons un exemple concret : supposons que vous ayez une application web relativement ancienne qui exige PHP7, tandis que vous souhaitez également installer une autre application plus récente qui nécessite la version PHP8. Dans une installation traditionnelle, vous auriez besoin de deux machines virtuelles distinctes (ou deux machines physiques). Cependant, grâce à la conteneurisation, vous pouvez exécuter les deux applications sur la même machine en utilisant des conteneurs, évitant ainsi la complexité et les coûts supplémentaires associés à la gestion de plusieurs machines.
Création d’un conteneur exécutant php7
Nous illustrerons cet exemple par une mise en pratique. Précédemment, nous avons récupérer une image Ubuntu, nous allons nous en servir pour créer une machine.
Création d’un conteneur utilisant l’image ubuntu
Nous créerons un conteneur que nous nommerons « apache_php7 » soit la commande suivante :
[root@docker ~]# docker run -it --name apache_php7 ubuntu
Mis à jours d’un conteneur docker Ubuntu
Après avois créé notre conteneur, nous mettrons à jours ce dernier.
root@94331d07f35a:/# apt update -y
root@94331d07f35a:/# apt upgrade -y
Installation serveur web Apache dans un conteneur Docker
PHP a besoin d’un serveur web pour fonctionner, et Apache est l’un des serveurs web les plus couramment utilisés pour exécuter des applications PHP. Pour exécuter des scripts PHP, vous devez avoir un serveur web configuré pour interpréter les fichiers PHP et les servir aux clients, par conséquent nous installerons « Apache httpd »
root@94331d07f35a:/# apt install apache2 -y
La version 22.04 d’Ubuntu inclut des dépôts qui fournissent PHP dans sa version 8 par défaut. Si nous souhaitons installer la version 7 de PHP, nous devrons recourir à un PPA (Personal Package Archive) qui contiendra les packages requis pour installer PHP 7. Pour pouvoir ajouter un PPA, nous allons avoir besoin de l’utilitaire « add-apt-repository » présent dans le package « software-properties-common ».
root@94331d07f35a:/# apt install software-properties-common -y
root@94331d07f35a:/# add-apt-repository ppa:ondrej/php -y
root@94331d07f35a:/# apt install php7.4 -y
Lors de l’installation, il nous sera demandé le choix de zone géographique et la ville pour configurer le fuseau horaire du fichier de configuration de php (php.ini).
Vérifions que la version de PHP est effectivement bien la version 7 à l’aide de la commande suivante : .
root@94331d07f35a:/# php --version
Nous allons générer un fichier qui exécute la fonction phpinfo(), ce qui nous permettra d’afficher la configuration de PHP. Ce fichier sera placé dans le répertoire par défaut d’Apache, qui est /var/www/html.
La configuration de ce conteneur étant terminé, nous sortirons de notre conteneur à l’aide de la combinaison de touche [ctrl]+[p], [ctrl]+[q].
Nous listerons nos conteneurs pour s’assurer que celui que nous venons de créer est bien présent et actif.
[root@docker ~]# docker ps
Pour accéder à ce conteneur, nous devons associer le port d’écoute du conteneur à un port disponible sur la machine hôte. Dans notre exemple, nous avons conteneurisé un serveur web Apache, qui utilise le port 80 par défaut pour répondre aux requêtes HTTP. En mappant le port 80 du conteneur à un port disponible sur la machine, nous permettons l’accès au contenu du serveur web depuis la machine hôte. Le conteneur sera alors accessible depuis l’adresse IP de la machine hôte. L’adresse de la machine utilise l’adresse 10.0.0.3, cette sur cette adresse que le serveur web sera accessible
Nous n’avons pas précisé d’écoute à la création de notre container, nous pouvons constater que le conteneur n’écoute sur aucun port.
Convertir un conteneur Docker en image Docker
Le mappage de port doit être configuré lors de la création du conteneur. Pour éviter de devoir répéter toutes les commandes que nous avons utilisées pour créer ce conteneur, la solution consiste à transformer ce conteneur en une image, nous appellerons cette image srv_web_php7 auxquel il me faudra ajouter un tag.
[root@docker ~]# docker commit apache_php7 srv_web_php7:latest
Nous vérifierons que l’image à bien été ajouter sur la machine hôte.
[root@docker ~]# docker images
Supprimer un conteneur
À chaque utilisation de cette image pour créer un conteneur, sa configuration sera répliquée dans chaque instance créé à partir sur cette image. En bref, le conteneur utilisé pour créer cette image peut être supprimé. Pour supprimer un conteneur, il faut qu’il soit stoppé. Après la suppression, nous vérifierons que le conteneur à bien été supprimé
[root@docker ~]# docker stop apache_php7
[root@docker ~]# docker rm apache_php7
[root@docker ~]# docker ps -a
Supprimer plusieurs conteneurs
Pour rappel, un conteneur se veut léger et jetable, donc dans une utilisation typique des conteneurs, lorsqu’un conteneur est éteint, cela signifie généralement qu’il n’a plus de raison d’être. Nous supprimerons donc les conteneurs qui sont stockées sur la machine hôte après avoir stopper la machine qui est en statut « UP ».
[root@docker ~]# docker stop 7c85ed7d6261
[root@docker ~]# docker rm -f $(docker ps -aq)
Explication de la commande :
- docker ps -aq liste tous les conteneurs, en incluant ceux qui ne sont pas en cours d’exécution, en imprimant uniquement leurs IDs.
- docker rm -f supprime tous les conteneurs dont les IDs sont renvoyés par la commande précédente.
Nous vérifierons qu’il n’y a plus aucun conteneur.
[root@docker ~]# docker ps -a
Exposition d’un port d’un conteneur Docker
Maintenant que nous commençons avec un environnement sans aucun conteneur préexistant, nous allons effectuer un test avec notre image en la faisant correspondre avec le port 80 du conteneur et le port 80 de la machine. Étant donné que le service httpd d’Apache Foundation n’est pas démarrer par défaut, nous allons le lancer manuellement.
[root@docker ~]# docker run -it -p 80:80 --name php7 srv_web_php7
root@81610801a595:/# apachectl start
L’adresse IP de la machine est 10.0.0.3, ce qui signifie que le serveur Web de notre conteneur sera accessible à partir de cette adresse. Pour permettre à la machine hôte de recevoir des requêtes sur le port 80, vous devrez configurer le pare-feu de la machine hôte pour autoriser le trafic entrant sur ce port.
root@81610801a595:/# [ctrl]+[P] & [ctrl]+[Q]
[root@docker ~]# firewall-cmd --add-service=http --permanent
[root@docker ~]# firewall-cmd –reload
En lançant un navigateur depuis une machine connectée au même réseau que notre machine hôte, où Docker est en cours d’exécution, nous visiterons l’URL http://10.0.0.3/. Nous devrions avoir accès à notre serveur WEB
En visitant la page que nous avons créée précédemment en y ajoutant la fonction phpinfo() à l’URL http://10.0.0.3/phpinfo.php, nous pourrons vérifier que nous utilisons PHP version 7.
Création d’un conteneur en exécutant php8
La création de notre conteneur qui exécutera PHP 8 suivra un processus similaire à celui que nous avons utilisé pour créer notre conteneur PHP 7, avec quelques différences notables. Nous commencerons à partir de notre image Ubuntu de base, où nous installerons à la fois le service httpd d’Apache Foundation et PHP. Il est important de noter que la version 8 de PHP est disponible par défaut dans les dépôts d’Ubuntu 22.04, donc nous n’aurons pas besoin de recourir à l’utilisation de PPA (Personal Package Archive) pour obtenir cette version spécifique.
Comme le port 80 de la machine hôte est déjà occupé par le conteneur qui exécute PHP version 7, nous allons plutôt faire correspondre le port 81 de la machine hôte avec le port 80 du nouveau conteneur que nous allons créer.
Configuration le conteneur docker PHP 8
Nous commencerons par créer notre conteneur avec la commande suivante :
[root@docker ~]# docker run -it -p 81:80 --name php8 ubuntu
Dans le conteneur que nous venons de créer, nous installerons le serveur web Apache et php 8 après avoir mis à jour la version ubuntu qui est exécuter dans le conteneur
root@fb9b37059606:/# apt update -y && apt upgrade -y
root@fb9b37059606:/# apt install apache2 -y
root@fb9b37059606:/# apt install php -y
root@fb9b37059606:/# apachectl restart
Une fois PHP installé, nous pouvons vérifier qu’il s’agit bien de la version 8 qui est en cours d’exécution.
root@fb9b37059606:/# php -v
Nous créerons également un fichier exécutant la fonction phpinfo() à la racine du répertoire « html » présent dans « /va/www/ »
root@fb9b37059606:/# echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php
Après être sorti de notre conteneur, nous autoriserons la machine hôte à recevoir des requêtes sur le port 81.
root@fb9b37059606:/#
[root@docker ~]# firewall-cmd --add-port=81/tcp --permanent
[root@docker ~]# firewall-cmd --reload
Nous pouvons maintenant nous rendre sur le port 81 de notre machine hôte, et nous constaterons que le conteneur exécutant PHP8 est bien fonctionnel.
Docker référencie bien ses deux conteneurs, un qui est mapper sur le port 80 et un autre mapper sur le port 81.
[root@docker ~]# docker ps
Accéder au conteneur derrière un reverse proxy
Dans un environnement de production, il est recommandé de configurer un serveur Web pour qu’il soit accessible sur son port par défaut associé aux protocoles « http » ou « https », c’est-à-dire le port 80 pour http et le port 443 pour https. Pour atteindre cet objectif, il est préférable d’utiliser un reverse proxy. Le reverse proxy prend en charge la réception des requêtes entrantes et les transmet ensuite au conteneur approprié. Afin que les utilisateurs puissent facilement identifier ces conteneurs, il est essentiel d’utiliser des noms de domaine pleinement qualifiés (FQDN), ce qui suppose l’utilisation d’un système de résolution de noms. Nous allons voir comment cela se met en place.
Création d’u conteneur de l’image PHP8
Pour démarrer ce nouveau projet, nous allons capitaliser sur notre travail antérieur. Cependant, avant de poursuivre, notre première étape consistera à créer l’image Docker pour le conteneur exécutant PHP 8. En d’autres termes, nous commencerons par élaborer cette nouvelle image de la même manière que nous l’avons fait pour l’image du conteneur srv_web_php7.
[root@docker ~]# docker commit fb9b37059606 srv_web_php8:latest
Nous vérifierons que l’image a bien été créée.
[root@docker ~]# docker images
Nous créerons l’architecture suivante :
Dans cette configuration de base, nous disposons déjà de l’image « srv_web_php7 » pour créer le conteneur « PHP7 », ainsi que de l’image « src_web_php8 » pour créer le conteneur « PHP8 ». Notre prochaine étape consiste à créer l’image qui exécutera le reverse proxy, lequel sera configuré sur un serveur nginx. Cependant, avant de faire cela, nous commencerons avec un environnement Docker propre, sans aucun conteneur. Nous supprimerons donc ceux que nous avons précédemment créer.
[root@docker ~]# docker rm -f $(docker ps -aq)
Nous commencerons par créer notre conteneur PHP7. Nous allons également avoir besoin d’outils « ip » pour configurer la partie réseau et de l’outils ping pour tester la communication entre nos conteneurs. L’outils « ip » est présent dans le paquet « iproute2 » et l’outil « ping » sera disponible en installant le paquet « iptutils-ping ». Ce conteneur sera ensuite converti en images, par conséquent, pour le moment, nous le mapperons sur aucun port de la machine physique.
[root@docker ~]# docker run -it --name PHP7 srv_web_php7
root@413ddd8824ab:/# apt update -y && apt upgrade -y
root@413ddd8824ab:/# apt install iproute2 iputils-ping -y
Nous vérifierons que nous avons bien la possibilité d’utiliser la commande « ip », par exemple, nous utiliserons la commande « ip » pour connaître l’adresse IP de notre passerelle.
root@413ddd8824ab:/# ip route show
Nous testerons également la commande « ping » en tenant de communiquer avec la passerelle configurée sur le conteneur.
root@413ddd8824ab:/# ping 172.17.0.1 -c 4
Création du conteneur PHP8
Pour la création du conteneur PHP8, nous procéderons de la même façon après avoir détacher notre terminal de l’invite de commande de notre nouveau conteneur en utilisant la combinaison de touche [ctrl]+[P] & [ctrl]+[Q].
Ensuite nous mettrons à jours le système et nous installerons les paquets « iproute2 » et « iputils-ping ».
root@212bbec8e72c:/# apt update -y && apt upgrade -y
root@212bbec8e72c:/# apt install iproute2 iputils-ping -y
Nous effectuerons les mêmes tests que ceux que nous avons fait sur le conteneur que nous avons précédemment créé.
root@212bbec8e72c:/# ip route show
root@212bbec8e72c:/# ping 172.17.0.1 -c4
Nous constaterons que les conteneurs partagent la même adresse de passerelle, ce qui indique clairement que les deux conteneurs sont sur le même réseau à savoir 172.17.0.0/16. Si l’adresse de ce conteneur est 172.17.0.3, l’adresse du précédent conteneur est 172.17.0.2. Cette information est donnée lors de la commande « ip route show » à la fin de la seconde ligne.
Ce conteneur étant configurer nous pouvons détacher notre terminal de l’invite de commande du conteneur.
Création du conteneur NGINX
Pour le conteneur docker qui exécutera nginx, nous devons repartir d’une image vierge. Nous repartirons sur l’image Ubuntu vu qu’elle est présente sur notre machine.
[root@docker ~]# docker run -ti --name reverse_proxy ubuntu
Dans ce conteneur, nous installerons le paquet « nginx » qui sera configuré en reverse proxy et nous y ajouterons les outils « iproute2 » et « iputils-ping » après avoir mis à jour le système d’exploitation.
root@e4cee4f4deb3:/# apt update -y && apt upgrade -y
root@e4cee4f4deb3:/# apt install nginx iproute2 iputils-ping -y
Rappelle fonctionnement Reverse Proxy
Le reverse proxy fonctionne en se basant sur l’URL de la requête. Une seule machine peut héberger plusieurs sites ou applications. Pour différencier ces sites, il est essentiel d’utiliser des noms de domaine pleinement qualifiés (FQDN) uniques. Lorsqu’une requête HTTP est reçue, le reverse proxy examine l’entête HTTP, en particulier l’URL et le nom d’hôte, pour déterminer où rediriger la requête. Cela nécessite que le reverse proxy puisse résoudre le nom d’hôte (FQDN) en une adresse IP pour identifier la destination correcte et transmettre la requête au bon conteneur ou serveur. Pour éviter d’avoir à déployer un DNS, nous utiliserons le fichier /etc/hosts présent dans le conteneur « reverse_proxy ». Nous rappellerons ici qu’un conteneur n’a pas d’éditeur de texte par défaut, nous utiliserons donc la commande « echo » pour modifier le fichier /etc/hosts. Une ligne étant une information, dans notre contexte, nous avons à renseigner la résolution de nom de deux conteneurs, nous aurons donc un saut de ligne, il nous faudra donc utiliser le commutateur « -e » pour la prise en charge du saut de ligne ( \n ) dans la commande echo
Etant donnée que le conteneur PHP7 a pour adresse IP 172.17.0.2, nous lui attribuerons le nom pleinement qualifier « php7.elkhrissi.lab » et pour le conteneur PHP8 disponible à l’adresse IP 172.17.0.3 avec le FQDN « php8.elkhrissi.lab ».
Nous ajouterons donc ses informations dans le fichier hosts de notre conteneur « reverse_proxy ».
root@e4cee4f4deb3:/# echo -e "172.17.0.2 php7 php7.elkhrissi.lab \n172.17.0.3 php8 php8.elkhrissi.lab" >> /etc/hosts
Nous vérifierons que la résolution de nom est bien effective à l’aide de la commande « ping ».
root@e4cee4f4deb3:/# ping php7.elkhrissi.lab -c1
root@e4cee4f4deb3:/# ping php8.elkhrissi.lab -c1
Ce conteneur doit pouvoir communiquer avec les deux autres conteneurs PHP7 et PHP8, nous exécuterons une commande ping pour vérifier que le conteneur nginx peut joindre le conteneur PHP7 et PHP8.
Configuration du reverse proxy
Nous mettrons en place une configuration dans nginx qui permettra de rediriger les requêtes adressées au nom de domaine pleinement qualifié (FQDN) « php7.elkhrissi.lab » vers le conteneur PHP7, et les requêtes adressées à « php8.elkhrissi.lab » vers le conteneur PHP8.
Reverse proxy pour PHP7
Pour la configuration des requêtes à destination du conteneur PHP7, nous créerons un fichier de configuration php7.conf qui devra être stocké /etc/nginx/conf.d/. Le contenu de se fichier sera le suivant :
server {
listen 80;
server_name php7.elkrhissi.lab;
location / {
proxy_pass http://php7.elkhrissi.lab;
}
- server { … } : Cela définit un bloc de configuration pour un serveur virtuel. Cela signifie que les instructions à l’intérieur de ce bloc s’appliqueront aux requêtes entrantes qui correspondent au nom du serveur spécifié (server_name) et au port spécifié (listen).
- listen 80; : Cette ligne indique au serveur nginx d’écouter les requêtes entrantes sur le port 80. Le port 80 est le port par défaut pour les requêtes HTTP non chiffrées.
- server_name php7.elkrhissi.lab; : Ici, nous spécifions le nom de domaine pleinement qualifié (FQDN) auquel le reverse proxy répondra. Dans ce cas, il s’agit de php7.elkrhissi.lab. Cela signifie que ce serveur nginx traitera les requêtes entrantes pour ce FQDN spécifique.
- location / { … } : Cela configure une directive location qui spécifie comment le serveur nginx doit gérer les requêtes pour les chemins correspondant à la racine (« / »). Cela signifie que toutes les requêtes entrantes seront gérées par cette directive.
- proxy_pass http://php7.elkhrissi.lab; : C’est la partie la plus importante de la configuration. Cette directive indique à nginx de rediriger toutes les requêtes entrantes à cette location vers le conteneur Docker nommé « php7.elkhrissi.lab » en utilisant le protocole HTTP. En d’autres termes, lorsque le serveur nginx reçoit une requête pour php7.elkrhissi.lab, il transmettra cette requête au conteneur Docker qui exécute PHP7, permettant ainsi au conteneur de traiter la requête et de renvoyer la réponse.
Pour créer ce fichier, nous utiliserons l’outil « echo »
root@e4cee4f4deb3:/#echo 'server {
listen 80;
server_name php7.elkhrissi.lab;
location / {
proxy_pass http://php7.elkhrissi.lab;
}
}' | tee /etc/nginx/conf.d/php7.conf
Reverse proxy pour PHP8
Pour les requêtes à destination du conteneur exécutant PHP8, nous créerons un fichier de configuration en gardant la même logique que le fichier que nous avons créé précédemment.
root@e4cee4f4deb3:/#echo 'server {
listen 80;
server_name php8.elkhrissi.lab;
location / {
proxy_pass http://php8.elkhrissi.lab;
}
}' | tee /etc/nginx/conf.d/php8.conf
Conversion conteneur docker en image docker
Nous convertirons ses conteneurs en une image, pour ce faire, nous devrons détacher notre terminal de l’invite de commande de notre conteneur à l’aide des combinaisons de touche [ctrl]+[P] & [ctrl]+[Q]
root@e4cee4f4deb3:/#
[root@docker ~]# docker commit e4cee4f4deb3 reverse-proxy
Nous avons notre image prête à l’emploi, nous allons pouvoir l’exécuter et mapper le port 80 du conteneur avec le port 80 de la machine hôte. Le conteneur s’appellera nginx-rp.
[root@docker ~]# docker run -ti -p 80:80 --name nginx-rp reverse-proxy
En lançant le service nginx dans notre conteneur, nous sommes confrontés à une erreur. Cette erreur survient en raison de l’absence de persistance des données stockées dans notre conteneur précédent, qui a servi de base pour créer cette image.
Cette caractéristique essentielle des conteneurs, à savoir leur nature éphémère, signifie qu’ils ne conservent pas les données au-delà de leur cycle de vie, du moins pas celles qui ne sont pas présentes dans leur système de fichiers principal. Par défaut, lors de l’installation d’une nouvelle application, les fichiers de configuration sont généralement stockés dans les répertoires du service, c’est-à-dire dans le système de fichiers principal du conteneur. Ainsi, nous retrouverons bien nos fichiers de configuration.
root@1c6760241900:/# ls /etc/nginx/conf.d/
En revanche, les modifications que nous avons apportées au fichier /etc/hosts n’ont pas été conservé.
Par conséquent, lors du démarrage du service nginx, nous avons configuré des fichiers de configuration dans lesquels nous avons spécifié les noms de domaine vers lesquels il doit rediriger les requêtes. Cependant, le service nginx ne peut pas les contacter car le système ne peut pas résoudre ces noms de domaine en adresses IP. En conséquence, le service nginx génère une erreur. Nous ajouterons nos deux entrées dans le fichier /etc/hosts puis nous serons en mesure de démarrer le service nginx.
root@1c6760241900:/# echo -e "172.17.0.2 php7 php7.elkhrissi.lab \n172.17.0.3 php8 php8.elkhrissi.lab" >> /etc/hosts
root@1c6760241900:/# service nginx start
Verification des accès aux serveurs Web
À partir d’une machine cliente Windows connectée au même réseau que notre machine hôte, nous vérifierons la fonctionnalité de notre configuration. Avant de procéder aux vérifications, nous ajusterons le fichier hosts de la machine cliente afin qu’elle puisse résoudre les noms de domaine de nos deux conteneurs. Nous ouvrions « notepad » en tant qu’administrateur, nous ouvrions le fichier hosts disponible au chemin C:\Windows\System32\drivers\etc. En sélectionnant « Tout les fichiers » en bas à doite nous devrions voir le fichier hosts. Nous le sélectionnerons et cliquerons sur le bouton « Ouvrir »
L’adresse IP que nous renseignerons sera l’adresse IP de notre machine hôte qui, dans le cadre de cette documentation est 10.0.0.3 qui cela la résolution de nom de « php7.elkhrissi.lab » et « php8.elkhrissi.lab »
Une fois que la modification a été enregistré, nous allons nous rendre aux URLs après nous êtes assurer que les services apache sur nos conteneurs est bien démarré :
Après avoir enregistré la modification, nous allons accéder aux URLs suivant une fois que nous nous sommes assurés que les services Apache dans nos conteneurs sont bien démarrés :
- http://php7.elkhrissi.lab/phpinfo.php
- http://php8.elkhrissi.lab/phpinfo.php
Stockage de données
Présentation du système de fichier de docker
Dans le chapitre précédent, nous avons constaté que les données à l’intérieur d’un conteneur ne sont pas persistantes, ce qui signifie qu’elles ne seront plus disponibles une fois que le conteneur a été arrêté. Cette spécificité découle du fonctionnement fondamental des conteneurs, qui vise à établir une séparation claire entre l’application et les données. C’est également cette caractéristique qui rend les conteneurs légers.
Les fichiers disponible en écrite sur le conteneur Docker
Pour comprendre cette notion, nous devons examiner le fonctionnement du système de fichiers de Docker, appelé UnionFS. Ce système fonctionne en utilisant des couches, comme les couches d’un oignon. Lorsque nous créons un conteneur à partir d’une image, nous copions les données de l’image dans un conteneur, ce qui crée la première couche de notre système de fichiers. Cette couche est en lecture seule, à l’exception de trois fichiers qui sont en lecture et écriture :
- etc/resolv.conf
- /etc/hostname
- /etc/hosts
Retournons dans notre conteneur nginx_rp pour voir cela. Les 3 fichiers sont en effet montés en « rw »
root@e4cee4f4deb3:/# mount -l | grep /etc/
Les fichiers qui sont en lecture et écriture à l’intérieur d’un conteneur Docker ne sont pas persistants, ce qui signifie qu’ils existent uniquement pendant le cycle de vie du conteneur. Lorsque le conteneur est arrêté, supprimé ou remplacé, ces fichiers sont également perdus.
Les volumes Docker
Pour garantir la persistance de ses données, il est essentiel de s’appuyer sur un volume. Un volume est un emplacement de stockage Docker qui existe en dehors du conteneur, tel qu’une baie de stockage, un partage réseau ou un disque local de la machine hôte, et qui peut être lié à un ou plusieurs conteneurs.
Les données stockées dans un volume sont conservées de manière permanente, même lorsque le conteneur est arrêté ou supprimé. Cela signifie que les volumes permettent de stocker des fichiers de configuration, des bases de données, des fichiers de logs ou tout autre type de données de manière fiable. Par exemple, un volume Docker peut être utilisé pour stocker la base de données d’une application web, assurant ainsi que les données critiques restent intactes même en cas de redémarrage ou de mise à jour du conteneur.
Mise en place de volume docker
Pour illustrer la configuration d’un volume que nous appellerons « data », nous allons le créer le volume directement sur la machine hôte. À partir de l’invite de commande de notre machine hôte, nous exécuterons la commande de création de volume.
[root@docker ~]# docker volume create data
Cette action va créer un répertoire data à l’emplacement /var/lib/docker/volumes/
Ce volume peut être associé à un ou plusieurs conteneurs. Pour plus de clarté, nous allons supprimer les conteneurs que nous avons créés précédemment et recommencer avec de nouveaux conteneurs.
[root@docker ~]# docker rm -f $( docker ps -aq )
Cependant, nous continuerons à utiliser les images que nous avions précédemment créées, à savoir :
- srv_web_php7
- srv_web_php8
- reverse-proxy
Comme précédemment mentionné, le fichier /etc/hosts est en lecture-écriture. Lors de la création du conteneur, nous y ajoutons des entrées de résolution de nom, puis nous montons le volume que nous avons préalablement créé. Dans cet exemple, nous supposons que les fichiers que nous souhaitons conserver ou pour lesquels nous voulons séparer les données du conteneur. C’est-à-dire décorréler les données du conteneur.
Concernant les noms des conteneurs :
- Instance srv_web_php7 : App1
- Instance srv_web_php8 : App2
- reverse-proxy : rproxy
Attaché le volume sur conteneur App1 et App2
Dans les conteneurs app1 et et app2, nous ajouterons notre volume qui aura comme point de montage /var/www/html, nous démarrerons également le service Apache2.
Nous exécuterons la commande suivant pour la création du conteneur app1
[root@docker ~]# docker run -ti -v data:/var/www/html --name app1 srv_web_php7
root@eed92a3999f2:/# apachectl start
Dans la même logique que la création du conteneur app1, nous exécuterons la commande suivante pour créer le conteneur app2.
[root@docker ~]# docker run -ti -v data:/var/www/html --name app2 srv_web_php8
root@ac8f7c628aff:/# apachectl start
Attacher volume conteneur rproxy
Pour le conteneur rproxy, nous ajouterons les informations dans le fichier /etc/hosts pour assurer résolution de nom.
[root@docker ~]# docker run -ti -p 80:80 --name rproxy --add-host php7.elkhrissi.lab:172.17.0.2 --add-host php8.elkhrissi.lab:172.17.0.3 reverse-proxy
root@21bfc77e5f5e:/# service nginx start
Tests et Vérifications
Dans notre conteneur nginx, nous allons vérifier que les informations ont bien été ajouter dans le fichier /etc/hosts.
root@21bfc77e5f5e:/# cat /etc/hosts
root@21bfc77e5f5e:/# ping php7.elkhrissi.lab -c1
root@21bfc77e5f5e:/# ping php8.elkhrissi.lab -c1
Nous pouvons également nous assurer que notre conteneur « rproxy » est en mesure de communiquer avec php7.elkhrissi.lab etphp8.elkhrissi.lab.
A ce stade nous sommes assurés que le conteneur « rproxy » peut communiquer avec nos serveur web conteneurisé et que la résolution de nom fonctionne bien.
Nous allons également nous assurer que du côté de nos conteneurs app1 et app2 fonctionnent correctement. Nous sortirons du conteneur « rproxy » à l’aide de la combinaison de touche [ctrl][P] et [ctrl][Q], puis nous allons nous rendre sur le conteneur app1.
[root@docker ~]# docker attach app1
Lors de la création de notre conteneur, nous avions préciser que le volume a comme point de montage /var/www/html. Nous allons vérifier que c’est bien le cas.
Dans le répertoire « /var/www/html », nous voyons que nous avons encore les données contenues dans nos images, en effet, les données qui était présent dans « /var/www/html/ » de notre image, seront toujours présent mais physiquement, elles seront hébergées dans le répertoire « /var/lib/docker/volumes/data/_data/ » de notre répertoire hébergé sur notre hôte.
Nous allons nous en assurer en listant le contenu du répertoire /var/lib/docker/volumes/data/_data/ de notre hôte.
[root@docker ~]# ls /var/lib/docker/volumes/data/_data/
Nous prcéderons au mêmes tests pour notre conteneur app2. Profitons de la répétition des action pour voir d’autres commande. Nous pouvons exécuter des commandes sur un conteneur depuis la machine hôte. Précédemment, nous avions utilisé la commande « mount » à laquelle nous avions ajouté un filtre avec l’outil « grep ». Nous utiliserons la commande « docker exec ».
[root@docker ~]# docker exec app2 mount | grep /var/www/html
Vu que le volume de app2 et le même que app1, nous allons avoir le même contenu.
[root@docker ~]# docker exec app2 ls /var/www/html
Ses données sont physiquement dans « /var/lib/docker/volumes/data/_data/ »
Ajout de donner persistante sur nos conteneurs APP1 et APP2
Cela implique que nous pouvons ajouter des données sur un conteneur en passant par le volume. Pour illustrer mes propos, nous allons créer un fichier dans le répertoire /var/lib/docker/volumes/data/_data/ de la machine hôte.
[root@docker ~]# vi /var/lib/docker/volumes/data/_data/test.php
Dans ce fichier, nous allons ajoute une fonction PHP qui nous indiquera sur quel serveur nous sommes.
<?php
echo "Je suis sur : " . $_SERVER['HTTP_HOST'];
?>
Nous devrions avoir accès à cette page sur nos deux conteneurs exécutant Apache depuis un navigateur web.
De ce fait, une même ressource pourra être utilisé par plusieurs conteneurs, ce qui peut représenter un gain au niveau de stockage mais aussi en termes de maintien en condition opérationnel.
Les réseaux sous Docker
Dans les exemples précédent nous avons vu qu’un réseau est déjà opérationnelle par défaut avec Docker. Dans ce réseau est intégré un DHCP, qui délivre les configurations réseau des conteneurs que nous créons. Lors de l’installation de Docker 3 réseaux sont créés, nous pouvons les lister en exécutant la commande suivante :
[root@docker ~]# docker network list
Le type de réseau est défini dans la colonne « DRIVER », ainsi nous avons 3 types de réseau :
- Bridge
- Host
- Null
Le vocabulaire utilisé dans Docker peut prêter à confusion, surtout si l’on a déjà une expérience en virtualisation. En effet, ici, la notion de bridge a une signification différente de celle de la virtualisation. Dans la conteneurisation Docker, un réseau bridge signifie que les conteneurs connectés à ce réseau accèdent aux ressources externes via une translation d’adresse, ce qui équivaut à ce que l’on appelle NAT en virtualisation. En revanche, le réseau host dans Docker permet de connecter les conteneurs au même réseau que la machine hôte, ce qui correspond à ce que l’on appelle bridge en virtualisation, il n’y aura plus aucune isolation réseau entre le conteneur et la machine hôte, ce qui peut être un risque de sécurité. Le troisième driver « Null », permet d’avoir un container qui ne pourra pas avoir d’accès vers l’extérieur.
Présentation des réseaux sous Docker
En considérant qu’un réseau peut être vu comme un environnement de confiance, il est essentiel de reconnaître que le niveau de confiance que nous accordons à une demande venant de l’extérieur ne peut pas être le même que celui que nous accordons à une demande émise à partir de notre système d’information. Pour remédier à cette situation, il serait envisageable de repenser l’architecture web précédente en mettant en place des zones de confiance, c’est-à-dire des réseaux distincts. Ainsi, dans notre précédente architecture, nous allons avoir :
- Un réseau dédié à la communication entre le conteneur exécutant php7 et le reverse proxy (en vert dans le schéma ci-après).
- Un réseau dédié à la communication entre le conteneur exécutant php8 et le reverse proxy (en bleu dans le schéma ci-après).
- Un réseau dédié destiné à isoler le reverse proxy de la machine hôte (en gris dans le schéma ci-après), la communication avec l’hôte se limitera à la communication sur le port 80.
Création des réseaux dans Docker
Nous créerons nos 3 réseaux :
[root@docker ~]# docker network create --subnet 192.168.0.0/24 reseau_proxy
[root@docker ~]# docker network create --subnet 192.168.1.0/24 reseau_php7
[root@docker ~]# docker network create --subnet 192.168.2.0/24 reseau_php8
Nous vérifierons ensuite que les 3 réseaux ont été ajouté à la liste des réseaux Docker. Pour ce faire; nous en exécuterons a commande suivante :
[root@docker ~]# docker network list
Bon à savoir :
Pour la mise en pratique, nous utiliserons les images que nous avons réalisé précédemment. Mais avant cette mise en pratique, nous effacerons les conteneurs que nous avons créée.
Liste des images
[root@docker ~]# docker images --format "{{.Repository}}"
Suppression des conteneurs
[root@docker ~]# docker rm -f $(docker ps -aq)
Nous vérifierons que nous n’avons plus de conteneur sur la machine hôte. Après s’être assuré que notre environnement n’héberge plus de conteneur, nous procéderons à la création de nos conteneurs qui seront connecté à leurs réseaux
[root@docker ~]# docker ps -a
Associer un réseau sur un conteneur
Nous commencerons par créer le conteneur app-php7.
[root@docker ~]# docker run -d --name app-php7 --network reseau_php7 srv_web_php7 apache2ctl -D FOREGROUND
Nous remarquerons que la commande pour créer ce conteneur diffère légèrement de ce que nous avons précédemment examiné. En effet, nous n’utilisons plus les options « -ti ». À la place, nous utilisons l’option « -d ». Cette option permet de lancer le conteneur sans attacher notre terminal à l’invite de commande du conteneur. Lorsque nous utilisions l’option « -ti », le conteneur était en mode interactif, ce qui signifie qu’un service était en cours d’exécution dans le conteneur pour permettre l’interaction, tout en maintenant le statut ‘UP’ du conteneur. Il est important de rappeler que lorsqu’un conteneur ne fait pas tourner de service, il s’éteint. De ce fait, nous devons également exécuter le service apache2ctl en mode foreground ».
Pour notre deuxième conteneur qui exécutera la version 8 de PHP, nous construirons notre commande en respectant la même logique que pour le conteneur app-php7.
[root@docker ~]# docker run -d --name app-php8 --network reseau_php8 srv_web_php8 apache2ctl -D FOREGROUND
Nous vérifierons que les deux conteneurs ont bien été créé.
Vérifier la configuration des conteneurs
Nous allons inspecter ses deux conteneurs à l’aide de la commande « docker inspect », cela nous permettra de voir la configuration réseau de nos conteneurs. La réponse fournit par « docker inspect » est fourni en format json. Par défaut, un nombre important d’information est donnée, nous ne souhaiterons limiter notre résultat à la section « Networks ». Nous aurons besoin d’un outils tiers nommé « jq » pour filtrer le résultat. Par défaut l’outils « jq » n’est pas disponible, il nous faudra donc l’installer.
[root@docker ~]# dnf install jq -y
Nous exécuterons ensuite la commande suivante pour afficher la configuration réseau :
[root@docker ~]# docker inspect app-php7 | jq ".[0].NetworkSettings.Networks"
Nous inspecterons également le conteneur « app-php8 » pour vérifier que la configuration du réseau du conteneur.
[root@docker ~]# docker inspect app-php8 | jq « .[0].NetworkSettings.Networks »
Pour créer notre instance reverse-proxy, nous débuterons par la création d’un conteneur que nous nommerons « rp ». Ce conteneur sera connecté au réseau « reseau-proxy ». Pour faciliter la résolution de noms, nous ajouterons des entrées personnalisées dans le fichier /etc/hosts. Une fois que le conteneur sera créé, nous lancerons le service ‘nginx’ à l’intérieur du conteneur. Pour terminer, nous sortirons du conteneur à l’aide de la combinaison [ctrl]+[P] et [ctrl]+[Q]
[root@docker ~]# docker run -ti --name rp --add-host php7.elkhrissi.lab:192.168.1.2 --add-host php8.elkhrissi.lab:192.168.2.2 --network reseau_proxy -p 80:80 reverse-proxy
root@69d601f365f8:/# service nginx start
Pour assurer la communication entre le conteneur « rp » et « app-php7 », nous devons :
- connecter le conteneur « rp » au réseau « reseau-php7 »
- connecter le conteneur « rp » au réseau « reseau-php8 ».
[root@docker ~]# docker network connect reseau_php7 rp
[root@docker ~]# docker network connect reseau_php8 rp
En inspectant la configuration réseau du conteneur « rp », nous constaterons que le conteneur « rp » est connecté au 3 réseaux.
[root@docker ~]# docker inspect rp | jq ".[0].NetworkSettings.Networks" | grep IPAddress
Depuis notre navigateur, testons notre architecture.
Nous vérifierons que les deux conteneurs ne communiquent pas ensemble. Pour ce faire, nous allons utiliser l’outils ping qui, par défaut, n’est pas présent. Nous l’installerons dans le conteneur app-php7, app-php8
[root@docker ~]# docker exec -it app-php7 apt-get install -y iputils-ping
[root@docker ~]# docker exec -it app-php8 apt-get install -y iputils-ping
Pour rappel, l’adresse IP de « app-php7 » est 192.168.1.2 et l’adresse IP de « app-php8 » est 192.168.2.2
Nous testerons que la communication entre app-php7 ne peux pas se faire.
[root@docker ~]# docker exec -it app-php7 ping -c 4 192.168.2.2
En toute logique, le conteneur app-php8 ne sera pas en mesure de communiquer avec app-php7.
[root@docker ~]# docker exec -it app-php7 ping -c 4 192.168.2.2
Dans ce chapitre, nous avons vu le cloisonnement sans passer par des solutions UTM. Si nous voulions durcir le cloisonnement la solution docker ne serait pas adaptée, d’autres solutions existent que nous verrons plus tard dans cette documentation.
Les outils des gestions
La gestion de Docker comprend la gestion d’éléments tels que les images, les conteneurs, les volumes et les réseaux. Pour simplifier cette gestion, vous pouvez opter pour une interface graphique conviviale plutôt que d’utiliser la ligne de commande. C’est précisément ce que vous offre l’outil Portainer.
Présentation et installation de Portainer
Portainer est une interface de gestion conviviale pour Docker, conçue pour simplifier la gestion des conteneurs Docker. Il s’agit d’un conteneur Docker en lui-même, ce qui signifie qu’il peut être facilement déployé sur votre système Docker. Portainer fonctionne en se connectant à l’API Docker de votre système. Il offre une interface web conviviale pour visualiser et gérer tous les aspects de Docker. Cela comprends entre autres la création et la gestion d’images, de conteneurs, de volumes et de réseaux. Vous n’avez pas besoin de mémoriser des commandes Docker complexes, car Portainer simplifie ces opérations courantes. Une fois connecté à Portainer via un navigateur web, vous pouvez effectuer des tâches telles que :
- le déploiement de nouveaux conteneurs à partir d’images existantes,
- la surveillance en temps réel des performances de vos conteneurs,
- la gestion de volumes de stockage, et la configuration de réseaux pour vos conteneurs.
Un des avantages de Portainer est sa simplicité d’installation et d’utilisation. Cet avantage fait de Portainer un outil idéal pour les utilisateurs novices de Docker.
Installation de Portainer et lancement du conteneur Docker
L’installation est également simplifiée, car Portainer est fourni sous forme d’image Docker, ce qui le rend facile à déployer. Globalement, Portainer facilite la gestion de Docker en rendant les opérations courantes plu simple.
L’image Docker que nous utiliserons est « portainer/portainer-ce :latest ». Nous mapperons deux ports :
- le 8000 qui servira au requête http
- le 9443 pour les requêtes https.
Nous ajouterons l’ argument « –restart=always ». Cet argument permet au conteneur de démarrer automatiquement à chaque redémarrage du service Docker.
De plus, Nous monterons deux volumes :
- -v /var/run/docker.sock:/var/run/docker.sock: On monte le socket Docker de l’hôte (/var/run/docker.sock) dans le conteneur. Cela permet à Portainer d’interagir avec le démon Docker de l’hôte, ce qui permettra la gestion des conteneurs.
- -v portainer_data:/data: Cet indicateur monte un volume nommé « portainer_data » dans le conteneur à l’emplacement « /data ». Ce volume est utilisé pour stocker les données de configuration et de stockage persistantes de Portainer.
En considérant tous les points que nous avons traités la commande sera la suivante :
[root@docker ~]# docker run -d -p 9000:9000 -p 9443:9443 -p 8000:8000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest
Dans l’exemple précédent, nous lançons le conteneur directement. Comme nous n’avons pas encore téléchargé l’image spécifiée dans la commande, Docker la télécharge automatiquement depuis la plateforme Docker Hub.
Nous configurerons notre pare-feu pour qu’autoriser les requêtes sur le port 9443 et 8000.
[root@docker ~]# firewall-cmd --add-port=8000/tcp --permanent
[root@docker ~]# firewall-cmd --add-port=9443/tcp --permanent
[root@docker ~]# firewall-cmd --reload
Accéder à l’interface web de Portainer
Depuis le navigateur d’une machine connectée sur le même réseau que notre machine exécutant Docker, nous allons nous rendre à l’url https://<IP_Hôte_Docker>:9443. Nous aurons un avertissement de sécurité vu que nous utilisons un certificat autosigné. Une fois sur l’interface, nous sommes invitées à renseigner un mot de passe pour l’utilisateur « admin ».
Présentation de l’interface web Portainer
Une fois que nous nous sommes connectés à l’interface d’administration, nous avons deux options disponibles. La première consiste à ajouter un environnement (via l’option « Add Environment »). Cette action permet la gestion des conteneurs situés chez des fournisseurs cloud (Azure Container Service, Amazon Elastic Container Service, etc). La seconde option est de commencer avec notre environnement local en cliquant sur « Get Started ». C’est cette dernière option que nous allons choisir.
Nous avons un bref récapitulatif de notre environnement local, dans la capture ci-dessous, nous pouvons voir que nous avons 4 conteneurs qui tourne, 2 volumes ou encore 5 images, nous avons également accès à des informations liée à l’hôte, nous voyons que notre hôte à 4 CPU et 3.8 Go de ram. Pour avoir plus de détail, nous cliquerons sur le bandeau « local ».
En cliquant sur le bandeau « local »,nous accédons à une interface qui offre plus d’information et plus d’options.
Sur la partie latérale, nous avons accès au différent composant de Docker. Cliquons sur Containers, nous devrions y vois nos conteneurs et leurs états.
Dans la section Images, nous avons accès à nos images, celle qui ne seront pas utilisé seront marquée « Unused ». Nous voyons, en haut de page, que le registry, c’est-à-dire l’endroit où Docker ira chercher les images par défaut. Il est tout à fait possible de télécharger les images Docker en indiquant son nom dans le champ images.
Dans la partie réseau, il nous sera possible de voir les réseaux qui ont été créé à l’installation de Docker. Ils sont reconnaissables au tag « system » mais aussi ceux que nous avons créé.
En cliquant sur le nom du réseau, nous connaitrons les conteneurs qui y sont connecté. Par exemple, reseau_php7 contient deux conteneurs « rp » et « app-php7 ».
La logique restera un peu la même pour les volumes.
La gestion des images Docker
Lorsque nous créons des images, elles sont initialement stockées sur notre propre ordinateur. Cependant, à mesure que nos projets se développent, il peut devenir essentiel de partager ces images personnalisées . La première idée qui vient souvent à l’esprit est de stocker ces images sur une plateforme publique, telle que Docker Hub.
Néanmoins, il est important de noter que la version gratuite de Docker Hub présente certaines limitations. L’une des limitations principales est la capacité totale de stockage, qui peut être restreinte. Pour des projets de petite envergure, cette limite peut être suffisante. Cependant, pour des projets plus importants ou des équipes de développement collaboratives, cette capacité limitée peut rapidement devenir un obstacle.
De plus, la version gratuite peut également imposer des restrictions :
- de bande passante
- de vitesse de téléchargement
- de stockage.
Ces restrictions peut affecter la rapidité avec laquelle vous pouvez distribuer vos images. Pour les projets nécessitant un espace de stockage plus important, il peut être judicieux de créer un registre Docker privé. Cette option offrira un contrôle total sur l’espace de stockage, la bande passante et la vitesse.
Un autre aspect important à considérer lors de l’utilisation de Docker Hub en version gratuite sont par défaut publiques. Cela signifie que quiconque connaît le nom de référence de votre image peut y accéder et la télécharger. Cette caractéristique peut être avantageuse pour les projets open source ou les collaborations publiques. Dans les autres cas elle peut poser des problèmes de sécurité ou de confidentialité pour les projets sensibles ou privé. Il est nécessaire de prendre en compte cette caractéristique publique lors du choix de l’outils qui hébergera vos images Docker. Si la confidentialité est une préoccupation, envisagez de passer à un abonnement payant sur Docker Hub. L’autre option c’est de créer sont propres Docker Hub. Nous appelons un Docker Hub privé un Registry local. Pour certains projets, la création de votre propre registre Docker privé peut être la meilleure solution avoir un contrôle totale.
Mise en place d’un conteneur privé local
Dans cette section, nous allons explorer le processus d’installation d’un registre Docker privé. À noter que cet outil était autrefois appelé « Docker Registry », a été renommé en « Docker Distribution » dans sa version 2.
Nous téléchargerons l’image depuis le portail Docker hub.
[root@docker ~]# docker pull registry
Nous créerons ensuite un répertoire sur notre machine local pour y stocker les images.
[root@docker ~]# mkdir -p /var/lib/registry
Nous allons ensuite démarrer notre instance docker-registry
[root@docker ~]# docker run -d -p 5000:5000 --restart=always --name registry -v /var/lib/registry:/var/lib/registry registry:latest
Ajout dimage dans Docker registry local
Nous allons ensuite peupler ce registre en y envoyant nos images que nous avons créé précédemment. Il nous faudra modifier le « tag » puis pousser l’image vers notre registre.
[root@docker ~]# docker tag srv_web_php8 localhost:5000/srv_web_php8
[root@docker ~]# docker push localhost:5000/srv_web_php8
En nous rendant dans le catalogue de notre registre, nous pouvons remarquer que nous voyons notre image.
[root@docker ~]# curl http://localhost:5000/v2/_catalog
Nous suivrons la même approche pour les images « reverse-proxy » et « srv_web_php7 ». Tout d’abord, nous allons modifier le tag de ces images :
[root@docker ~]# docker tag srv_web_php7 localhost:5000/srv_web_php7
[root@docker ~]# docker tag reverse-proxy localhost:5000/reverse_proxy
Ensuite, nous allons les téléverser vers notre registry local :
[root@docker ~]# docker push localhost:5000/srv_web_php7
[root@docker ~]# docker push localhost:5000/reverse_proxy
De cette manière, notre catalogue d’images dans le registry local s’enrichira avec ces nouvelles images. Le déplacement d’une image vers un autre dépôt n’entraîne pas la suppression de l’image d’origine. En réalité, l’attribution d’un tag à une image n’est qu’un alias. Cela devient évident lorsque nous examinons notre liste d’images en filtrant :
- par la chaîne de caractères « srv » pour nos images « srv_web_php7 » et « srv_web_php8 »,
- par « reverse» pour notre image « reverse-proxy ».
[root@docker ~]# docker images --format {{.Repository}} | grep -E 'srv|reverse'
Nous supprimerons les images que nous avons transférer dans notre repository local.
[root@docker ~]# docker rmi reverse-proxy srv_web_php8 srv_web_php7
Ajouter le Docker registry en utilisant Portainer
Nous ajouterons ce repository dans Portainer, après nous être connecter, nous sélectionnerons dans le menu latéral gauche « Registries ».
Nous cliquerons ensuite sur le bouton « Add registry ».
Dans la page d’ajout de registry, nous sélectionnerons « Custom Registry », puis dans le formulaire en dessous, nous indiquerons un nom, puis l’adresse de notre registry, dans notre contexte, Portainer et notre registry sont installés sur la même machine, nous indiquerons donc notre adresse IP localhost avant de cliquer sur le bouton « Add Registry ».
De retour sur notre pages « Regestries », notre registry est maintenant listé et donc ajouté.
Pour conclure cette partie, rappelons qu’un registry permet de centraliser les images de conteneurs dans un seul emplacement, ce qui facilite la gestion et le partage des images entre les équipes. Un registre Docker peut également être utilisé pour améliorer la sécurité et les performances des applications conteneurisées.
Automatisation des créations des images
La création d’un conteneur n’est en fait que l’exécution d’une image dans laquelle nous allons installer un ensemble d’outil, de librairies, de framework, etc… Avant de déployer un conteneur, il nous faut donc créer des images que nous allons adapter à nos besoins. Cette création passera par l’installation des éléments précité mais aussi par des éléments de configurations spécifique à notre conteneur, par exemple, les volumes, le réseaux, etc…
Les Dockerfiles
Il pourrait s’avérer fastidieux de recréer cette image à chaque que nous souhaitons la reproduire. Au lieu de cela, nous créer un fichier qui contiendras toutes les instructions pour créer notre image docker personnalisé. Ce fichier s’appelle un DockerFile, il listera un ensemble d’instruction qui sont défini par des mots clés. En voici une liste non-exhaustive :
FROM : Spécifie l’image de base à partir de laquelle vous allez construire votre image. L’argument est le nom de l’image de base, par exemple : FROM ubuntu:20.04.
RUN : Exécute une commande dans l’image pour installer des logiciels, mettre à jour des paquets, créer des répertoires, etc. L’argument est la commande à exécuter, par exemple : RUN apt-get update && apt-get install -y apache2.
CMD : Définit la commande par défaut à exécuter lorsque le conteneur est lancé. L’argument est la commande, généralement sous forme d’une liste, par exemple : CMD [« apachectl », « -D », « FOREGROUND »].
EXPOSE : Spécifie les ports sur lesquels le conteneur écoutera. L’argument est le numéro de port ou une plage de ports, par exemple : EXPOSE 80.
ENV : Définit des variables d’environnement pour l’image. L’argument est une paire clé-valeur, par exemple : ENV MY_VARIABLE= »valeur ».
ADD : Copie des fichiers depuis le système de fichiers hôte dans l’image, tout en permettant des opérations plus avancées. L’argument est le chemin source et de destination, par exemple : ADD app.tar.gz /app.
COPY : Copie des fichiers depuis le système de fichiers hôte dans l’image, sans les opérations avancées offertes par ADD. L’argument est le chemin source et de destination, par exemple : COPY index.html /var/www/html/.
WORKDIR : Définit le répertoire de travail actuel pour les instructions suivantes dans le Dockerfile. L’argument est le chemin du répertoire, par exemple : WORKDIR /app.
USER : Spécifie l’utilisateur ou l’identifiant de l’utilisateur sous lequel les commandes RUN, CMD, et ENTRYPOINT sont exécutées. L’argument est le nom de l’utilisateur ou l’identifiant.
VOLUME : Crée un point de montage de volume pour le stockage de données persistantes en dehors du conteneur. L’argument est le chemin du point de montage, par exemple : VOLUME /data.
En somme toutes les étapes que nous avons réalisées pour créer nos images peuvent être fait à l’aide d’un DockerFile.
Créer un DockerFile
Dans cette mise en pratique nous allons créer un dockerFile qui nous permettra d’exécuter PHP en version 7.4.
Nous commencerons par créer un répertoire de travail.
[root@docker ~]# mkdir /home/MonProjet
Dans ce répertoire, nous allons créer un fichier, qui devra se nommer Dockerfile
[root@docker ~]# vi /home/MonProjet/Dockerfile
Dans ce fichier, nous allons renseigner les instructions pour la création de mon image. Nous commencerons par indiquer l’image que nous souhaitons utiliser.
# Utilisation d’une image Ubuntu de base
FROM ubuntu:latest
Nous supprimerons toutes les interactivités imposée lors de l’installation de paquets
ENV DEBIAN_FRONTEND=nonintercative
Nous mettons ensuite à jours la distribution puis nous installons le service Apache2
# Mise à jour des packages et installation d'Apache2
RUN apt update -y && apt install -y apache2
Nous installerons les dépendances nécessaires à l’installation de PHP 7.4 puis nous installerons PHP
# Installation de PHP7.4
RUN apt install software-properties-common -y && add-apt-repository ppa:ondrej/php -y && apt install php7.4 -y
Nous exposerons le port 80 de notre image
# Exposer le port 80 pour Apache
EXPOSE 80
Nous allons ensuite créer le volume pour rendre les données persistantes. Pour cette étape, nous créerons le répertoire sur notre machine. Ensuite nous le monterons dans un volume de notre image
# Créez un répertoire pour les données d'Apache et configurez un point de montage de volume
RUN mkdir /var/www/html/data
VOLUME /var/www/html/data
Nous créerons un fichier PHP qui affichera les informations PHP.
# Créez un fichier PHP qui exécute phpinfo()
RUN echo "<?php phpinfo(); ?>" > /var/www/html/data/phpinfo.php
Nous démarrerons ensuite PHP
# Démarrer Apache en premier plan
CMD ["apachectl", "-D", "FOREGROUND"]
Executer un DockerFile
La liste de ses instructions, vont être exécuté lorsque nous créerons notre image à l’aide de la commande « docker build ». Nous allons nous rendre dans le répertoire de travail où se trouver notre Dockerfile.
[root@docker ~]# cd /home/MonProjet/
Dans ce répertoire, nous exécuterons la commande de création de notre image, en lui donnant un nom.
[root@docker MonProjet]# docker build -t php74 .
Une fois la commande exécutée, nous voyons les étapes de la création.
Nous vérifierons que l’images à bien été ajouter à notre registre interne.
[root@docker MonProjet]# docker images | grep php74
Testons cette images, après avoir créé notre instance, nous tenterons d’accéder à notre page « phpinfo.php »
[root@docker MonProjet]# docker run -d -p 80:80 php74
Création et gestion de multi-conteneur avec Docker-compose
L’un des principaux avantages de la conteneurisation est de pouvoir partitionner un serveur en services distincts. Cela permet de déployer et de gérer chaque service de manière indépendante. Dans le cas d’une machine virtuelle, tous les services sont exécutés sur le même système d’exploitation. Dans le cas de mise à jour, nous devons souvent arrêter la machine virtuelle. L’arrét de cette VM peut entraîner des interruptions de service et des pertes financières. Avec la conteneurisation, chaque service est exécuté dans son propre conteneur. Les conteneurs partagent le noyau du système d’exploitation, les rendant plus légers et plus portables que les machines virtuelles. Les Docker-compose permet de déployer des services rapidement et facilement, et de les mettre à l’échelle de manière dynamique.
Docker Compose est un service qui permet d’envoyer des instructions à Docker, telles que la spécification des dépendances, des informations de configuration et des interactions, afin de créer et de gérer des conteneurs Docker pour une application multi-conteneurs. Les instructions sont données sous forme de mot clé dont en voici les principaux utilisés :
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Les instructions doivent être contenu dans un fichier qui se nommera « docker-compose.yml ». Dans ce Docker compose, nous allons pouvoir faire des appels à des Dockerfiles. Dans un premier temps nous nous concentrerons sur Docker compose. Cela nous permettra de comprendre sont fonctionnement. Dans un second temps nous verrons comment faire fonctionner Docker-compose avec des Dockerfiles.
Exemple de Docker-compose
Pour illustrer, nous prendrons comme exemple la conteneurisation d’un stack LAMP (Linux Apache MariaDB et PHP) où nous installerons WordPress. Avant cela, nous allons avoir besoin d’installer l’outils Docker-Compose. Docker-compose est un outil écrit en Python et il est distribué via le gestionnaire de paquet Python appelé « pip ».
Nous installerons le gestionnaire pip puis nous installerons Docker-Compose.
[root@docker ~]# dnf install -y python3-pip
[root@docker ~]# pip3 install docker-compose
Nous créerons ensuite un répertoire de travail.
[root@docker ~]# mkdir /home/Stack-LAMP
Après être entrés dans le répertoire que nous avons créé précédemment, nous créeronsons un fichier nommé « docker-compose.yml ». Nous éditerons ce fichier à l’aide d’un éditeur de texte en ligne de commande (nano, vi, etc…)
[root@docker ~]# cd /home/Stack-LAMP/
[root@docker Stack-LAMP]# vi docker-compose.yml
Pour rappel, les fichiers YAML à une syntaxe basée sur l’indentation (espaces ou tabulations). Cette contrainte rend le code plus lisible mais exige une attention particulière à la mise en forme. Rappelons également YAML est sensible à la case.
En premier lieu, nous allons définir la version de la syntaxe qui sera utilisé dans ce fichier. La version de la syntaxe est liée à la version de Docker Engine installée sur la machine. Dans notre exemple, nous sommes en version 24.0.5, nous utiliserons la version 3.8.
Nous commencerons donc notre fichier à déclarer la version de la syntaxe.
version: '3.8'
Ensuite, nous allons spécifier nos services : un container hébergera la base de données, le second hébergera le serveur web et l’application WordPress. Ses informations seront renseignées dans la section « services ».
version: '3.8'
services :
#Service base de données
db :
#Instruction
# Web Server et wordpress
wordpress :
#Instruction
Dans les instructions du conteneur db, nous indiquerons l’images que nous utiliserons, à savoir l’image officiel de mariadb dans sa dernière version, les données de la base de données devront être persistante, nous montrons donc un volume.
services :
#Service base de données
db:
image: mariadb:latest
volumes:
- db_data:/var/lib/mysql
# Web Server et wordpress
wordpress :
#Instruction
Nous allons définir les variables d’environnement pour paramétrer la base de données, en spécifiant un mot de passe pour l’utilisateur root. Nous créerons également un utilisateur dédié à la gestion de la base de données WordPress, avec un identifiant et un mot de passe spécifiques, et créerons une base de données dédiée à WordPress.
services :
#Service base de données
db:
image: mariadb:latest
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: p@ssw0rd1
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: p@ssw0rd1
# Web Server et wordpress
wordpress :
#Instruction
Le conteneur de notre base de données étant configuré, nous pouvons maintenant passer à la création de notre conteneur WordPress. Nous indiquerons que le conteneur « wordpress » dépend du conteneur « db », cela aura pour conséquence que Docker s’assurera que le container « db» soit démarrer avant le container « Apache ».
services :
#Service base de données
db:
#+---------------+
#|Instruction db |
#+---------------+
# Web Server et wordpress
wordpress :
depends_on:
- db
image: wordpress:latest
Le conteneur « wordpress » sera en écoute sur le port 80 de la machine hôte, il nous faut donc mapper le port 80 de notre conteneur (qui est le port pas défaut d’écoute du serveur web) sur le port 80 de la machine hôte.
services :
#Service base de données
db:
#+---------------+
#|Instruction db |
#+---------------+
# Web Server et wordpress
wordpress :
depends_on:
- db
image: wordpress:latest
ports:
- "80:80"
Les données de l’application seront persistantes, nous créerons le volumes pour stocker les fichiers de WordPress.
services :
#Service base de données
db:
#+---------------+
#|Instruction db |
#+---------------+
# Web Server et wordpress
wordpress :
depends_on:
- db
image: wordpress:latest
ports:
- "80:80"
volumes:
- wordpress_data:/var/www/html
Pour conclure la configuration du conteneur, nous configurerons notre environnement pour permettre à WordPress de se connecter à la base de donnée que nous avons préparer dans le conteneur « db ».
services :
#Service base de données
db:
#+---------------+
#|Instruction db |
#+---------------+
# Web Server et wordpress
wordpress :
depends_on:
- db
image: wordpress:latest
ports:
- "80:80"
volumes:
- wordpress_data:/var/www/html
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: p@ssw0rd1
WORDPRESS_DB_NAME: wordpress
Pour assurer la persistance des données des volumes utilisées par nos conteneurs, nous devons les déclarés explicitement. Nous monterons le volumes que nous avons utilisées dans nos conteneurs.
services :
#Service base de données
db:
#+---------------+
#|Instruction db |
#+---------------+
# Web Server et wordpress
wordpress :
depends_on:
- db
image: wordpress:latest
ports:
- "80:80"
volumes:
- wordpress_data:/var/www/html
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: p@ssw0rd1
WORDPRESS_DB_NAME: wordpress
volumes:
db_data:
wordpress_data:
Nous exécuterons ce docker-compose à l’aide de la commande suivante :
[root@docker Stack-LAMP]# docker compose up
Après avoir exécuter la commande, un certain nombre de tâche s’exécute, notamment les téléchargement des images utilisées dans le fichier docker-compose.yml. Une fois que toutes les tâches ont été exécuté, nous testerons l’accès à notre serveur WordPress depuis un navigateur.
Exécution de nos conteneur en arriere plan avec Docker-compose
Notre serveur est pleinement opérationnel. Ceux-ci étant dit, nous avons perdu l’accès à notre terminal, en effet, il est attaché à nos conteneurs, si nous le détachons cela provoquera l’arrêt des conteneurs. Pour exécuter les containeurs en arrière-plan, nous devons ajouter le commutateur « -d », lorsque nous exécutons le docker-compose. Pour tester cette commande, nous allons supprimer ce que nous avons fait, nous tuerons nos conteneurs à l’aide de la combinaison de touche [ctrl]+[c].
Nous supprimerons les services à l’aide de la commande suivante :
[root@docker Stack-LAMP]# docker compose down
Dans notre docker-compose des volumes sont créés, nous les supprimerons afin de repartir d’une configuration initiale, nous listerons les volumes pour récupérer leurs noms complets.
[root@docker Stack-LAMP]# docker volume list | grep "stack-lamp"
[root@docker Stack-LAMP]# docker volume rm stack-lamp_db_data stack-lamp_wordpress_data
Nous exécutons à nouveau le docker-compose en précisant l’option « -d ».
Docker Compose offre une approche déclarative, simplifiant la définition complète d’une application, de ses services, de ses dépendances et de ses configurations dans un unique fichier (docker-compose.yml). Cette approche favorise un déploiement homogène et reproductible en décrivant l’état souhaité sans spécifier les détails de mise en œuvre.
Pour exploiter pleinement Docker Compose, il est essentiel de comprendre comment l’associer à un autre élément clé de l’écosystème Docker que nous avons vu plus tôt : les Dockerfiles.
Intégration de Dockerfile dans Docker Compose
Pour ce chapitre, nous utiliserons la même étude de cas que celle utilisés dans le chapitre précédent, c’est-à-dire une stack LAMP qui fera tourner un WordPress. Nous aurons un Dockerfile par conteneur.
Pour ce projet, nous créer un nouveau répertoire sur notre machine hôte, puis nous allons nous y rendre.
[root@docker ~]# mkdir /home/stack-lamp-compose-dockerfile
[root@docker ~]# cd /home/stack-lamp-compose-dockerfile
Dans ce repertoire nous créerons notre premier dockerfile pour le conteneur « mariadb ».
[root@docker stack-lamp-compose-dockerfile]# vi dockerfile-mariadb
Contenu du dockerfile MariaDB
# Utilisez l'image officielle MariaDB
FROM mariadb:latest
# Définir les variables d'environnement
ENV MYSQL_ROOT_PASSWORD=p@ssw0rd1 \
MYSQL_DATABASE=wordpress \
MYSQL_USER=wordpress_user \
MYSQL_PASSWORD=p@ssw0rd1
# Copiez le script d'initialisation
COPY init.sql /docker-entrypoint-initdb.d/
# Exposez le port 3306 pour MariaDB
EXPOSE 3306
Dans ce dockerfile, nous allons copier un fichier « init.sql », ce fichier que nous allons créer contiendras les requêtes SQL :
- Création de la base de données
- Création de l’utilisateur
- La gestion des droits de l’utilisateur sur la base de données
[root@docker stack-lamp-compose-dockerfile]# vi init.sql
Contenu init.sql
CREATE DATABASE IF NOT EXISTS wordpress;
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress_user'@'%' IDENTIFIED BY 'p@ssw0rd1';
FLUSH PRIVILEGES;
Ce script sql devra avoir le droit en exécution.
[root@docker stack-lamp-compose-dockerfile]# chmod +x init.sql
Nous créerons également le Dockerfile pour le serveur web et WordPress.
[root@docker stack-lamp-compose-dockerfile]# vi dockerfile-wordpress
Contenu du Dockerfile WordPress
# Utilisez l'image Ubuntu la plus récente
FROM ubuntu:latest
# Mises à jour et installations nécessaires
RUN apt-get update \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get install -y apache2 php libapache2-mod-php php-mysql mysql-client php-gd php-curl php-zip php-mbstring php-xml wget \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
&& cd /var/www/html/
&& rm index.html
# Télécharger et configurer WordPress
RUN cd /var/www/html && \
wget https://wordpress.org/latest.tar.gz && \
tar -xzvf latest.tar.gz --strip-components=1 && \
rm latest.tar.gz && \
mv wp-config-sample.php wp-config.php && \
sed -i 's/database_name_here/wordpress/g' wp-config.php && \
sed -i 's/username_here/wordpress_user/g' wp-config.php && \
sed -i 's/password_here/p@ssw0rd1/g' wp-config.php && \
sed -i 's/localhost/mariadb-server/g' wp-config.php && \
chown -R www-data:www-data /var/www/html
# Attribue une adresse IP au serveur web
CMD ["apache2ctl", "-D", "FOREGROUND"]
# Expose le port 80 pour Apache
EXPOSE 80
Nous créerons le fichier Docker-Compose qui s’appuiera sur les deux Dockerfile que nous avons créé.
version: '3.8'
services:
# Service pour le serveur web Apache
web-server:
container_name: wp
build:
context: .
dockerfile: dockerfile-wordpress
depends_on:
- mariadb-server
ports:
- "80:80"
# Service pour la base de données MariaDB
mariadb-server:
container_name: mariadb-server
build:
context: .
dockerfile: dockerfile-mariadb
Nous pouvons tester que notre déploiement de WordPress est maintenant fonctionnel.
[root@docker stack-lamp-compose-dockerfile]# docker-compose up -d
Depuis un navigateur, nous allons tenter de nous connecter à notre serveur WEB.
L’approche qui consiste à combiner de Docker Compose et des Dockerfiles segmente le travail de développement et de déploiement d’applications en conteneurs en introduisant une séparation claire des responsabilités. Cette segmentation se manifeste à plusieurs niveaux.
Tout d’abord, les Dockerfiles sont dédiés à la description détaillée de la construction des images de conteneurs. Les développeurs se concentrent sur la définition des dépendances, des configurations, et des étapes d’installation spécifiques à chaque composant de l’application. Cette séparation des préoccupations permet aux développeurs de se focaliser sur la construction et la maintenance des images conteneurs, sans nécessairement avoir à gérer la configuration spécifique des environnements de déploiement.
En parallèle, Docker Compose intervient au niveau de la composition et de la gestion de l’ensemble de l’application. Les opérations liées à l’orchestration des conteneurs, la gestion des réseaux, et la coordination entre les différents services sont encapsulées dans le fichier docker-compose.yml. Les équipes opérationnelles peuvent ainsi se concentrer sur la gestion des aspects liés au déploiement global de l’application, en utilisant une configuration déclarative simplifiée.
Cette segmentation du travail permet une collaboration plus fluide entre les équipes de développement et d’exploitation. Les développeurs peuvent se concentrer sur la création d’environnements conteneurisés spécifiques à leurs applications, tandis que les opérations peuvent prendre en charge l’orchestration et la gestion des déploiements à grande échelle. En résumé, l’utilisation de Docker Compose et des Dockerfiles encourage une approche modulaire et efficace du développement et du déploiement d’applications en conteneurs.