freedzhost.cloud

DevOps module 2 : chaînes CI/CD et conteneurs

Niveau DébutantTemps estimé 14 hÀ lire avant DevOps module 1 : la culture, Linux, le shell et GitUn mot inconnu ? Lexique

Quatorze séances pour faire vérifier la machine : une petite application avec ses tests, une chaîne qui les lance à chaque push, une image de conteneur bien construite, et un registre pour la livrer. Le module se termine par une application qui se construit, se teste et s'empaquette seule.

Les exemples utilisent la syntaxe GitHub Actions. Forgejo Actions est conçu pour sembler familier mais sa compatibilité n'est pas garantie : sur Forgejo, les workflows vivent dans .forgejo/workflows/, runs-on désigne une étiquette de votre runner (souvent docker), et le guide Forgejo Actions liste les différences. L'article du site votre propre CI sur Forgejo raconte les pièges rencontrés en pratique.

Jour 15 · 17/11 — Intégration, livraison, déploiement continus

Objectif : distinguer la CI, la livraison continue et le déploiement continu.

Théorie 1/2 : l'intégration continue

Grady Booch a employé les mots « continuous integration » dans son livre de conception orientée objet de 1991 ; l'Extreme Programming (Kent Beck, fin des années 1990) en a fait une pratique, et l'article de Martin Fowler (2000, révisé depuis) l'a décrite : chacun fusionne dans la ligne principale au moins une fois par jour, et chaque fusion est construite et testée automatiquement. Une construction cassée se répare en premier, avant toute autre chose. Le but : trouver les problèmes d'intégration quand ils sont petits.

Quelle équipe pratique l'intégration continue ?

  1. Chaque développeur travaille un mois sur une branche, puis tout est fusionné
  2. Chacun fusionne de petits changements tous les jours, et chaque fusion lance les tests
Réponse

Intégration fréquente plus tests automatiques.

Théorie 2/2 : livraison ou déploiement

Continuous Delivery (Jez Humble et David Farley, 2010) va plus loin : tout changement qui passe la chaîne est livrable ; un humain décide quand il part en production, d'un clic. Le déploiement continu supprime ce clic : tout changement qui passe part en production tout seul. La chaîne (pipeline) est l'enchaînement automatique : construire, tester, empaqueter, déployer.

La chaîne est verte et la mise en production attend le clic d'un responsable. C'est…

  1. de la livraison continue
  2. du déploiement continu
Réponse

Livraison : livrable à tout moment, livré sur décision.

La construction de main est rouge. Dans une équipe CI, qu'est-ce qui passe en premier ?

  1. Fusionner d'autres fonctionnalités
  2. Désactiver le test qui échoue
  3. Réparer (ou annuler) le changement qui l'a cassée
Réponse

Un main rouge bloque tout le monde ; le réparer est la priorité.

Pratique (30 min)

  1. Lisez la première partie de l'article de Martin Fowler sur l'intégration continue (en anglais), et écrivez ses pratiques sous forme de liste à cocher dans votre carnet.
  2. Dessinez la chaîne que vous voulez au jour 28 : push → analyse → tests → construction de l'image → envoi de l'image → (plus tard) déploiement. Pour chaque case, écrivez ce qui la fait échouer.
  3. Créez un nouveau dépôt public hello-ops sur votre forge et clonez-le.

Avec votre IA : « Donne-moi six courtes situations d'équipes logicielles et, pour chacune, demande-moi si c'est de l'intégration continue, de la livraison continue, du déploiement continu, ou rien de tout cela. Attends ma réponse à chaque fois et justifie la bonne en une phrase. »

Synthèse (10 min)

Trois lignes : CI, livraison, déploiement. Puis redessinez votre chaîne cible de mémoire.

Jour 16 · 18/11 — YAML et votre premier workflow

Objectif : écrire un workflow qui tourne à chaque push, et lire son journal.

Théorie 1/2 : YAML en cinq règles

YAML décrit des données par l'indentation : des espaces uniquement, jamais de tabulations ; clé: valeur pour un dictionnaire ; - élément pour une liste ; | pour un texte sur plusieurs lignes ; # pour un commentaire. Une indentation fausse change le sens sans erreur, d'où les outils d'analyse (yamllint).

name: ci
on: [push]
jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: echo "Hello from $GITHUB_REPOSITORY"
      - run: |
          uname -a
          ls -la

Peut-on indenter du YAML avec des tabulations ?

  1. Oui
  2. Non, des espaces uniquement
Réponse

Des espaces uniquement ; les tabulations sont refusées.

Théorie 2/2 : workflow, job, étape

GitHub Actions est apparu en 2018 et a été ouvert à tous en novembre 2019. Un workflow (un fichier dans .github/workflows/) réagit à des événements (on: push, pull_request, schedule…). Il contient des jobs, qui tournent en parallèle par défaut sur un runner (runs-on), sauf si l'un needs (a besoin d')un autre. Un job est une liste d'étapes : une commande shell (run) ou une action réutilisable (uses, comme actions/checkout, qui clone votre dépôt dans le runner). Une étape qui sort en non nul fait échouer le job.

Sans actions/checkout, un job voit-il votre code ?

  1. Non, le runner démarre vide
  2. Oui, toujours
Réponse

Le runner part d'une machine propre ; checkout clone le dépôt.

Quelle clé fait attendre un job qu'un autre soit fini ?

Réponse

needs:, par exemple needs: test.

Pratique (30 min)

  1. Dans hello-ops, créez .github/workflows/ci.yml (ou .forgejo/workflows/ci.yaml avec l'étiquette de votre runner) avec le workflow ci-dessus. Commitez et poussez.
  2. Ouvrez l'onglet Actions, puis l'exécution, puis chaque étape : lisez le journal.
  3. Ajoutez une étape run: exit 1, poussez, voyez l'exécution échouer et quelle étape est rouge. Retirez-la.
  4. Ajoutez un second job after avec needs: hello qui affiche « done », et vérifiez l'ordre sur le graphe de l'exécution.

Avec votre IA : « Voici mon premier workflow GitHub Actions : [collez ci.yml]. Pour chaque ligne, dis-moi en quelques mots ce qu'elle fait, puis pose-moi deux questions pour vérifier que j'ai compris la différence entre un job et une étape. »

Synthèse (10 min)

Trois lignes : les règles de YAML, workflow / job / étape, ce qui fait échouer un job. Puis réécrivez le workflow de mémoire.

Jour 17 · 19/11 — Les tests dans la chaîne

Objectif : donner ses tests à l'application et les faire lancer par la chaîne à chaque push et chaque pull request.

Théorie 1/2 : quoi tester, où

La pyramide des tests (Mike Cohn, Succeeding with Agile, 2009) : beaucoup de tests unitaires rapides (une fonction), moins de tests d'intégration (des morceaux ensemble : l'appli et sa base), peu de tests de bout en bout, lents (tout le système, comme un utilisateur). Dans une chaîne, les vérifications les moins chères passent d'abord : l'analyse statique (lint : style et erreurs évidentes), puis les tests unitaires, puis le reste, pour qu'un échec soit signalé en quelques secondes. Un test qui n'échoue jamais ne prouve rien : cassez le code une fois exprès et regardez-le passer au rouge.

Quels tests doivent être les plus nombreux ?

  1. Les tests unitaires
  2. Les tests de bout en bout
Réponse

Les tests unitaires : rapides, précis, bon marché.

Pourquoi lancer l'analyse statique avant les tests lents ?

  1. C'est obligatoire
  2. Pour échouer vite sur les erreurs bon marché
Réponse

Un retour rapide : des secondes, pas des minutes.

Théorie 2/2 : l'application

Un petit service web en Python, avec la seule bibliothèque standard, pour que rien ne cache son fonctionnement. app.py :

import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

VERSION = os.environ.get("APP_VERSION", "dev")


def route(path):
    if path == "/health":
        return 200, {"status": "ok"}
    if path == "/":
        return 200, {"message": "hello", "version": VERSION}
    return 404, {"error": "not found"}


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        code, body = route(self.path)
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(body).encode())


if __name__ == "__main__":
    HTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8080"))), Handler).serve_forever()

test_app.py, lancé par pytest :

from app import route


def test_health():
    assert route("/health") == (200, {"status": "ok"})


def test_unknown_path_is_404():
    assert route("/nothing")[0] == 404

Quel code renvoie route("/admin") ?

Réponse

404 : seuls / et /health existent.

Pratique (30 min)

  1. Tapez les deux fichiers dans hello-ops. En local : python3 -m venv .venv && . .venv/bin/activate && pip install pytest && pytest -q. Lancez l'appli avec python3 app.py et curl -i http://127.0.0.1:8080/health.
  2. Remplacez les étapes du workflow :
    on:
      push:
        branches: [main]
      pull_request:
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v7
          - name: Install tools
            run: |
              python3 -m venv .venv
              .venv/bin/pip install pytest ruff
          - name: Lint
            run: .venv/bin/ruff check .
          - name: Unit tests
            run: .venv/bin/pytest -q
  3. Poussez ; puis ouvrez une pull request qui casse route (renvoyer 500 pour /health) : la vérification passe au rouge sur la pull request. Fermez-la sans fusionner.
  4. Ajoutez un .gitignore avec .venv/ et __pycache__/.

Avec votre IA : « Voici ma fonction route() et ses deux tests : [collez les deux]. Propose trois tests unitaires de plus qui attraperaient un vrai bogue (pas des doublons des miens), chacun avec une phrase sur le bogue qu'il attrape. Je les écrirai moi-même. »

Le faire faire par un agent : les agents écrivent bien des tests unitaires quand on leur dit quel comportement compte. Consigne : « Ajoute des tests pytest pour route() couvrant / et un chemin inconnu avec une barre finale ; ne modifie pas app.py ; lance pytest et montre la sortie. » Vous vérifiez encore : chaque nouveau test échoue quand vous cassez le code qu'il couvre (modifiez une ligne, lancez, annulez).

Synthèse (10 min)

Trois lignes : la pyramide, pourquoi l'analyse statique d'abord, pourquoi voir un test échouer une fois. Puis réécrivez test_health de mémoire.

Jour 18 · 20/11 — Matrice, cache, artefacts, actions épinglées

Objectif : tester sur deux versions de Python, garder un rapport de tests, et épingler une action tierce par son commit.

Théorie 1/2 : matrice et artefacts

Une matrice lance le même job une fois par valeur : ici, dans deux images de conteneur Python officielles. Un artefact est un fichier que l'exécution garde après sa fin (un rapport, un binaire) ; un cache garde les téléchargements d'une exécution à l'autre pour gagner du temps.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.12", "3.13"]
    container: python:${{ matrix.python }}-slim
    steps:
      - uses: actions/checkout@v7
      - run: pip install pytest && pytest -q --junitxml=report.xml

Avec la matrice ci-dessus, combien de jobs de test tournent ?

Réponse

Deux, un par version de Python.

Théorie 2/2 : une étiquette peut mentir, un commit non

uses: owner/action@v4 lance ce vers quoi l'étiquette v4 pointe aujourd'hui, et une étiquette peut être déplacée. En mars 2025, un attaquant a redirigé les étiquettes de l'action très utilisée tj-actions/changed-files vers un commit qui affichait les secrets des runners dans les journaux (CVE-2025-30066) ; en mars 2026, la plupart des étiquettes de aquasecurity/trivy-action, l'action d'un scanner de sécurité, ont été poussées de force vers un voleur d'identifiants (CVE-2026-33634). La parade : épingler les actions tierces sur une empreinte de commit complète (SHA), avec la version en commentaire, et donner au jeton du workflow le moins de droits possible (permissions: contents: read).

permissions:
  contents: read
steps:
  - uses: some-org/some-action@<empreinte de commit de 40 caractères>  # v2.3.1

Trouvez vous-même l'empreinte d'une étiquette avec git ls-remote --tags https://github.com/some-org/some-action, jamais dans un billet de blog.

Quelle référence ne peut pas être changée en douce par le propriétaire de l'action ?

  1. @v4
  2. @ une empreinte de commit complète
  3. @main
Réponse

Une empreinte de commit désigne un seul contenu pour toujours. Étiquettes et branches bougent.

Qu'a fait la compromission de tj-actions ?

  1. Afficher les secrets de CI dans les journaux de construction
  2. Supprimer les dépôts
Réponse

Elle a vidé les secrets de la mémoire du runner dans des journaux lisibles par tous.

Pratique (30 min)

  1. Transformez votre job de test en la matrice ci-dessus ; poussez ; voyez deux jobs.
  2. Gardez le rapport : ajoutez une étape uses: actions/upload-artifact@v4 avec name: report-${{ matrix.python }} et path: report.xml (vérifiez la version majeure actuelle sur sa page). Sur Forgejo, lisez d'abord le guide : ses actions d'artefacts diffèrent.
  3. Ajoutez permissions: contents: read en tête du workflow.
  4. Cherchez l'empreinte de l'étiquette que vous utilisez pour actions/checkout avec git ls-remote --tags, épinglez-la, gardez la version en commentaire, poussez.

Avec votre IA : « Audite ce workflow pour les risques de chaîne d'approvisionnement : [collez ci.yml]. Liste chaque action tierce, si elle est épinglée par empreinte, les droits que reçoit le jeton, et le changement que tu ferais en premier. N'invente pas d'empreintes : dis-moi comment les trouver. »

Le faire faire par un agent : des outils et des agents peuvent épingler d'un coup toutes les actions d'un dépôt. Consigne : « Épingle chaque uses: sur l'empreinte de commit de son étiquette actuelle, trouvée avec git ls-remote, et garde l'étiquette en commentaire ; liste les recherches faites. » Vous vérifiez encore deux empreintes vous-même avec git ls-remote : une empreinte fausse, c'est exactement ce qu'un attaquant voudrait vous faire accepter.

Synthèse (10 min)

Trois lignes : matrice, artefact, pourquoi épingler par empreinte. Puis racontez l'histoire de tj-actions en deux phrases.

Jour 19 · 21/11 — Les runners : hébergés et auto-hébergés

Objectif : savoir où tournent vos jobs, et quand un runner sur votre propre machine est un risque.

Théorie 1/2 : qui exécute le job

Un runner est le programme qui prend les jobs auprès de la forge et les exécute. GitHub fournit des runners hébergés (une machine virtuelle neuve par job, gratuite sur les dépôts publics dans certaines limites). Vous pouvez aussi enregistrer un runner auto-hébergé sur votre machine : celui de GitHub, ou sur Forgejo le Forgejo Runner, qui lance chaque job dans un conteneur. L'étiquette runs-on choisit quel runner reçoit le job.

Qu'est-ce qui décide quel runner prend un job ?

  1. Le nom du job
  2. L'étiquette runs-on qui correspond aux étiquettes d'un runner
Réponse

Les étiquettes. « No matching runner » veut dire qu'aucun runner en ligne n'a cette étiquette.

Théorie 2/2 : le risque d'un runner auto-hébergé

Un job exécute le code que dit le workflow. Sur un dépôt public, n'importe qui peut ouvrir une pull request avec un workflow modifié ; sur un runner auto-hébergé, ce code tournerait sur votre machine, avec son réseau et ses fichiers. La documentation de GitHub déconseille les runners auto-hébergés sur les dépôts publics. L'isolation aide (un conteneur ou une VM jetable par job, pas de socket Docker monté, aucun secret sur l'hôte), tout comme l'obligation d'approuver les workflows des contributeurs extérieurs avant de les lancer.

Pourquoi un runner auto-hébergé sur un dépôt public est-il risqué ?

  1. La pull request d'un inconnu peut exécuter du code sur votre machine
  2. Il est plus lent
Réponse

Du code non fiable tourne sur votre infrastructure.

Le conteneur d'un runner reçoit le socket Docker de l'hôte. Que peut faire un job ?

  1. Seulement construire des images
  2. Piloter le Docker de l'hôte, donc en pratique devenir root sur l'hôte
Réponse

L'accès au socket Docker équivaut à root sur l'hôte.

Pratique (30 min)

  1. Lisez, dans une de vos exécutions, l'étape « Set up job » : quelle image, quelle version du runner, quelles étiquettes ?
  2. Lisez la page GitHub sur les runners auto-hébergés et sa partie sécurité ; notez trois risques dans votre carnet.
  3. Facultatif, si vous hébergez Forgejo (voir Homelab 8) : enregistrez un Forgejo Runner sur un dépôt de test privé, avec une étiquette à vous, et envoyez-lui un job.

Avec votre IA : « Je veux un runner CI auto-hébergé chez moi pour des dépôts privés uniquement. Liste les mesures d'isolation par ordre d'importance (sept au plus), chacune avec l'attaque qu'elle empêche. Marque tout ce dont tu n'es pas sûr par "à vérifier dans la doc". »

Synthèse (10 min)

Trois lignes : hébergé ou auto-hébergé, à quoi servent les étiquettes, pourquoi pas sur un dépôt public. Puis nommez deux mesures d'isolation sans notes.

Jour 20 · 22/11 — Les conteneurs : ce qu'ils sont

Objectif : expliquer ce qui isole un conteneur, et lancer vos premiers.

Théorie 1/2 : un processus entouré de murs

Un conteneur n'est pas une petite machine virtuelle : c'est un processus de l'hôte, qui partage le noyau de l'hôte, entouré de murs. Ces murs sont des fonctions de Linux : les namespaces (il voit ses propres processus, son réseau, son arborescence, son nom d'hôte) et les cgroups (il reçoit une part plafonnée de CPU et de mémoire). L'idée est ancienne : chroot (Unix V7, 1979), les jails de FreeBSD (2000), les cgroups dans Linux 2.6.24 (2008), LXC (2008). Docker, présenté par Solomon Hykes, de dotCloud, à PyCon en mars 2013, l'a rendue simple avec un format d'image et une ligne de commande ; l'Open Container Initiative (2015) a normalisé images et environnements d'exécution.

Un conteneur a-t-il son propre noyau ?

  1. Oui, comme une machine virtuelle
  2. Non, il partage le noyau de l'hôte
Réponse

Il partage le noyau ; une VM fait tourner le sien.

Quelle fonction de Linux limite le CPU et la mémoire d'un conteneur ?

Réponse

Les cgroups (control groups). Les namespaces isolent ce qu'il voit.

Théorie 2/2 : image et conteneur

Une image est un modèle en lecture seule (des fichiers plus des métadonnées : quoi lancer, quel port). Un conteneur est une instance en marche d'une image, avec une fine couche inscriptible qui disparaît avec lui. Une image, plusieurs conteneurs.

docker run --rm hello-world
docker run --rm -it debian:trixie bash   # un shell dedans, disparu à la sortie
docker run -d --name web -p 8081:80 nginx
docker ps; docker logs web; docker stop web; docker rm web

Vous écrivez un fichier dans un conteneur lancé avec --rm, puis vous sortez. Le fichier est…

  1. perdu
  2. enregistré dans l'image
Réponse

Perdu avec la couche inscriptible du conteneur. Les données qui comptent vont dans un volume (jour 23).

Pratique (30 min)

  1. Installez Docker Engine en suivant le guide officiel de votre distribution ; vérifiez docker version.
  2. Lancez hello-world, puis un shell Debian : à l'intérieur, lancez ps aux et hostname, et comparez avec l'hôte.
  3. Démarrez nginx sur le port 8081, curl -I http://127.0.0.1:8081, lisez docker logs web, arrêtez-le et supprimez-le.
  4. Sur l'hôte, trouvez le processus du conteneur : ps aux | grep nginx pendant qu'il tourne. C'est juste un processus.

Ajouter votre utilisateur au groupe docker vous épargne sudo, mais ce groupe équivaut à root sur la machine (leçon d'hier). Sur une machine partagée, pensez au mode rootless de Docker ou à Podman.

Avec votre IA : « Explique-moi la différence entre un conteneur et une machine virtuelle à trois niveaux : une phrase pour un débutant, un paragraphe qui parle des namespaces et des cgroups, et un tableau qui compare noyau, temps de démarrage, isolation et taille. Puis interroge-moi avec trois questions. »

Synthèse (10 min)

Trois lignes : namespaces, cgroups, image ou conteneur. Puis refaites la suite nginx sans notes.

Jour 21 · 23/11 — Le Dockerfile et ses couches

Objectif : empaqueter hello-ops dans une image, et ordonner le Dockerfile pour que les reconstructions soient rapides.

Théorie 1/2 : les instructions

FROM python:3.13-slim        # image de base
WORKDIR /app                 # dossier de travail
COPY app.py .                # fichiers du contexte de construction
EXPOSE 8080                  # documente le port
CMD ["python", "app.py"]     # ce que lance le conteneur

RUN exécute une commande à la construction (installer des paquets) ; ENV pose une variable ; CMD est la commande par défaut, ENTRYPOINT la commande fixe. docker build -t hello-ops:dev . construit ; le . est le contexte de construction envoyé au constructeur, filtré par .dockerignore.

Quelle instruction s'exécute à la construction ?

  1. CMD
  2. RUN
Réponse

RUN à la construction ; CMD au démarrage du conteneur.

Théorie 2/2 : couches et cache

Chaque instruction produit une couche. À la reconstruction, Docker réutilise chaque couche jusqu'à la première dont l'entrée a changé, et reconstruit tout ce qui suit. Mettez donc ce qui change rarement (paquets système, dépendances) avant ce qui change souvent (votre code) :

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt   # en cache tant que les dépendances ne changent pas
COPY . .                                              # votre code, en dernier

Vous changez une ligne de app.py. Avec l'ordre ci-dessus, pip install est-il relancé ?

  1. Non, sa couche vient du cache
  2. Oui, tout est reconstruit
Réponse

Seules les couches à partir de COPY . . sont reconstruites.

Quel fichier garde .git et .venv hors du contexte de construction ?

Réponse

.dockerignore.

Pratique (30 min)

  1. Écrivez le premier Dockerfile ci-dessus dans hello-ops, plus un .dockerignore avec .git, .venv, __pycache__.
  2. docker build -t hello-ops:dev ., puis docker run --rm -p 8080:8080 hello-ops:dev et curl http://127.0.0.1:8080/.
  3. Lancez-le avec -e APP_VERSION=1.0.0 : la version apparaît dans la réponse.
  4. Lisez les couches avec docker history hello-ops:dev. Modifiez app.py, reconstruisez, et regardez quelles étapes affichent CACHED.

Avec votre IA : « Voici mon Dockerfile : [collez-le]. Explique l'effet sur le cache de la position de chaque instruction, et dis-moi quelle instruction invaliderait le cache le plus souvent. Pas de réécriture, juste le raisonnement. »

Le faire faire par un agent : un agent peut écrire un premier Dockerfile pour n'importe quel petit projet. Consigne : « Écris un Dockerfile pour ce dépôt : base officielle slim, dépendances avant le code, .dockerignore compris ; construis-le et lance la vérification de santé. » Vous vérifiez encore : l'image démarre, /health répond, et docker history ne montre rien qui ne devrait pas y être (un .env, le dossier .git).

Synthèse (10 min)

Trois lignes : contexte de construction, couches, ordre pour le cache. Puis réécrivez le Dockerfile sans notes.

Jour 22 · 24/11 — De bonnes images : petites, sans root, multi-étapes

Objectif : réduire l'image, la faire tourner sous un utilisateur normal, et la désigner par son empreinte.

Théorie 1/2 : multi-étapes et sans root

Une construction multi-étapes utilise une étape pour construire ou tester et ne copie que le résultat dans une étape finale propre : outils et fichiers de test n'atteignent jamais la production. Et par défaut un conteneur tourne en root : ajoutez un utilisateur et basculez dessus avec USER, pour qu'une faille de l'appli ne donne pas root dans le conteneur.

FROM python:3.13-slim AS test
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir pytest && pytest -q

FROM python:3.13-slim
WORKDIR /app
COPY --from=test /app/app.py .
RUN useradd --system --uid 10001 app
USER app
EXPOSE 8080
CMD ["python", "app.py"]

Dans ce Dockerfile, pytest est-il dans l'image finale ?

  1. Oui
  2. Non, seul app.py est copié depuis l'étape de test
Réponse

L'étape finale repart de la base propre.

Quelle instruction fait tourner le conteneur sous un autre utilisateur ?

Réponse

USER, après avoir créé l'utilisateur.

Théorie 2/2 : étiquettes et empreintes

Une étiquette (python:3.13-slim, latest) est un nom mobile, comme une étiquette Git. Une empreinte (digest, python@sha256:…) désigne un contenu exact. Épinglez l'empreinte quand vous voulez une construction reproductible ; laissez un robot (Renovate, Dependabot) proposer les mises à jour. latest veut seulement dire « la dernière poussée sans étiquette », pas « la plus récente stable ».

Quelle référence garantit la même image demain ?

  1. @sha256:…
  2. :latest
Réponse

L'empreinte. latest peut changer à chaque envoi.

Pratique (30 min)

  1. Remplacez votre Dockerfile par la version multi-étapes ; construisez ; cassez un test et voyez la construction échouer à l'étape de test ; réparez.
  2. Vérifiez l'utilisateur : docker run --rm hello-ops:dev id affiche l'uid 10001.
  3. Comparez les tailles : docker images, puis essayez FROM python:3.13 (pas slim) et comparez.
  4. Trouvez l'empreinte de votre base : docker inspect --format '{{index .RepoDigests 0}}' python:3.13-slim, et notez-la.

Avec votre IA : « Relis mon Dockerfile selon ces critères, et réponds par un tableau avec les colonnes critère / respecté ou non / ligne : utilisateur non root, multi-étapes, aucun secret, base slim, dépendances avant le code, base épinglée. Le voici : [collez-le]. »

Le faire faire par un agent : un agent peut durcir un Dockerfile selon cette liste. Vous vérifiez encore en le lançant : id dans le conteneur, la taille, la vérification de santé, et que les tests tournent toujours à la construction.

Synthèse (10 min)

Trois lignes : multi-étapes, sans root, étiquette ou empreinte. Puis réécrivez l'étape finale sans notes.

Jour 23 · 25/11 — Ports, volumes, réseaux, débogage

Objectif : garder des données d'un conteneur à l'autre, relier deux conteneurs par leur nom, et déboguer un conteneur qui ne démarre pas.

Théorie 1/2 : ports et volumes

-p 8080:80 publie le port 80 du conteneur sur le port 8080 de l'hôte (l'hôte d'abord). Un volume garde les données hors de la couche inscriptible du conteneur : un volume nommé (-v pgdata:/var/lib/postgresql/data) est géré par Docker ; un montage lié (bind mount, -v "$PWD/conf:/etc/app:ro") montre un dossier de l'hôte à l'intérieur, ici en lecture seule.

-p 3000:8080 : sur quel port de l'hôte joint-on l'appli ?

  1. 8080
  2. 3000
Réponse

Le port de l'hôte d'abord : 3000 sur l'hôte mène à 8080 dans le conteneur.

Théorie 2/2 : réseaux et débogage

Les conteneurs d'un même réseau défini par l'utilisateur se joignent par leur nom : Docker leur fournit un DNS. Pour déboguer : docker ps -a (s'est-il arrêté ? avec quel code ?), docker logs, docker inspect (configuration, montages, réseau), docker exec -it nom sh (un shell dans un conteneur en marche).

docker network create lab
docker run -d --name db --network lab -e POSTGRES_PASSWORD=dev-only postgres:17
docker run --rm --network lab postgres:17 pg_isready -h db

Un conteneur s'arrête juste après avoir démarré. Quelle commande montre ce qu'il a affiché ?

Réponse

docker logs nom (marche aussi sur un conteneur arrêté).

Comment le second conteneur trouve-t-il db ?

  1. Le DNS de Docker sur le réseau défini par l'utilisateur résout le nom
  2. Par le /etc/hosts de l'hôte
Réponse

Sur un réseau défini par l'utilisateur, les noms des conteneurs se résolvent.

Pratique (30 min)

  1. Lancez les trois commandes ci-dessus ; puis docker exec -it db psql -U postgres -c "create table t(x int); insert into t values (42);".
  2. Supprimez le conteneur, relancez-le sans volume : la table a disparu. Relancez avec -v pgdata:/var/lib/postgresql/data, créez la table, supprimez et recréez le conteneur : la table reste. (Vérifiez le chemin des données pour votre version de PostgreSQL sur la page de l'image.)
  3. Cassez quelque chose exprès : lancez hello-ops:dev avec -e PORT=abc. Trouvez pourquoi il s'arrête avec docker ps -a et docker logs.
  4. Rangez : docker rm -f db, docker network rm lab, docker volume ls.

Avec votre IA : « Mon conteneur s'arrête immédiatement. Voici docker ps -a et docker logs : [collez les deux]. Donne-moi les trois causes les plus probables par ordre, la commande qui confirmerait chacune, et rien d'autre. »

Le faire faire par un agent : un agent avec accès à Docker peut dérouler toute la routine ps / logs / inspect et résumer. Demandez-lui de citer la ligne du journal sur laquelle il fonde son diagnostic. Vous vérifiez encore cette ligne vous-même, et c'est vous qui appliquez la correction.

Synthèse (10 min)

Trois lignes : l'ordre des ports, volume nommé ou montage lié, les quatre commandes de débogage. Puis refaites le test du volume sans notes.

Jour 24 · 26/11 — Docker Compose

Objectif : décrire une pile de deux services dans un fichier et la lancer d'une commande.

Théorie 1/2 : le fichier compose

Compose décrit conteneurs, réseaux et volumes dans un fichier YAML, compose.yaml, selon la Compose Specification. La commande est docker compose (Compose v2, un greffon de Docker) ; l'ancien docker-compose et la clé de premier niveau version: sont obsolètes. Chaque service rejoint un réseau par défaut et joint les autres par leur nom de service.

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      APP_VERSION: dev
    depends_on:
      cache:
        condition: service_healthy
  cache:
    image: redis:8
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      retries: 5

Comment app joint-il le cache ?

  1. Par l'IP de l'hôte
  2. Par le nom de service cache
Réponse

Compose met les deux sur un réseau où les noms de service se résolvent.

Théorie 2/2 : les commandes de tous les jours

docker compose up -d --build    # construire et démarrer en arrière-plan
docker compose ps               # état et santé
docker compose logs -f app      # suivre un service
docker compose down             # arrêter et supprimer (ajoutez -v pour supprimer les volumes)

depends_on avec condition: service_healthy attend que la vérification de santé de la dépendance passe, pas seulement qu'elle démarre. Les secrets vont dans un fichier .env listé dans .gitignore, jamais dans compose.yaml.

Quelle commande arrête la pile et supprime ses volumes nommés ?

Réponse

docker compose down -v : attention, les données partent avec.

Sans vérification de santé, depends_on attend…

  1. seulement que le conteneur soit démarré, pas qu'il soit prêt
  2. que le service réponde aux requêtes
Réponse

Démarré n'est pas prêt ; la vérification de santé fait la différence.

Pratique (30 min)

  1. Écrivez le fichier compose ci-dessus dans hello-ops (vérifiez l'étiquette actuelle de Redis sur sa page d'image).
  2. docker compose up -d --build, docker compose ps jusqu'à ce que cache soit healthy, curl http://127.0.0.1:8080/health.
  3. Ajoutez aussi une vérification de santé à app, en Python : test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"].
  4. docker compose logs -f, puis docker compose down.

Avec votre IA : « Voici mon compose.yaml : [collez-le]. Vérifie-le par rapport à la Compose Specification : clés obsolètes, vérifications de santé manquantes, secrets en clair, ports publiés sans nécessité. Réponds par une liste de six constats au plus, avec la clé concernée. Dis "pas sûr" quand tu ne l'es pas. »

Le faire faire par un agent : un agent peut transformer une série de commandes docker run en fichier compose. Donnez-lui les commandes exactes du jour 23. Vous vérifiez encore : docker compose config le valide, la pile devient healthy, et aucun mot de passe n'a atterri dans le fichier.

Synthèse (10 min)

Trois lignes : ce que remplace Compose, comment les services se trouvent, démarré ou en bonne santé. Puis réécrivez le fichier compose de mémoire.

Jour 25 · 27/11 — Registres et étiquettes

Objectif : envoyer votre image sur un registre, avec des étiquettes qui disent ce qu'il y a dedans.

Théorie 1/2 : les registres

Un registre stocke et sert des images : Docker Hub (par défaut quand aucun hôte n'est donné), ghcr.io de GitHub, le registre de conteneurs intégré à Forgejo, ou le vôtre. Le nom complet d'une image est hôte/propriétaire/nom:étiquette, par exemple ghcr.io/alice/hello-ops:1.0.0. On se connecte avec un jeton limité aux paquets, jamais son mot de passe, passé sur l'entrée standard pour qu'il n'atterrisse pas dans l'historique du shell.

echo "$TOKEN" | docker login ghcr.io -u alice --password-stdin
docker tag hello-ops:dev ghcr.io/alice/hello-ops:1.0.0
docker push ghcr.io/alice/hello-ops:1.0.0

docker pull nginx : depuis quel registre ?

  1. ghcr.io
  2. Docker Hub
Réponse

Sans hôte, Docker Hub (docker.io/library/nginx).

Quelle option de docker login lit le jeton sur l'entrée standard ?

Réponse

--password-stdin.

Théorie 2/2 : une stratégie d'étiquettes

Donnez à chaque construction une étiquette immuable qui ramène au code : l'empreinte du commit (:3f9c2ab) ; donnez aux versions publiées une version sémantique (:1.4.0). Ne redéployez jamais un autre contenu sous la même étiquette de version. Une étiquette mobile comme :main convient pour « la dernière construction de main », jamais pour la production.

La production tourne sur :latest et casse après un redémarrage. Pourquoi est-ce dur à déboguer ?

  1. Vous ne savez pas quelle construction était latest avant et après
  2. Parce que latest est lent
Réponse

Une étiquette mobile cache ce qui a changé. Déployez par empreinte ou par version.

Pratique (30 min)

  1. Créez sur votre forge un jeton avec le seul droit paquets / registre ; gardez-le dans votre gestionnaire de mots de passe.
  2. Connectez-vous, étiquetez hello-ops:dev avec l'empreinte de votre commit (git rev-parse --short HEAD) et avec 0.1.0, envoyez les deux.
  3. Voyez le paquet sur la forge ; rendez-le public si votre dépôt l'est.
  4. Supprimez l'image locale et lancez-la depuis le registre : docker run --rm -p 8080:8080 ghcr.io/vous/hello-ops:0.1.0.

Avec votre IA : « Propose un schéma d'étiquettes d'images pour une petite appli déployée depuis main, avec des versions marquées dans Git en v1.2.3. Donne un tableau : événement, étiquettes envoyées, qui peut la déployer. Puis donne-moi deux erreurs que font les équipes avec les étiquettes et comment mon schéma les évite. »

Synthèse (10 min)

Trois lignes : le nom complet d'une image, la connexion par jeton sur l'entrée standard, les étiquettes par empreinte et par version. Puis refaites l'étiquetage et l'envoi sans notes.

Jour 26 · 28/11 — La chaîne construit l'image, et prompter pour une chaîne

Objectif : faire construire et envoyer l'image par la chaîne une fois les tests passés sur main, et écrire un prompt qui donne une chaîne digne de confiance.

Théorie 1/3 : un job de construction après les tests

  image:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v7
      - name: Build and push
        env:
          TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          image=ghcr.io/${{ github.repository }}:${{ github.sha }}
          echo "$TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
          docker build -t "$image" .
          docker push "$image"

needs: test ne construit qu'après les tests ; if: réserve l'envoi à main, si bien qu'une pull request teste sans rien publier ; le jeton reçoit packages: write pour ce seul job. Sur Forgejo, utilisez un secret que vous créez avec un jeton de registre, et l'hôte de votre instance. (Le nom du dépôt doit être en minuscules pour un registre.)

Une pull request lance ce workflow. Une image est-elle envoyée ?

  1. Oui
  2. Non, le if: limite le job à main
Réponse

Les tests tournent sur la pull request ; la publication seulement sur main.

Pourquoi packages: write sur ce job plutôt que sur tout le workflow ?

  1. Moindre privilège : le job de test n'a pas besoin de publier
  2. C'est une syntaxe obligatoire
Réponse

Chaque job reçoit seulement les droits qu'il utilise.

Théorie 2/3 : prompter pour une chaîne

Un prompt faible : « écris une chaîne GitHub Actions pour mon appli Python avec Docker ». Ce qui ne va pas : le modèle ne connaît ni vos fichiers, ni votre registre, ni quand publier, ni vos règles de sécurité ; il ajoutera des actions que vous n'avez pas demandées, dans des versions dont il se souvient, peut-être périmées ou inventées.

Le prompt retravaillé :

Rôle : tu es un ingénieur CI qui applique le moindre privilège.
Contexte : GitHub Actions ; le dépôt contient app.py, test_app.py,
Dockerfile (multi-étapes, tests dans la construction) ; registre ghcr.io.
Tâche : un seul workflow .github/workflows/ci.yml.
Contraintes : tester à chaque push et pull request ; construire et
envoyer seulement sur main, après les tests ; étiqueter avec l'empreinte
du commit ; permissions de premier niveau contents: read ; packages:
write sur le seul job d'envoi ; aucune action tierce sauf
actions/checkout ; aucun secret en clair.
Entrée : voici mon ci.yml actuel : [coller].
Sortie : le fichier complet, puis la liste de chaque changement et pourquoi.
Vérification : je lancerai actionlint et j'ouvrirai une pull request
pour voir qu'elle teste sans envoyer.

Quelle ligne du prompt retravaillé empêche le modèle d'ajouter des actions inconnues ?

  1. Le rôle
  2. Le format de sortie
  3. « aucune action tierce sauf actions/checkout »
Réponse

Cette contrainte ; chaque action en plus est du code auquel il faudrait faire confiance.

Corrigez ce prompt : « Rends ma chaîne plus rapide. » L'ajout le plus utile ?

  1. « S'il te plaît, c'est urgent »
  2. Le fichier du workflow, la durée de chaque étape lors de la dernière exécution, et « ne supprime aucun test »
Réponse

Les vraies entrées et la contrainte qui protège l'essentiel.

Pratique (30 min)

  1. Ajoutez le job image à ci.yml ; poussez sur main ; vérifiez que le paquet apparaît avec l'empreinte du commit comme étiquette.
  2. Ouvrez une pull request : vérifiez qu'elle teste sans envoyer.
  3. Analysez le workflow avec actionlint (il existe en image Docker : docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest).

Avec votre IA : envoyez le prompt retravaillé ci-dessus avec votre vrai ci.yml. Comparez sa réponse avec la vôtre ligne à ligne ; passez actionlint dessus ; ne gardez que les changements que vous savez expliquer.

Le faire faire par un agent : un agent dans votre dépôt peut désormais entretenir la chaîne. Consigne : « Ajoute un job qui lance shellcheck sur scripts/*.sh avant les tests ; garde des droits minimaux ; lance actionlint ; ouvre une pull request. » Vous vérifiez encore l'exécution sur la pull request, le bloc des permissions, et que rien n'est envoyé depuis une pull request.

Synthèse (10 min)

Trois lignes : needs, if sur main, les droits par job ; et les parties d'un prompt pour une chaîne. Puis réécrivez le job image de mémoire.

Jour 27 · 29/11 — Mettre en production : progressif, bleu-vert, canari, retour arrière

Objectif : choisir une stratégie de mise en production et savoir défaire une version.

Théorie 1/2 : quatre stratégies

  • Recréation : arrêter l'ancienne version, démarrer la nouvelle. Simple, avec interruption.
  • Mise à jour progressive (rolling update) : remplacer les instances quelques-unes à la fois ; les deux versions tournent ensemble un moment (le comportement par défaut de Kubernetes, jour 39).
  • Bleu-vert : la nouvelle version (verte) tourne à côté de l'ancienne (bleue) ; le trafic bascule d'un coup ; rebasculer, c'est le retour arrière.
  • Canari : une petite part du trafic va vers la nouvelle version ; on surveille les métriques, puis on élargit ou on abandonne.

Les drapeaux de fonctionnalité séparent le déploiement du code de la mise à disposition d'une fonctionnalité : le code part éteint.

5 % des utilisateurs reçoivent la nouvelle version pendant que vous surveillez le taux d'erreur. Quelle stratégie ?

  1. Recréation
  2. Bleu-vert
  3. Canari
Réponse

Canari : une petite part exposée, puis on élargit.

Pendant une mise à jour progressive, ancienne et nouvelle versions peuvent-elles servir en même temps ?

  1. Non
  2. Oui : l'appli et ses changements de base doivent tolérer les deux
Réponse

Oui, donc les changements doivent rester compatibles avec la version d'avant.

Théorie 2/2 : retour arrière et changements de base de données

Un retour arrière redéploie l'étiquette d'image précédente : une raison de plus pour des étiquettes immuables. Les changements de schéma de base de données ne reviennent pas aussi facilement, on les découpe donc en étapes compatibles (le motif étendre puis contracter, expand and contract) : ajouter la nouvelle colonne, déployer un code qui utilise les deux, migrer les données, puis supprimer l'ancienne colonne dans une version ultérieure.

Pourquoi ajouter une nouvelle colonne avant de supprimer l'ancienne, en deux versions ?

  1. Pour que la version précédente marche encore en cas de retour arrière
  2. Pour utiliser plus de disque
Réponse

Chaque étape reste compatible avec la version d'avant.

Pratique (30 min)

  1. Simulez le bleu-vert avec deux conteneurs : lancez l'étiquette A sur le port 8081 et l'étiquette B sur le port 8082 (-e APP_VERSION différent). Votre « routeur » est le port que vous interrogez ; basculez, puis rebasculez.
  2. Écrivez dans votre carnet la procédure de retour arrière de hello-ops : quelle commande, quelle étiquette, comment vérifier que c'est fait (/ affiche la version).
  3. Pour chaque stratégie, écrivez un cas où vous la choisiriez.

Avec votre IA : « Je déploie une petite API avec Docker Compose sur un seul serveur. Compare recréation, bleu-vert et canari pour cette configuration dans un tableau (interruption, vitesse du retour arrière, ce qu'il me faut pour la mettre en place), puis demande-moi par laquelle je commencerais et pourquoi. »

Synthèse (10 min)

Trois lignes : les quatre stratégies, déployer ou mettre à disposition, étendre puis contracter. Puis refaites la bascule bleu-vert sans notes.

Jour 28 · 30/11 — Livrable du module 2 : une appli qui se livre seule

Objectif : livrer hello-ops avec sa chaîne, son image et sa documentation, et vérifier vos acquis.

Théorie 1/2 : ce que prouve le livrable

Un lecteur de votre dépôt doit voir, sans demander : chaque pull request est analysée et testée ; main produit dans un registre une image étiquetée par commit ; l'image est petite et tourne sans root ; la pile démarre d'une commande ; le README dit comment lancer, tester et revenir en arrière.

Un badge du README dit « build passing ». Que prouve-t-il ?

  1. Que l'appli marche en production
  2. Que la dernière exécution de la chaîne sur cette branche a passé ses vérifications
Réponse

Seulement ce que vérifie la chaîne : il vaut ce que valent ses tests.

Théorie 2/2 : révision du module

Tout changement qui passe part en production automatiquement. C'est…

  1. du déploiement continu
  2. de la livraison continue
Réponse

Déploiement ; la livraison garde une décision humaine.

Quelle référence est la plus sûre pour une action tierce ?

  1. @main
  2. @v3
  3. une empreinte de commit complète
Réponse

L'empreinte ; étiquettes et branches peuvent être redirigées.

Vous ne changez que votre code. Quel ordre du Dockerfile garde la couche des dépendances en cache ?

  1. COPY . . puis pip install
  2. copier les dépendances, pip install, puis COPY . .
Réponse

Les dépendances avant le code.

Exercice généré sans fin (JavaScript requis) : les codes HTTP que rencontreront vos vérifications de santé.

Pratique (30 min)

  1. Vérifiez la liste : workflow avec analyse, tests (matrice), job d'image sur main avec étiquettes par empreinte ; actions épinglées ; droits minimaux ; Dockerfile multi-étapes sans root ; compose.yaml avec vérifications de santé ; .dockerignore et .gitignore.
  2. Écrivez le README : ce que fait l'appli, un schéma de la chaîne (du texte suffit), comment lancer avec Compose, comment lancer les tests, comment revenir en arrière.
  3. Marquez une version : git tag v0.1.0 && git push --tags, et notez l'image qui lui correspond.
  4. Ajoutez le dépôt à votre liste de portfolio : livrable 2.

Avec votre IA : « Voici l'arborescence de mon dépôt, mon ci.yml et mon Dockerfile : [collez]. Joue un relecteur confirmé : liste ce qu'une équipe de production exigerait encore avant de faire confiance à cette chaîne (sept points au plus, le plus important d'abord). N'écris pas de code. »

Le faire faire par un agent : un agent peut rédiger la partie « chaîne » du README à partir de ci.yml. Vous vérifiez encore qu'il décrit ce que fait le fichier, pas ce que font les chaînes en général : comparez chaque phrase avec un job ou une étape.

Synthèse (10 min)

Comptez votre score. Les sujets manqués reviennent au jour 55, quand la chaîne finale est construite.

Sources

Utile ? Dites-nous ce qu'il faut mesurer ensuite.