ajoute le portail Laravel : connexion prof/élève, session unique, gestion de classe, salle d'attente
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
aa31b6112f
commit
30e14b1c54
191 changed files with 25324 additions and 0 deletions
182
portail/public/js/contenus-devops-1.js
Normal file
182
portail/public/js/contenus-devops-1.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Cours complets · DevOps lot 1 : Docker, Docker Compose, Ansible
|
||||
(function () {
|
||||
var reg = function (c) {
|
||||
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
|
||||
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
|
||||
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
|
||||
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
|
||||
};
|
||||
|
||||
reg({
|
||||
cours: { id: 'docker', match: /Docker(?! Compose)|Dockerfile|conteneur/i,
|
||||
titre: 'Conteneurs Docker : images, Dockerfile, réseaux et volumes', duree: '',
|
||||
niveau: 'BTS SIO SISR / SLAM · BTS CIEL IR · BUT R&T parcours DevCloud · AIS · Master',
|
||||
prerequis: ['Le terminal Linux', 'Processus et services systemd', 'Réseau : ports, NAT'],
|
||||
competences: ['BTS SIO SISR · Bloc 2 · Virtualisation et conteneurs', 'BTS SIO SLAM · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Compétence « Automatiser le déploiement »', 'AIS · Administrer une infrastructure virtualisée'],
|
||||
intro: '« Chez moi ça marche. » Pendant des années, le passage d’une application du poste du développeur au serveur de production a été la principale source d’incidents : version de PHP différente, bibliothèque manquante, configuration oubliée. Le conteneur règle ce problème en livrant l’application avec tout son environnement d’exécution, dans une image immuable qui s’exécute à l’identique partout. Docker a popularisé cette approche en 2013 ; elle est aujourd’hui le socle du déploiement moderne, des petites applications de lycée jusqu’aux plateformes Kubernetes des hébergeurs. Ce module explique ce qu’est réellement un conteneur pour le noyau Linux, comment construire des images propres et sûres, et comment les exploiter en production.',
|
||||
cm: [
|
||||
{ t: 'Ce qu’est un conteneur pour le noyau', d: '', html: 'Un conteneur n’est pas une machine virtuelle. Il n’y a ni matériel émulé ni second noyau : c’est un <b>processus Linux ordinaire</b>, que le noyau isole grâce à deux mécanismes. Les <b>namespaces</b> donnent au processus sa propre vue du système : arborescence de processus (pid), interfaces réseau (net), points de montage (mnt), nom d’hôte (uts), utilisateurs (user), communications inter-processus (ipc). Les <b>cgroups</b> (control groups) limitent et mesurent les ressources qu’il consomme : processeur, mémoire, entrées-sorties.<br><br>Conséquences directes : un conteneur démarre en une fraction de seconde, consomme très peu de mémoire en plus de son application, et partage le noyau de l’hôte. C’est aussi sa limite de sécurité : une faille du noyau touche tous les conteneurs. Une VM isole plus fortement, un conteneur est plus léger ; en production, on combine souvent les deux (conteneurs dans des VM).', table: [['', 'Machine virtuelle', 'Conteneur'], ['Isolation', 'hyperviseur, noyau séparé', 'namespaces et cgroups, noyau partagé'], ['Démarrage', 'dizaines de secondes', 'fraction de seconde'], ['Taille', 'plusieurs Go', 'quelques Mo à centaines de Mo'], ['OS invité', 'n’importe lequel', 'même noyau que l’hôte (Linux)'], ['Usage type', 'serveur complet, isolation forte', 'application et ses dépendances']] },
|
||||
{ t: 'Images, couches et registres', d: '', html: 'Une <b>image</b> est un modèle en lecture seule, composé de <b>couches</b> superposées : chaque instruction d’un Dockerfile qui modifie le système de fichiers ajoute une couche. Le pilote de stockage (overlay2) les empile et ajoute au-dessus une couche inscriptible propre à chaque conteneur. Plusieurs conteneurs issus de la même image partagent ses couches sur le disque.<br><br>Une image est identifiée par un nom, un <b>tag</b> et un <b>digest</b> (empreinte SHA-256 du contenu). Le tag est une étiquette mobile : <code>nginx:latest</code> ne désigne pas la même image aujourd’hui et dans six mois. En production, on fixe une version précise (<code>nginx:1.27.2-alpine</code>) voire le digest, pour que chaque déploiement soit reproductible.<br><br>Les images sont stockées dans des <b>registres</b> : Docker Hub, GitHub Container Registry, ou un registre privé (Harbor, Forgejo, GitLab). <code>docker pull</code> télécharge, <code>docker push</code> publie.', attention: 'Ne jamais utiliser le tag latest en production : un redémarrage peut récupérer silencieusement une nouvelle version majeure et casser l’application.' },
|
||||
{ t: 'Écrire un Dockerfile de production', d: '', html: 'Le <b>Dockerfile</b> décrit la construction d’une image. Chaque instruction produit une couche, et Docker réutilise en cache les couches dont les instructions et les fichiers n’ont pas changé. L’ordre est donc décisif : on place en premier ce qui change rarement (système, dépendances) et en dernier ce qui change souvent (code).<br><br>Quatre bonnes pratiques font la différence entre un Dockerfile de TP et un Dockerfile de production. Partir d’une <b>image de base minimale et versionnée</b> (debian:bookworm-slim, alpine, distroless). Utiliser un <b>build multi-étapes</b> : on compile dans une première image complète, et on ne copie que le résultat dans une image finale légère, sans compilateurs. <b>Ne pas exécuter en root</b> : créer un utilisateur et le déclarer avec USER. Ajouter un <b>.dockerignore</b> pour ne pas envoyer .git, les secrets ou node_modules dans le contexte de construction.', exemple: '# syntax=docker/dockerfile:1\nFROM python:3.12-slim AS build\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --prefix=/install -r requirements.txt\n\nFROM python:3.12-slim\nRUN useradd --create-home --uid 10001 app\nCOPY --from=build /install /usr/local\nWORKDIR /app\nCOPY --chown=app:app . .\nUSER app\nEXPOSE 8000\nHEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen(\'http://localhost:8000/health\')"\nCMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]' },
|
||||
{ t: 'Cycle de vie d’un conteneur', d: '', html: '<code>docker run</code> crée et démarre un conteneur à partir d’une image. Les options essentielles : <code>-d</code> en arrière-plan, <code>--name</code> pour le nommer, <code>-p 8080:80</code> pour publier un port, <code>-e</code> pour une variable d’environnement, <code>-v</code> pour un volume, <code>--restart unless-stopped</code> pour le redémarrer automatiquement, <code>--memory</code> et <code>--cpus</code> pour le limiter.<br><br>Le processus lancé par CMD ou ENTRYPOINT est le <b>PID 1</b> du conteneur : quand il s’arrête, le conteneur s’arrête. C’est pourquoi on ne lance pas un service en arrière-plan dans un conteneur, et pourquoi le programme doit traiter correctement le signal SIGTERM envoyé par <code>docker stop</code> (au bout de 10 secondes, Docker envoie SIGKILL).<br><br>Un conteneur est <b>jetable</b> : on ne le modifie pas à la main, on le recrée à partir d’une nouvelle image. Tout ce qui est écrit dans sa couche inscriptible disparaît avec <code>docker rm</code>.' },
|
||||
{ t: 'Persister les données : volumes et bind mounts', d: '', html: 'Puisque le conteneur est jetable, les données qui doivent survivre (base de données, fichiers envoyés, configuration) vont dans un <b>volume</b>. Un <b>volume nommé</b> est géré par Docker dans /var/lib/docker/volumes : c’est la solution recommandée pour les données d’application. Un <b>bind mount</b> monte un dossier précis de l’hôte : pratique pour la configuration ou le développement, mais il dépend de l’arborescence de l’hôte et de ses droits.<br><br>La sauvegarde ne se fait pas en copiant un volume de base de données à chaud : on utilise l’outil de la base (pg_dump, mariadb-dump) depuis le conteneur, ou on arrête le service le temps de la copie.', exemple: 'docker volume create pgdata\ndocker run -d --name db -e POSTGRES_PASSWORD_FILE=/run/secrets/pg -v pgdata:/var/lib/postgresql/data postgres:16\ndocker exec db pg_dump -U postgres app > app-$(date +%F).sql' },
|
||||
{ t: 'Réseaux Docker', d: '', html: 'Par défaut, chaque conteneur est relié au réseau <b>bridge</b> docker0 et reçoit une adresse privée en 172.17.0.0/16. Pour être joignable de l’extérieur, un port doit être publié avec <code>-p</code> : Docker crée alors une règle de <b>DNAT</b> dans netfilter vers le conteneur.<br><br>Sur un <b>réseau bridge utilisateur</b> (<code>docker network create</code>), les conteneurs se trouvent par leur <b>nom</b> grâce au DNS intégré de Docker (127.0.0.11) : l’application joint la base de données par le nom db, sans connaître son adresse. C’est la pratique recommandée. Le mode <b>host</b> supprime l’isolation réseau : le conteneur utilise directement les interfaces de l’hôte.', attention: 'Les ports publiés par Docker passent par ses propres règles netfilter, avant celles d’un pare-feu comme UFW : un -p 5432:5432 expose la base de données sur Internet même si UFW semble la bloquer. On publie uniquement sur la boucle locale (-p 127.0.0.1:5432:5432) ou pas du tout, en utilisant un réseau interne.' },
|
||||
{ t: 'Sécurité des conteneurs', d: '', html: 'Appartenir au groupe <b>docker</b> équivaut à être root sur l’hôte : on peut lancer un conteneur qui monte / et le modifier. On réserve ce groupe aux administrateurs, ou on utilise Docker en mode <b>rootless</b> ou Podman.<br><br>Les règles de base : images officielles ou construites en interne, <b>analyse de vulnérabilités</b> des images (Trivy, Docker Scout) dans la chaîne d’intégration, exécution en utilisateur non privilégié, jamais de <code>--privileged</code> sans raison impérative, secrets passés par fichiers ou gestionnaire de secrets plutôt qu’en variables d’environnement visibles dans <code>docker inspect</code>, et limites de ressources pour qu’un conteneur ne puisse pas saturer l’hôte.' }
|
||||
],
|
||||
synthese: ['Conteneur = processus isolé par namespaces et limité par cgroups, noyau partagé', 'Image en couches, immuable, identifiée par tag et digest ; jamais latest en production', 'Dockerfile : base minimale versionnée, multi-étapes, USER non root, .dockerignore', 'PID 1 et SIGTERM : un processus au premier plan par conteneur', 'Données dans des volumes nommés, sauvegarde avec les outils de la base', 'Réseau bridge utilisateur et DNS par nom ; attention aux ports publiés qui contournent UFW', 'Groupe docker = root ; scanner les images, limiter les ressources'],
|
||||
qcm: [
|
||||
['Quels mécanismes du noyau isolent un conteneur ?', ['Hyperviseur et BIOS', 'Namespaces et cgroups', 'chroot et sudo', 'SELinux uniquement'], 1, 'Namespaces pour la vue, cgroups pour les ressources.'],
|
||||
['Pourquoi éviter le tag latest en production ?', ['Il est payant', 'Il peut désigner une nouvelle version à chaque pull', 'Il est lent', 'Il n’existe pas'], 1, 'Le déploiement devient non reproductible.'],
|
||||
['Quel est l’intérêt d’un build multi-étapes ?', ['Accélérer le réseau', 'Obtenir une image finale sans outils de compilation', 'Lancer plusieurs conteneurs', 'Chiffrer l’image'], 1, 'Image plus petite et surface d’attaque réduite.'],
|
||||
['Que se passe-t-il quand le processus PID 1 d’un conteneur se termine ?', ['Rien', 'Le conteneur s’arrête', 'Docker le relance toujours', 'L’hôte redémarre'], 1, 'Le conteneur vit tant que son processus principal vit.'],
|
||||
['Où stocker les données d’une base PostgreSQL conteneurisée ?', ['Dans la couche du conteneur', 'Dans un volume nommé', 'Dans l’image', 'Dans /tmp'], 1, 'Elles survivent à la suppression du conteneur.'],
|
||||
['Comment une application joint-elle la base de données sur un réseau bridge utilisateur ?', ['Par l’adresse 172.17.0.1', 'Par le nom du conteneur', 'Par localhost', 'Impossible'], 1, 'DNS intégré de Docker.'],
|
||||
['Pourquoi -p 5432:5432 peut-il exposer la base malgré UFW ?', ['UFW est désactivé par Docker', 'Docker insère ses règles DNAT avant celles d’UFW', 'Le port 5432 est public par nature', 'Ce n’est pas possible'], 1, 'Publier sur 127.0.0.1 ou ne pas publier.'],
|
||||
['Être membre du groupe docker revient à :', ['n’avoir aucun droit', 'être root sur l’hôte', 'pouvoir seulement lire les journaux', 'être administrateur réseau'], 1, 'On peut monter / dans un conteneur.']
|
||||
],
|
||||
td: [
|
||||
{ n: 'Échauffement', c: 'Classer VM ou conteneur : (a) serveur Windows pour un logiciel métier, (b) API Python déployée dix fois par jour, (c) isolation forte entre deux clients hébergés, (d) base de tests jetable pour chaque élève.', r: '(a) VM · (b) conteneur · (c) VM (ou conteneurs dans des VM séparées) · (d) conteneur' },
|
||||
{ n: 'Application', c: 'Réécrire ce Dockerfile pour la production : « FROM python:latest / COPY . /app / RUN pip install -r /app/requirements.txt / CMD python /app/app.py ».', r: 'Version fixée (python:3.12-slim), COPY requirements.txt avant le code pour le cache, pip --no-cache-dir, utilisateur non root et USER, .dockerignore, CMD en forme exec ["python","app.py"], éventuellement multi-étapes et HEALTHCHECK.' },
|
||||
{ n: 'Application', c: 'Lancer Nginx accessible uniquement depuis l’hôte sur le port 8080, redémarré automatiquement, limité à 256 Mo, avec un dossier de configuration en lecture seule.', r: 'docker run -d --name web --restart unless-stopped --memory 256m -p 127.0.0.1:8080:80 -v /srv/web/conf:/etc/nginx/conf.d:ro nginx:1.27-alpine' },
|
||||
{ n: 'Approfondissement', c: 'Montrer avec des commandes qu’un conteneur n’est qu’un processus de l’hôte, et retrouver son PID et ses cgroups.', r: 'docker inspect -f "{{.State.Pid}}" web → 4312\nps -fp 4312 · cat /proc/4312/cgroup · ls -l /proc/4312/ns' }
|
||||
],
|
||||
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Une VM Debian avec Docker : construire, publier, exploiter.', etapes: ['Construire l’image d’une API Python multi-étapes', 'La lancer avec limites de ressources et redémarrage automatique', 'Ajouter PostgreSQL sur un réseau utilisateur avec volume', 'Vérifier les ports réellement exposés', 'Scanner l’image avec Trivy'] },
|
||||
plusLoin: [['Docker Docs · Dockerfile best practices', 'https://docs.docker.com/build/building/best-practices/'], ['ANSSI · Recommandations de sécurité relatives au déploiement de conteneurs Docker', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-au-deploiement-de-conteneurs-docker'], ['Trivy', 'https://trivy.dev/']] },
|
||||
plus: {
|
||||
cas: { titre: '', texte: '' },
|
||||
resolu: { enonce: 'L’application de réservation de salles, conteneurisée, redémarre en boucle depuis la mise à jour de ce matin. Les utilisateurs obtiennent une erreur 502 du reverse proxy. Diagnostiquer et rétablir.', etapes: [
|
||||
['Constater l’état', 'docker ps -a --filter name=resa\nNAMES IMAGE STATUS\nresa resa-app:2.4.0 Restarting (1) 12 seconds ago\n→ Le conteneur démarre puis s’arrête avec le code 1, et la politique de redémarrage le relance sans fin. Le reverse proxy ne trouve donc jamais de backend : d’où les 502.'],
|
||||
['Lire les journaux', 'docker logs --tail 20 resa\nKeyError: \'DATABASE_URL\'\n→ L’application s’arrête au démarrage faute de la variable DATABASE_URL. La nouvelle version attend un nom de variable différent de l’ancienne (DB_URL).'],
|
||||
['Comparer avec la version précédente', 'docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" resa | grep -i db\nDB_URL=postgresql://resa@db/resa\n→ Le conteneur est lancé avec l’ancien nom de variable. Le changement était annoncé dans les notes de version 2.4.0, mais le fichier de déploiement n’a pas été adapté.'],
|
||||
['Rétablir le service', 'docker rm -f resa\ndocker run -d --name resa --network resa-net --restart unless-stopped -e DATABASE_URL=postgresql://resa@db/resa resa-app:2.4.0\ndocker ps --filter name=resa\nresa resa-app:2.4.0 Up 20 seconds (healthy)\n→ On ne corrige pas le conteneur existant : on le recrée avec la bonne configuration. Le HEALTHCHECK confirme que l’application répond.'],
|
||||
['Vérifier et prévenir', 'curl -sI https://resa.lycee.local | head -1\nHTTP/2 200\n→ Service rétabli. Actions préventives : décrire le déploiement dans un fichier Compose versionné plutôt qu’une commande docker run tapée à la main, relire les notes de version, et tester la nouvelle image sur un environnement de préproduction avant la production.']
|
||||
] },
|
||||
erreurs: ['Utiliser latest en production', 'Lancer le processus en root dans l’image', 'Stocker des données dans la couche du conteneur', 'Modifier un conteneur à la main au lieu de reconstruire l’image', 'Publier une base de données sur 0.0.0.0', 'Mettre des secrets dans le Dockerfile ou l’image', 'Oublier .dockerignore et embarquer .git', 'Donner le groupe docker à des utilisateurs non administrateurs'],
|
||||
memo: [['Commande', 'Usage en production'], ['docker ps -a', 'conteneurs et leur état, y compris arrêtés'], ['docker logs -f --tail 100 <c>', 'journaux de l’application'], ['docker inspect <c>', 'configuration complète : env, montages, réseau'], ['docker exec -it <c> sh', 'shell dans un conteneur en cours'], ['docker stats', 'CPU et mémoire consommés en direct'], ['docker build -t app:1.2.0 .', 'construire une image versionnée'], ['docker image ls · docker image prune', 'lister et nettoyer les images'], ['docker volume ls', 'volumes de données'], ['docker network ls · inspect', 'réseaux et conteneurs rattachés'], ['docker system df', 'espace disque utilisé par Docker'], ['docker run --rm -it image sh', 'explorer une image sans la garder']],
|
||||
examen: { titre: 'Sujet type · 20 points', q: ['Comparer VM et conteneur en citant les mécanismes du noyau. (4 pts)', 'Critiquer et corriger un Dockerfile fourni. (6 pts)', 'Écrire la commande de lancement d’une application avec volume, réseau, limites et redémarrage. (4 pts)', 'Diagnostiquer un conteneur en redémarrage permanent à partir de sorties fournies. (4 pts)', 'Citer quatre mesures de sécurité des conteneurs. (2 pts)'], r: ['namespaces, cgroups, noyau partagé', 'version, cache, USER, multi-étapes, .dockerignore', 'docker run -d --name … --restart … --memory … -v … --network …', 'logs, inspect, code de sortie', 'non root, scan, pas de --privileged, secrets hors image'] },
|
||||
prof: { deroule: [['Ouverture', 'Démonstration : la même application qui échoue sur deux machines, puis qui marche partout en conteneur'], ['Cours', 'Namespaces, cgroups, images et couches'], ['Démonstration', 'Dockerfile naïf puis Dockerfile de production, comparaison de taille'], ['Pratique', 'Lab : API + base de données, volumes et réseau'], ['Incident', 'Conteneur en boucle de redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : Dockerfile à compléter, fiche mémo. Élèves avancés : mode rootless, scan Trivy dans une CI, image distroless.', eval: 'Étapes du lab validées, analyse du Dockerfile, sujet type.' }
|
||||
},
|
||||
exemples: [
|
||||
['Pourquoi ce conteneur s’arrête-t-il ?', 'Un conteneur passe en Exited juste après son lancement.', 'docker ps -a --filter name=api --format "{{.Names}} {{.Status}}"\ndocker logs api | tail -3', 'api Exited (137) 2 minutes ago\nKilled', 'Le code 137 = 128 + 9 : le processus a reçu SIGKILL. Sans docker stop, c’est presque toujours le noyau qui l’a tué pour dépassement de mémoire (OOM). On le confirme avec docker inspect -f "{{.State.OOMKilled}}" api, puis on augmente --memory ou on corrige la fuite.'],
|
||||
['Mesurer la consommation', 'Le serveur rame depuis le déploiement.', 'docker stats --no-stream --format "table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}"', 'NAME CPU % MEM USAGE / LIMIT\nresa 0.4% 182MiB / 512MiB\nglpi 187.3% 1.1GiB / 15.6GiB\ndb 3.1% 420MiB / 1GiB', 'Le conteneur glpi consomme presque deux cœurs et n’a pas de limite mémoire (la limite affichée est la RAM de l’hôte). On regarde ses journaux et on lui fixe --cpus et --memory pour protéger les autres services.'],
|
||||
['Vérifier les ports réellement exposés', 'Audit avant mise en production.', 'docker ps --format "{{.Names}} {{.Ports}}"', 'web 0.0.0.0:443->443/tcp\ndb 0.0.0.0:5432->5432/tcp\nredis 127.0.0.1:6379->6379/tcp', 'La base PostgreSQL est publiée sur toutes les interfaces : joignable depuis Internet malgré le pare-feu de l’hôte. On la retire du -p et on la laisse sur le réseau interne ; Redis, publié sur 127.0.0.1, est correct.'],
|
||||
['Comprendre le cache de construction', 'Chaque build prend trois minutes.', 'docker build -t app:dev .', ' => CACHED [2/5] WORKDIR /app\n => [3/5] COPY . .\n => [4/5] RUN pip install -r requirements.txt 172.4s', 'COPY . . est placé avant l’installation des dépendances : toute modification du code invalide le cache et relance pip. En copiant requirements.txt seul, puis en installant, puis en copiant le code, le build passe à quelques secondes.'],
|
||||
['Retrouver un conteneur par son nom', 'L’application ne joint pas la base de données.', 'docker exec api getent hosts db ; docker network inspect -f "{{range .Containers}}{{.Name}} {{end}}" appnet', '(aucune sortie)\napi', 'Le nom db ne se résout pas, car le conteneur de base de données n’est pas sur le réseau appnet. On le rattache : docker network connect appnet db.'],
|
||||
['Libérer de l’espace disque', 'Le disque de l’hôte est plein à 95 %.', 'docker system df', 'TYPE TOTAL ACTIVE SIZE RECLAIMABLE\nImages 48 6 21.3GB 18.9GB (88%)\nContainers 9 6 1.2GB 0.4GB\nLocal Volumes 12 5 8.7GB 3.1GB\nBuild Cache — — 6.4GB 6.4GB', 'Les anciennes images et le cache de construction occupent 25 Go récupérables. docker image prune -a et docker builder prune les suppriment. On évite docker volume prune sans vérifier : il supprimerait des données.']
|
||||
],
|
||||
termes: ['Docker', 'conteneur', 'image', 'Dockerfile', 'namespaces', 'cgroups', 'couche', 'tag', 'digest', 'registre', 'volume', 'bind mount', 'bridge', 'docker run', 'docker ps', 'docker logs', 'docker exec', 'docker inspect', 'docker stats', 'PID 1', 'SIGTERM', 'SIGKILL', 'multi-étapes', 'USER', 'HEALTHCHECK', 'latest', 'Trivy', 'rootless', 'OOM']
|
||||
});
|
||||
|
||||
reg({
|
||||
cours: { id: 'compose', match: /Docker Compose/i,
|
||||
titre: 'Docker Compose : décrire et exploiter une application multi-conteneurs', duree: '',
|
||||
niveau: 'BTS SIO SISR / SLAM · BUT R&T DevCloud · AIS',
|
||||
prerequis: ['Conteneurs Docker'],
|
||||
competences: ['BTS SIO · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser le déploiement'],
|
||||
intro: 'Une application réelle ne tient jamais dans un seul conteneur : un site Laravel a besoin d’un serveur web, de PHP-FPM, d’une base de données, souvent d’un cache Redis et d’un planificateur de tâches. Lancer et relier tout cela à coups de docker run est fragile et impossible à reproduire. Docker Compose décrit l’ensemble dans un fichier YAML versionné : services, réseaux, volumes, variables, ordre de démarrage. Ce fichier devient la documentation exécutable du déploiement.',
|
||||
cm: [
|
||||
{ t: 'Le fichier compose.yaml', d: '', html: 'Le fichier <code>compose.yaml</code> (anciennement docker-compose.yml) déclare des <b>services</b>, chacun correspondant à un conteneur issu d’une image ou d’un Dockerfile, ainsi que des <b>volumes</b> et des <b>réseaux</b>. Compose crée automatiquement un réseau par projet : chaque service y est joignable par son nom.<br><br>La commande actuelle est <code>docker compose</code> (plugin intégré, version 2), qui remplace l’ancien binaire docker-compose écrit en Python.', exemple: 'services:\n web:\n image: nginx:1.27-alpine\n ports: ["443:443"]\n volumes: ["./nginx:/etc/nginx/conf.d:ro", "certs:/etc/ssl/app:ro"]\n depends_on: [app]\n app:\n build: .\n env_file: .env\n depends_on:\n db: { condition: service_healthy }\n db:\n image: postgres:16\n environment:\n POSTGRES_PASSWORD_FILE: /run/secrets/db_password\n secrets: [db_password]\n volumes: ["pgdata:/var/lib/postgresql/data"]\n healthcheck:\n test: ["CMD", "pg_isready", "-U", "postgres"]\n interval: 5s\n retries: 10\nvolumes:\n pgdata:\n certs:\nsecrets:\n db_password:\n file: ./secrets/db_password.txt' },
|
||||
{ t: 'Ordre de démarrage et healthchecks', d: '', html: '<code>depends_on</code> seul garantit uniquement que le conteneur de la base est <b>démarré</b> avant l’application, pas qu’il est <b>prêt</b> à accepter des connexions. PostgreSQL met plusieurs secondes à s’initialiser au premier lancement, et l’application qui s’y connecte trop tôt échoue.<br><br>La bonne pratique combine un <b>healthcheck</b> sur la base et <code>condition: service_healthy</code> dans le depends_on de l’application. L’application doit aussi savoir réessayer sa connexion : en production, un redémarrage de la base peut survenir à tout moment.' },
|
||||
{ t: 'Variables, fichiers .env et secrets', d: '', html: 'Compose lit automatiquement le fichier <code>.env</code> du dossier du projet pour substituer les variables <code>${VAR}</code> du fichier YAML, et <code>env_file</code> injecte des variables dans un conteneur. Ces fichiers ne doivent <b>jamais être versionnés</b> s’ils contiennent des mots de passe : on versionne un <code>.env.example</code> sans valeurs.<br><br>Les <b>secrets</b> de Compose montent un fichier en lecture seule dans <code>/run/secrets/</code> : le mot de passe n’apparaît ni dans les variables d’environnement, ni dans <code>docker inspect</code>. Les images officielles comme postgres ou mariadb acceptent les variables en <code>_FILE</code> pour lire leur valeur dans ce fichier.' },
|
||||
{ t: 'Exploiter un projet Compose', d: '', html: '<code>docker compose up -d</code> crée ou met à jour tout le projet ; il ne recrée que les services dont la configuration ou l’image a changé. <code>docker compose pull</code> récupère les nouvelles images, <code>docker compose up -d --build</code> reconstruit celles qui ont un Dockerfile. <code>docker compose ps</code>, <code>logs -f service</code>, <code>exec service sh</code> et <code>down</code> complètent l’exploitation courante.<br><br><code>docker compose down</code> supprime conteneurs et réseau mais <b>conserve les volumes</b> ; <code>down -v</code> supprime aussi les volumes, donc les données. Cette option ne doit jamais être tapée par réflexe en production.', attention: 'docker compose down -v sur le serveur de production efface la base de données. Sauvegarder avant toute opération de maintenance, et réserver -v aux environnements de test.' },
|
||||
{ t: 'Plusieurs environnements', d: '', html: 'Un même projet tourne en développement, en préproduction et en production avec des différences : ports, volumes de code montés, niveau de journalisation, nombre de répliques. Compose fusionne plusieurs fichiers : <code>compose.yaml</code> contient la base commune, <code>compose.override.yaml</code> (chargé automatiquement) les réglages de développement, et <code>compose.prod.yaml</code> ceux de production, appliqués avec <code>docker compose -f compose.yaml -f compose.prod.yaml up -d</code>. Les <b>profils</b> permettent d’activer des services optionnels (outils d’administration, tests).' },
|
||||
{ t: 'Limites de Compose', d: '', html: 'Compose pilote les conteneurs d’<b>une seule machine</b>. Il ne répartit pas la charge entre plusieurs serveurs, ne déplace pas un service quand un nœud tombe, et ne gère pas les mises à jour sans interruption. Pour un établissement ou une PME, c’est souvent largement suffisant et bien plus simple à maintenir. Au-delà, on passe à un orchestrateur comme <b>Kubernetes</b>, dont les notions (services, volumes, secrets, sondes de santé) prolongent directement celles de Compose.' }
|
||||
],
|
||||
synthese: ['compose.yaml versionné = documentation exécutable du déploiement', 'Réseau par projet, services joignables par leur nom', 'depends_on + healthcheck + condition service_healthy', '.env jamais versionné ; secrets montés dans /run/secrets', 'up -d ne recrée que ce qui a changé ; down -v efface les données', 'Plusieurs fichiers pour plusieurs environnements', 'Une seule machine : au-delà, Kubernetes'],
|
||||
qcm: [
|
||||
['Que garantit depends_on sans condition ?', ['Que la base accepte les connexions', 'Que le conteneur de la base est démarré avant', 'Que la base est sauvegardée', 'Rien'], 1, 'Démarré ne veut pas dire prêt.'],
|
||||
['Que fait docker compose down -v ?', ['Redémarre', 'Supprime conteneurs, réseau et volumes', 'Met à jour les images', 'Affiche la version'], 1, 'Les données sont effacées.'],
|
||||
['Comment le service app joint-il le service db ?', ['Par l’IP de l’hôte', 'Par le nom db', 'Par localhost', 'Par un port publié'], 1, 'DNS du réseau du projet.'],
|
||||
['Pourquoi préférer les secrets aux variables d’environnement pour un mot de passe ?', ['Plus rapide', 'Le mot de passe n’apparaît pas dans docker inspect', 'Obligatoire', 'Chiffrement automatique sur le disque'], 1, 'Fichier monté en lecture seule.'],
|
||||
['Quel fichier est chargé automatiquement en plus de compose.yaml ?', ['compose.prod.yaml', 'compose.override.yaml', 'docker.yaml', '.env.example'], 1, 'Réglages de développement.'],
|
||||
['Après modification de l’image d’un seul service, docker compose up -d :', ['recrée tous les services', 'recrée seulement ce service', 'ne fait rien', 'supprime les volumes'], 1, 'Mise à jour ciblée.']
|
||||
],
|
||||
td: [
|
||||
{ n: 'Application', c: 'Écrire le compose.yaml d’un GLPI : service web (image diouxx/glpi ou équivalent), service MariaDB avec volume et healthcheck, mot de passe en secret, seul le web publié sur 443.', r: 'services: web (ports 443, depends_on db service_healthy) ; db (mariadb:11, MARIADB_ROOT_PASSWORD_FILE, secrets, volume dbdata, healthcheck healthcheck.sh --connect) ; volumes: dbdata ; secrets: db_root.' },
|
||||
{ n: 'Application', c: 'Écrire la procédure de mise à jour d’un projet Compose en production sans perte de données.', r: '1. Sauvegarde (docker compose exec db pg_dump …) · 2. docker compose pull · 3. docker compose up -d · 4. docker compose ps et logs · 5. test fonctionnel · 6. en cas d’échec : revenir au tag précédent dans compose.yaml puis up -d.' },
|
||||
{ n: 'Approfondissement', c: 'Séparer la configuration de développement (code monté, port 8000 publié, débogage) de celle de production (image construite, pas de port applicatif publié).', r: 'compose.yaml : base commune · compose.override.yaml : volumes ./:/app, ports 8000, APP_DEBUG=1 · compose.prod.yaml : image registre:tag, restart unless-stopped, pas de ports sur app · lancement prod : -f compose.yaml -f compose.prod.yaml.' }
|
||||
],
|
||||
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Déployer une application web, sa base et son cache avec Compose.', etapes: ['Écrire compose.yaml avec trois services', 'Ajouter healthcheck et secrets', 'Démarrer, vérifier ps et logs', 'Mettre à jour une image sans perte de données', 'Sauvegarder et restaurer la base'] },
|
||||
plusLoin: [['Docker Docs · Compose file reference', 'https://docs.docker.com/reference/compose-file/'], ['Docker Docs · Secrets in Compose', 'https://docs.docker.com/compose/how-tos/use-secrets/']] },
|
||||
plus: {
|
||||
cas: { titre: '', texte: '' },
|
||||
resolu: { enonce: 'Après un redémarrage du serveur, l’application Laravel du CDI affiche « SQLSTATE[HY000] [2002] Connection refused » pendant plusieurs minutes, puis parfois reste en erreur. Diagnostiquer et fiabiliser.', etapes: [
|
||||
['Observer l’ordre de démarrage', 'docker compose ps\nNAME SERVICE STATUS\ncdi-app app Up 3 minutes\ncdi-db db Up 3 minutes (health: starting)\n→ L’application tourne alors que la base n’est pas encore prête.'],
|
||||
['Lire les journaux', 'docker compose logs app | head -3\nSQLSTATE[HY000] [2002] Connection refused\ndocker compose logs db | tail -2\nInnoDB: Starting crash recovery...\nready for connections.\n→ Après un arrêt brutal, MariaDB effectue une récupération qui prend du temps. L’application a tenté de se connecter trop tôt et a mis la base en échec dans son cache de configuration.'],
|
||||
['Lire le fichier Compose', 'grep -A3 depends_on compose.yaml\n depends_on:\n - db\n→ depends_on sans condition : Compose attend seulement que le conteneur db soit lancé.'],
|
||||
['Corriger', 'db:\n healthcheck:\n test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]\n interval: 5s\n retries: 30\napp:\n depends_on:\n db: { condition: service_healthy }\n restart: unless-stopped\ndocker compose up -d\n→ L’application ne démarre plus qu’une fois la base réellement prête, et redémarre automatiquement en cas d’échec.'],
|
||||
['Valider', 'sudo reboot (sur la préproduction)\ndocker compose ps\ncdi-db Up 2 minutes (healthy)\ncdi-app Up 1 minute\n→ L’ordre est respecté. Compte rendu : cause (depends_on sans healthcheck), correction, test de redémarrage ajouté à la procédure de validation.']
|
||||
] },
|
||||
erreurs: ['depends_on sans healthcheck', 'Versionner le fichier .env avec les mots de passe', 'Taper docker compose down -v en production', 'Publier les ports de la base de données', 'Ne pas fixer les versions des images', 'Mettre à jour sans sauvegarde préalable'],
|
||||
memo: [['Commande', 'Usage'], ['docker compose up -d', 'créer ou mettre à jour le projet'], ['docker compose ps', 'état et santé des services'], ['docker compose logs -f <s>', 'journaux d’un service'], ['docker compose exec <s> sh', 'shell dans un service'], ['docker compose pull', 'récupérer les nouvelles images'], ['docker compose up -d --build', 'reconstruire et redéployer'], ['docker compose config', 'afficher la configuration fusionnée et validée'], ['docker compose down', 'arrêter en conservant les volumes'], ['docker compose -f a.yaml -f b.yaml up -d', 'combiner des fichiers d’environnement'], ['docker compose --profile outils up -d', 'activer des services optionnels']],
|
||||
examen: { titre: 'Sujet type · 20 points', q: ['Écrire le compose.yaml d’une application trois tiers. (8 pts)', 'Expliquer depends_on et les healthchecks. (4 pts)', 'Rédiger la procédure de mise à jour sans perte de données. (5 pts)', 'Justifier le passage à Kubernetes pour un cas donné. (3 pts)'], r: ['services, volumes, secrets, réseau', 'démarré ≠ prêt, condition service_healthy', 'sauvegarde, pull, up -d, contrôle, retour arrière', 'plusieurs nœuds, haute disponibilité'] },
|
||||
prof: { deroule: [['Ouverture', 'Lancer à la main trois conteneurs reliés, puis la même chose en un fichier'], ['Cours', 'Services, réseaux, volumes, secrets'], ['Pratique', 'Lab application trois tiers'], ['Incident', 'Ordre de démarrage au redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : compose.yaml à compléter. Élèves avancés : plusieurs environnements, profils, déploiement par CI.', eval: 'Lab validé, sujet type.' }
|
||||
},
|
||||
exemples: [
|
||||
['Valider le fichier avant déploiement', 'Une modification a été faite dans compose.yaml.', 'docker compose config --quiet && echo OK', 'services.app.depends_on.db.condition must be one of the following: "service_started", "service_healthy", "service_completed_successfully"', 'Faute de frappe dans la condition. docker compose config valide et fusionne les fichiers sans rien lancer : à exécuter avant chaque up.'],
|
||||
['Voir la santé des services', 'L’application répond lentement au démarrage.', 'docker compose ps --format "table {{.Service}}\\t{{.Status}}"', 'SERVICE STATUS\ndb Up 40 seconds (healthy)\nredis Up 40 seconds\napp Up 12 seconds (health: starting)', 'La base est prête, l’application exécute encore son healthcheck. Tant qu’elle n’est pas healthy, le reverse proxy peut renvoyer des erreurs : c’est normal pendant quelques secondes.'],
|
||||
['Mettre à jour une seule image', 'Correctif de sécurité publié pour Nginx.', 'docker compose pull web && docker compose up -d web', '[+] Pulling web … Pulled\n[+] Running 1/1 ✔ Container cdi-web Recreated', 'Seul le service web est recréé ; la base et l’application continuent de tourner.'],
|
||||
['Sauvegarder la base d’un projet', 'Avant une mise à jour.', 'docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz ; ls -lh backup-*', '-rw-r--r-- 1 admin admin 14M sept. 30 07:02 backup-2026-09-30.sql.gz', '-T désactive le pseudo-terminal pour que la sortie puisse être redirigée proprement dans un fichier.'],
|
||||
['Retrouver le nom réel des ressources', 'Un collègue cherche le volume de la base.', 'docker volume ls --filter label=com.docker.compose.project=cdi', 'DRIVER VOLUME NAME\nlocal cdi_pgdata', 'Compose préfixe les volumes et réseaux par le nom du projet (le dossier). Deux projets peuvent donc avoir chacun un volume pgdata sans conflit.'],
|
||||
['Afficher les journaux de plusieurs services', 'Erreur 500 intermittente.', 'docker compose logs --since 10m app web | grep -E " 500 |ERROR"', 'cdi-web | "POST /reservations HTTP/1.1" 500\ncdi-app | ERROR: duplicate key value violates unique constraint "resa_salle_creneau"', 'En croisant les journaux, on relie l’erreur 500 vue par Nginx à une violation de contrainte dans l’application : deux réservations simultanées du même créneau. C’est un bug applicatif, pas une panne d’infrastructure.']
|
||||
],
|
||||
termes: ['Docker Compose', 'compose.yaml', 'service', 'depends_on', 'healthcheck', 'service_healthy', 'secrets', '.env', 'env_file', 'docker compose up', 'docker compose down', 'docker compose ps', 'docker compose logs', 'docker compose config', 'profils', 'volume', 'Kubernetes']
|
||||
});
|
||||
|
||||
reg({
|
||||
cours: { id: 'ansible', match: /Ansible|IaC|infrastructure as code/i,
|
||||
titre: 'Ansible : automatiser la configuration des serveurs', duree: '',
|
||||
niveau: 'BTS SIO SISR · BUT R&T DevCloud · AIS · Master',
|
||||
prerequis: ['Le terminal Linux', 'SSH par clé', 'Paquets et services'],
|
||||
competences: ['BTS SIO SISR · Bloc 2 · Automatiser les tâches d’administration', 'BUT R&T DevCloud · Automatiser le déploiement d’une infrastructure', 'AIS · Industrialiser l’administration'],
|
||||
intro: 'Configurer un serveur à la main prend une heure ; en configurer quarante de façon identique est impossible sans erreur, et personne ne sait plus, six mois après, ce qui a été fait sur lequel. Ansible décrit l’état attendu des machines dans des fichiers YAML versionnés, et l’applique par SSH, sans agent à installer. Relancé autant de fois qu’on veut, il ne change que ce qui n’est pas conforme. C’est l’outil d’automatisation le plus répandu dans les services informatiques, des établissements scolaires aux grands hébergeurs.',
|
||||
cm: [
|
||||
{ t: 'Principe : décrire un état, pas des commandes', d: '', html: 'Un script Bash enchaîne des actions : « installe Nginx, copie ce fichier, redémarre ». Lancé deux fois, il peut échouer ou dupliquer une ligne. Ansible décrit un <b>état désiré</b> : « Nginx doit être installé, ce fichier doit avoir ce contenu, le service doit être actif ». Chaque <b>module</b> vérifie l’état actuel et n’agit que si nécessaire : c’est l’<b>idempotence</b>. Un playbook relancé sur une machine déjà conforme affiche <code>changed=0</code>.<br><br>Ansible est <b>sans agent</b> : le poste de contrôle se connecte en SSH, envoie un petit programme Python, l’exécute et le supprime. Il suffit d’un accès SSH et de Python sur les machines gérées.' },
|
||||
{ t: 'L’inventaire', d: '', html: 'L’<b>inventaire</b> liste les machines gérées et les range en <b>groupes</b>. Il peut être statique (fichier INI ou YAML) ou dynamique (généré depuis un hyperviseur ou un fournisseur cloud). Les <b>variables</b> se placent dans <code>group_vars/</code> et <code>host_vars/</code>, ce qui sépare les données (quelle machine, quelle valeur) de la logique (les tâches).', exemple: '# inventaire/hosts.yaml\nall:\n children:\n web:\n hosts:\n web1.lycee.local:\n web2.lycee.local:\n db:\n hosts:\n db1.lycee.local:\n ansible_host: 10.62.40.31\n vars:\n ansible_user: admin\n\n$ ansible -i inventaire all -m ping\nweb1.lycee.local | SUCCESS => {"changed": false, "ping": "pong"}' },
|
||||
{ t: 'Playbooks, tâches et modules', d: '', html: 'Un <b>playbook</b> est une liste de <b>plays</b> : chacun associe un groupe de machines à une liste de <b>tâches</b>. Chaque tâche appelle un <b>module</b> avec des paramètres : <code>ansible.builtin.apt</code> pour les paquets, <code>template</code> pour générer un fichier de configuration, <code>service</code> ou <code>systemd</code> pour les services, <code>user</code>, <code>lineinfile</code>, <code>copy</code>, <code>file</code>. On utilise les noms complets (FQCN) pour éviter toute ambiguïté.<br><br><code>become: true</code> élève les privilèges avec sudo. Les <b>handlers</b> sont des tâches déclenchées par <code>notify</code> seulement si une tâche a réellement changé quelque chose : on ne redémarre Nginx que si sa configuration a été modifiée.', exemple: '- name: Serveurs web\n hosts: web\n become: true\n tasks:\n - name: Installer nginx\n ansible.builtin.apt:\n name: nginx\n state: present\n update_cache: true\n - name: Déployer la configuration du site\n ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n mode: "0644"\n notify: Recharger nginx\n - name: Nginx actif au démarrage\n ansible.builtin.service:\n name: nginx\n state: started\n enabled: true\n handlers:\n - name: Recharger nginx\n ansible.builtin.service:\n name: nginx\n state: reloaded' },
|
||||
{ t: 'Templates Jinja2 et variables', d: '', html: 'Le module <code>template</code> génère un fichier à partir d’un modèle <b>Jinja2</b> : <code>{{ variable }}</code> insère une valeur, <code>{% for %}</code> et <code>{% if %}</code> ajoutent de la logique. La même configuration sert ainsi à toutes les machines, avec leurs valeurs propres. Les <b>facts</b> collectés automatiquement (adresse IP, distribution, mémoire) sont aussi disponibles comme variables.<br><br>Les variables ont un ordre de <b>priorité</b> : valeurs par défaut d’un rôle, group_vars, host_vars, variables du playbook, puis <code>-e</code> en ligne de commande, qui l’emporte sur tout. Mal maîtrisé, cet ordre explique des valeurs « inattendues ».' },
|
||||
{ t: 'Rôles et collections', d: '', html: 'Au-delà de quelques dizaines de tâches, on range le code en <b>rôles</b> : une arborescence normalisée (tasks, handlers, templates, defaults, vars, files) qui regroupe tout ce qu’il faut pour une fonction, par exemple « serveur web » ou « durcissement SSH ». Un rôle se réutilise dans plusieurs playbooks et se partage. Les <b>collections</b> distribuent rôles et modules (community.general, ansible.posix) et s’installent avec <code>ansible-galaxy</code>.' },
|
||||
{ t: 'Tester avant d’appliquer et protéger les secrets', d: '', html: '<code>--check</code> simule l’exécution sans rien modifier, <code>--diff</code> montre les différences de fichiers, <code>--limit web1</code> restreint à une machine, <code>--tags</code> à certaines tâches. En production, on valide toujours sur une machine avant tout le parc.<br><br>Les mots de passe et clés ne doivent pas être en clair dans le dépôt Git. <b>Ansible Vault</b> chiffre un fichier de variables avec AES-256 ; le mot de passe du coffre est fourni à l’exécution. <code>ansible-lint</code> détecte les mauvaises pratiques (tâches non nommées, modules shell là où un module dédié existe, droits de fichiers absents).', attention: 'Les modules command et shell ne sont pas idempotents par nature : ils s’exécutent à chaque passage. Préférer un module dédié, ou ajouter creates, removes ou changed_when pour décrire l’état.' }
|
||||
],
|
||||
synthese: ['Ansible décrit un état désiré ; les modules sont idempotents', 'Sans agent : SSH et Python sur les machines gérées', 'Inventaire en groupes, variables dans group_vars et host_vars', 'Playbook → plays → tâches → modules ; become pour sudo', 'Handlers déclenchés uniquement en cas de changement', 'Templates Jinja2, rôles réutilisables', '--check --diff et --limit avant d’appliquer ; Vault pour les secrets'],
|
||||
qcm: [
|
||||
['Que signifie qu’une tâche Ansible est idempotente ?', ['Elle est rapide', 'La relancer sur une machine conforme ne change rien', 'Elle s’exécute en parallèle', 'Elle est chiffrée'], 1, 'changed=0 au second passage.'],
|
||||
['De quoi Ansible a-t-il besoin sur les machines gérées ?', ['Un agent Ansible', 'SSH et Python', 'Docker', 'Un serveur web'], 1, 'Architecture sans agent.'],
|
||||
['Quand un handler s’exécute-t-il ?', ['À chaque tâche', 'Quand une tâche qui le notifie a produit un changement', 'Au début du playbook', 'Jamais automatiquement'], 1, 'Par exemple recharger Nginx seulement si sa configuration a changé.'],
|
||||
['Quelle option simule sans modifier ?', ['--dry', '--check', '--test', '--simulate'], 1, 'Souvent combinée avec --diff.'],
|
||||
['Où placer une variable commune à tous les serveurs web ?', ['host_vars/web1.yaml', 'group_vars/web.yaml', 'dans le template', 'dans /etc/ansible/ansible.cfg'], 1, 'Variables de groupe.'],
|
||||
['Comment protéger un mot de passe dans le dépôt ?', ['Le mettre en commentaire', 'Ansible Vault', 'Le coder en base64', 'Le mettre dans le template'], 1, 'Chiffrement AES-256.'],
|
||||
['Pourquoi éviter le module shell pour installer un paquet ?', ['Il est interdit', 'Il n’est pas idempotent et masque l’état réel', 'Il est plus lent', 'Il ne supporte pas sudo'], 1, 'Utiliser apt ou dnf.']
|
||||
],
|
||||
td: [
|
||||
{ n: 'Application', c: 'Écrire un playbook qui crée l’utilisateur technicien, lui dépose une clé SSH publique, désactive l’authentification SSH par mot de passe et recharge sshd seulement si nécessaire.', r: 'user (name, groups: sudo, shell) · ansible.posix.authorized_key · lineinfile sur /etc/ssh/sshd_config avec regexp "^#?PasswordAuthentication" line "PasswordAuthentication no" validate "sshd -t -f %s" notify Recharger ssh · handler service ssh reloaded.' },
|
||||
{ n: 'Application', c: 'Transformer ce passage en tâche idempotente : « shell: echo "10.62.40.20 ent" >> /etc/hosts ».', r: 'ansible.builtin.lineinfile: path=/etc/hosts line="10.62.40.20 ent" (la ligne n’est ajoutée qu’une fois).' },
|
||||
{ n: 'Approfondissement', c: 'Organiser en rôle le déploiement d’un serveur web : arborescence, variables par défaut, template et handler.', r: 'roles/web/{tasks/main.yml, handlers/main.yml, templates/site.conf.j2, defaults/main.yml (web_port: 443, web_root: /var/www/site)} · playbook : roles: [web].' }
|
||||
],
|
||||
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un poste de contrôle et trois VM Debian à configurer.', etapes: ['Écrire l’inventaire et tester ansible -m ping', 'Playbook : paquets et utilisateurs', 'Déployer Nginx avec un template et un handler', 'Vérifier l’idempotence (changed=0)', 'Chiffrer un secret avec Vault', 'Passer ansible-lint'] },
|
||||
plusLoin: [['Documentation Ansible', 'https://docs.ansible.com/'], ['Ansible Lint', 'https://ansible.readthedocs.io/projects/lint/'], ['Xavki · tutoriels Ansible en français', 'https://www.youtube.com/@xavki']] },
|
||||
plus: {
|
||||
cas: { titre: '', texte: '' },
|
||||
resolu: { enonce: 'Le playbook de mise à jour des 12 serveurs web s’est interrompu cette nuit : 4 serveurs ont la nouvelle configuration Nginx, 8 l’ancienne, et web3 ne répond plus. Diagnostiquer, rétablir et fiabiliser.', etapes: [
|
||||
['Lire le récapitulatif', 'PLAY RECAP\nweb1 ok=6 changed=2 failed=0\nweb3 ok=4 changed=1 failed=1\nweb4…web12 (non traités)\n→ Ansible s’est arrêté après l’échec sur web3 : par défaut, un play traite les machines par lots et s’arrête si toutes celles du lot échouent. L’état du parc est hétérogène.'],
|
||||
['Identifier l’erreur', 'TASK [Recharger nginx] fatal: [web3]: FAILED! => {"msg": "Unable to reload service nginx: Job for nginx.service failed"}\nssh web3 sudo nginx -t\nnginx: [emerg] unknown directive "ssl_protocol" in /etc/nginx/conf.d/site.conf:7\n→ Le template contient une faute (ssl_protocol au lieu de ssl_protocols). La configuration invalide a été déposée, le rechargement a échoué et Nginx s’est arrêté sur web3.'],
|
||||
['Rétablir web3', 'Correction du template site.conf.j2 (ssl_protocols TLSv1.2 TLSv1.3;)\nansible-playbook site.yml --limit web3 --diff\nweb3 ok=6 changed=2 failed=0\ncurl -sI https://web3.lycee.local | head -1\nHTTP/2 200\n→ On corrige la source (le template), jamais le fichier à la main sur le serveur : sinon le prochain passage réintroduirait l’erreur.'],
|
||||
['Fiabiliser le playbook', '- ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n validate: nginx -t -c /etc/nginx/nginx.conf\n notify: Recharger nginx\n- hosts: web\n serial: 2\n max_fail_percentage: 0\n→ validate teste le fichier avant de le mettre en place ; serial: 2 déploie deux serveurs à la fois et arrête tout au premier échec. Une erreur ne touche plus qu’un lot.'],
|
||||
['Appliquer au parc et contrôler', 'ansible-playbook site.yml --check --diff --limit web4\nansible-playbook site.yml\nweb1…web12 failed=0\nansible-playbook site.yml (second passage)\nweb1…web12 changed=0\n→ Le parc est homogène et le second passage confirme l’idempotence. Compte rendu : cause, impact, correction, et règle d’équipe : --check sur une machine témoin avant tout déploiement, validation des templates dans la CI avec ansible-lint.']
|
||||
] },
|
||||
erreurs: ['Utiliser shell ou command là où un module dédié existe', 'Modifier un fichier à la main sur un serveur géré par Ansible', 'Laisser des mots de passe en clair dans le dépôt', 'Déployer sur tout le parc sans --check ni serial', 'Redémarrer un service à chaque passage au lieu d’un handler', 'Déposer une configuration sans validate'],
|
||||
memo: [['Commande', 'Usage'], ['ansible all -m ping', 'tester la connexion à tout l’inventaire'], ['ansible web -m ansible.builtin.setup', 'afficher les facts d’un groupe'], ['ansible-playbook site.yml --check --diff', 'simuler et voir les différences'], ['ansible-playbook site.yml --limit web1', 'restreindre à une machine'], ['ansible-playbook site.yml --tags nginx', 'n’exécuter que certaines tâches'], ['ansible-inventory --graph', 'visualiser l’inventaire'], ['ansible-vault create|edit|encrypt', 'gérer les secrets chiffrés'], ['ansible-playbook … --ask-vault-pass', 'fournir le mot de passe du coffre'], ['ansible-galaxy collection install', 'installer une collection'], ['ansible-lint', 'contrôler les bonnes pratiques']],
|
||||
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer l’idempotence et l’architecture sans agent. (4 pts)', 'Écrire un inventaire et un playbook de déploiement avec template et handler. (8 pts)', 'Rendre idempotentes trois tâches shell fournies. (4 pts)', 'Proposer une stratégie de déploiement sûr sur 50 serveurs. (4 pts)'], r: ['État désiré, SSH + Python', 'hosts.yaml, apt, template, service, handler', 'lineinfile, apt, creates', '--check, serial, validate, max_fail_percentage'] },
|
||||
prof: { deroule: [['Ouverture', 'Configurer trois VM à la main en chronométrant, puis avec Ansible'], ['Cours', 'Inventaire, playbooks, modules, idempotence'], ['Pratique', 'Lab trois VM'], ['Incident', 'Template invalide sur un parc'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : playbook à compléter. Élèves avancés : rôles, Vault, CI avec ansible-lint, Molecule.', eval: 'Lab validé (idempotence vérifiée), sujet type.' }
|
||||
},
|
||||
exemples: [
|
||||
['Tester l’accès à tout le parc', 'Avant un déploiement.', 'ansible -i inventaire all -m ping -o', 'web1.lycee.local | SUCCESS => {"changed": false,"ping": "pong"}\nweb2.lycee.local | UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Connection timed out"}', 'web2 est injoignable en SSH : on le traite avant de lancer le playbook, sinon le parc sera partiellement configuré.'],
|
||||
['Simuler un changement', 'Nouvelle version du template Nginx.', 'ansible-playbook site.yml --check --diff --limit web1', '--- before: /etc/nginx/conf.d/site.conf\n+++ after: site.conf.j2\n- client_max_body_size 8m;\n+ client_max_body_size 64m;\nweb1 : ok=5 changed=1 failed=0', 'Seule la ligne attendue change. On peut appliquer sereinement.'],
|
||||
['Vérifier l’idempotence', 'Contrôle qualité d’un playbook.', 'ansible-playbook site.yml | tail -3', 'PLAY RECAP\nweb1 : ok=7 changed=1 unreachable=0 failed=0\nweb2 : ok=7 changed=1 unreachable=0 failed=0', 'Au second passage, une tâche est encore « changed » : c’est une tâche shell sans changed_when. Il faut la remplacer par un module dédié ou décrire sa condition de changement.'],
|
||||
['Lire les facts d’une machine', 'Adapter un template à la mémoire disponible.', 'ansible db1 -m ansible.builtin.setup -a "filter=ansible_memtotal_mb"', 'db1 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 7957}}', 'La variable ansible_memtotal_mb est utilisable dans un template, par exemple pour dimensionner shared_buffers de PostgreSQL au quart de la mémoire.'],
|
||||
['Chiffrer un secret', 'Mot de passe de la base dans group_vars.', 'ansible-vault encrypt group_vars/db/vault.yml ; head -1 group_vars/db/vault.yml', 'Encryption successful\n$ANSIBLE_VAULT;1.1;AES256', 'Le fichier peut être versionné : sans le mot de passe du coffre, il est illisible.'],
|
||||
['Détecter les mauvaises pratiques', 'Revue de code avant fusion.', 'ansible-lint site.yml', 'command-instead-of-module: apt-get used in place of apt module\nsite.yml:14 Task/Handler: shell apt-get install -y nginx\nname[missing]: All tasks should be named\nsite.yml:22', 'ansible-lint signale une installation via shell et une tâche sans nom. On l’intègre dans la CI pour bloquer ces erreurs avant qu’elles atteignent la production.']
|
||||
],
|
||||
termes: ['Ansible', 'playbook', 'inventaire', 'module', 'tâche', 'handler', 'notify', 'idempotence', 'become', 'group_vars', 'host_vars', 'Jinja2', 'template', 'facts', 'rôle', 'collection', 'ansible-galaxy', 'Ansible Vault', 'ansible-lint', '--check', '--diff', '--limit', 'serial', 'validate', 'changed=0']
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue