Objectif de la journée : chaque push sur main déploie automatiquement la nouvelle version sur votre machine de l'Atelier, et vous vérifiez que l'application vit dans Prometheus et Grafana.
Hier, chaque commit poussé sur main produisait une image sur ghcr.io. Aujourd'hui, la chaîne va jusqu'au bout : un quatrième job, deploy, installe cette image sur une machine et vérifie qu'elle répond, puis Prometheus et Grafana montrent comment elle se porte.
Cette semaine, la machine de l'Atelier joue trois rôles à la fois :
| Rôle | Sur la machine de l'Atelier |
|---|---|
| Poste de développement | votre clone ~/devops-tasktracker, où vous éditez, testez, commitez et poussez |
| Runner GitHub Actions | un service qui exécute le job deploy de votre fork, et lui seul |
| Production | la stack nginx, app, db, prometheus et grafana, lancée par le job deploy |
Le runner est un programme de GitHub installé sur la machine. C'est lui qui contacte GitHub, en HTTPS sortant, pour demander s'il y a un job à exécuter : GitHub n'ouvre jamais de connexion vers la machine. Il n'y a donc aucun port entrant à ouvrir et aucune clé SSH à confier à GitHub, et la machine peut rester derrière un pare-feu. Les jobs lint, test et build continuent de tourner sur les machines de GitHub ; seul deploy tourne sur votre runner.
Ce matin, dans le lab superviser-service-prometheus-grafana, vous avez vu Grafana dans Firefox. La machine de l'Atelier n'a pas de navigateur : ici, vous interrogez Prometheus et Grafana avec curl, sur leur API.
Résultat attendu : vous savez dire où tourne chacun des quatre jobs, et pourquoi le runner n'a besoin d'aucun port entrant.
Ouvrez le lab Atelier projet DevOps. La machine est neuve : relancez atelier-init (un nouveau code vous sera demandé), puis placez-vous dans votre clone :
atelier-init
cd ~/devops-tasktrackerLa branche start-j4 du dépôt du cours, publiée ce matin, contient le kit du jour. Récupérez-le selon où vous en êtes.
Vous avez terminé le jour 3 (Atelier à 12/12) : prenez seulement les fichiers du kit. Votre workflow et votre compose.yml ne bougent pas ; le dossier src du kit, lui, remplace le vôtre (il contient la correction du jour 1 et le branchement des métriques).
git fetch upstream
git checkout upstream/start-j4 -- src package.json package-lock.json tests/metrics.test.js monitoring deploy
npm ci
npm run lint
npm test
git status
git commit -m "feat: kit du jour 4 (métriques, supervision, rôle du runner)"
git pushgit checkout upstream/start-j4 -- <chemins> copie ces chemins depuis la branche du cours et les ajoute déjà à l'index : git status les affiche en vert, prêts à être commités.
Vous n'avez pas terminé le jour 3 : fusionnez toute la branche, qui contient aussi la correction du jour 3 (c'est la section 11 de docs/J3.md) :
git add -A
git commit -m "wip: avant rattrapage" # ignorez le message si rien n'est à committer
git fetch upstream
git merge -X theirs --no-edit upstream/start-j4
git commit --allow-empty -m "rattrapage du jour 3"
npm ci
npm test
git push origin mainCe que contient le kit :
| Fichier | Rôle |
|---|---|
src/metrics.js, src/app.js |
les métriques de l'application et la route /metrics (section 3) |
tests/metrics.test.js |
un test qui vérifie que /metrics compte les requêtes |
package.json, package-lock.json |
ajoutent la bibliothèque prom-client |
monitoring/prometheus/prometheus.yml |
Prometheus lit http://app:3000/metrics toutes les 15 secondes |
monitoring/prometheus/alerts.yml |
l'alerte AppDown : l'application ne répond plus depuis 1 minute |
monitoring/grafana/provisioning/ |
la source de données Prometheus (uid prometheus) et l'emplacement des tableaux de bord, chargés par Grafana au démarrage |
monitoring/grafana/dashboards/tasktracker.json |
le tableau de bord Tasktracker, avec un premier panneau |
monitoring/export-dashboard.sh |
exporte vers le dépôt un tableau de bord modifié dans le navigateur (servira au projet final) |
deploy/site.yml, deploy/roles/runner/ |
le playbook et le rôle Ansible qui installent le runner (section 7) |
deploy/group_vars/app.yml.example |
le modèle de la variable github_owner |
Pourquoi vous ne l'écrivez pas vous-même : ces fichiers sont du code d'infrastructure courant, qu'on écrit rarement de zéro. On part d'un modèle, on le lit, on le comprend, puis on l'adapte. Ce matin, vous avez écrit un playbook et une configuration Prometheus et Grafana dans les labs ; cet après-midi, votre travail est de les brancher sur votre projet : la supervision dans la stack, un tableau de bord versionné, un fork protégé, le runner installé et le job de déploiement.
Résultat attendu : npm test est au vert, tests/metrics.test.js compris, et le push déclenche dans l'onglet Actions un run vert (lint, test, puis build).
Lisez src/metrics.js (less src/metrics.js, q pour quitter) :
| Élément | Ce qu'il mesure |
|---|---|
collectDefaultMetrics |
l'état du processus Node.js : mémoire, temps processeur, boucle d'événements |
http_requests_total |
un compteur : le nombre de requêtes traitées, par méthode, route et code de réponse ; il ne fait que monter, et repart de zéro quand l'application redémarre |
http_request_duration_seconds |
un histogramme : chaque durée est rangée dans une tranche (buckets, de 5 ms à 2,5 s), ce qui permet de calculer des percentiles |
metricsMiddleware |
à la fin de chaque réponse (finish), incrémente le compteur et enregistre la durée |
Dans src/app.js, app.use(metricsMiddleware) s'applique à toutes les requêtes, et la route GET /metrics renvoie les valeurs au format texte que lit Prometheus. Reconstruisez la stack avec ce code, puis faites quelques requêtes :
docker compose up -d --build --wait
curl -s localhost/tasks > /dev/null
curl -s -o /dev/null -X DELETE localhost/tasks/12345
curl -s localhost/metrics | grep '^http_requests_total'Pourquoi l'étiquette route contient le motif Express (/tasks/:id) et jamais l'URL reçue (/tasks/12345) : Prometheus crée une série temporelle pour chaque combinaison de valeurs d'étiquettes. Avec l'URL brute, chaque identifiant de tâche créerait une nouvelle série, et quelques milliers de tâches suffiraient à saturer la mémoire de Prometheus. Avec le motif, le nombre de séries reste fixe. Pour la même raison, une URL qui ne correspond à aucune route (un robot qui essaie des adresses au hasard) est comptée sous route="non_route".
En production, /metrics ne passe pas par le reverse proxy public : Prometheus le lit directement sur app:3000, par le réseau interne de Compose. Ici, le passage par Nginx sert seulement à le regarder avec curl.
Résultat attendu : parmi les lignes affichées, http_requests_total{method="GET",route="/tasks",status_code="200"} et http_requests_total{method="DELETE",route="/tasks/:id",status_code="404"}, avec le motif /tasks/:id et non /tasks/12345.
Avant de toucher à compose.yml, arrêtez la stack actuelle : vous allez changer le nom du projet Compose, et Compose ne retrouverait plus ces conteneurs sous le nouveau nom (ils garderaient le port 80).
docker compose down
nano compose.ymlAjoutez en première ligne du fichier, sans indentation :
name: tasktrackerSans cette ligne, Compose nomme le projet d'après le dossier. Or, dès la section 8, c'est le runner qui lance la stack, depuis sa propre copie de votre fork (/opt/actions-runner/_work/devops-tasktracker/devops-tasktracker). Avec un nom fixe, votre clone et la copie du runner pilotent le même projet, donc les mêmes conteneurs, au lieu de deux stacks qui se disputeraient les ports 80, 9090 et 3000.
Ajoutez ensuite deux services, qui respectent ces exigences :
| Exigence | prometheus |
grafana |
|---|---|---|
| Image | prom/prometheus:v3.13.3 |
grafana/grafana:13.2.2 |
| Port publié sur l'hôte | 9090:9090 |
3000:3000 |
| Montages | ./monitoring/prometheus:/etc/prometheus:ro (le dossier entier : prometheus.yml et alerts.yml) et le volume nommé prometheus-data:/prometheus |
./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro et ./monitoring/grafana/dashboards:/etc/grafana/dashboards:ro, aucun volume de données |
| Environnement | aucun | GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} |
| Démarre après | app (depends_on) |
prometheus (depends_on) |
Déclarez aussi prometheus-data: sous le bloc volumes: de premier niveau, à côté de db-data:.
Pourquoi ces choix :
- Des dossiers montés, pas des fichiers seuls. Quand Git remplace un fichier (checkout, pull), un conteneur qui monte ce fichier seul continue de voir l'ancienne version ; avec un dossier monté, il voit la nouvelle.
- Grafana sans volume de données. Tout ce que Grafana doit connaître vient du dépôt : la source de données et le tableau de bord sont chargés depuis
monitoring/grafana/à chaque démarrage (c'est le provisioning). À chaque recréation du conteneur, Grafana repart de zéro et réapplique le mot de passe deGF_SECURITY_ADMIN_PASSWORD. Ce qui n'est pas dans le dépôt disparaît : c'est voulu, un tableau de bord se versionne comme du code. ${GRAFANA_ADMIN_PASSWORD:-admin}. Compose prend la variableGRAFANA_ADMIN_PASSWORDsi elle est définie et non vide,adminsinon. La section 10 la fournit depuis un secret GitHub.- Des ports publiés pour Prometheus et Grafana : vous les interrogez avec
curldepuis la machine.appetdbrestent sans port publié, derrière Nginx.
Lancez la stack complète et vérifiez chaque service :
docker compose up -d --build --wait
docker compose ps
curl -s localhost:9090/api/v1/targets | python3 -m json.tool | grep -E '"job"|"health"'
curl -s -u admin:admin localhost:3000/api/health
curl -s -u admin:admin localhost:3000/api/datasources/uid/prometheus/healthGrafana met une trentaine de secondes à démarrer la première fois, et Prometheus lit l'application toutes les 15 secondes : si une commande ne répond pas encore, ou si la cible affiche "health": "unknown", attendez un peu et relancez-la.
Si docker compose up répond port is already allocated pour le port 3000, un npm start tourne encore dans une autre console : arrêtez-le avec Ctrl+C. Pour le port 80, docker ps montre le conteneur qui l'occupe.
Enregistrez et poussez :
git add compose.yml
git commit -m "feat: Prometheus et Grafana dans la stack"
git pushRésultat attendu : docker compose ps montre cinq services en marche ; la cible "job": "app" est "health": "up" ; /api/health de Grafana répond "database": "ok" ; la source de données répond "status": "OK".
Le fichier monitoring/grafana/dashboards/tasktracker.json décrit le tableau de bord Tasktracker (uid tasktracker, à ne pas changer). Il contient un panneau, le débit de requêtes par route. Ajoutez-lui un second panneau, la latence au 95e percentile :
{
"id": 2,
"type": "timeseries",
"title": "Latence p95 (secondes)",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))"
}
]
}Les panneaux sont les éléments de la liste "panels": [ ... ], séparés par des virgules. Trouvez la fin du premier panneau :
grep -n '"type": "timeseries"' monitoring/grafana/dashboards/tasktracker.json
nano +27 monitoring/grafana/dashboards/tasktracker.jsongrep affiche la ligne 26, "type": "timeseries" ; la ligne 27, }, ferme le premier panneau, juste avant la ligne ], qui ferme la liste. Sur la ligne 27, ajoutez une virgule après l'accolade ( },), puis collez le bloc ci-dessus sur la ligne suivante (Ctrl+Shift+V), avant ],. Enregistrez (Ctrl+O, Entrée) et quittez (Ctrl+X).
Vérifiez que le fichier est toujours du JSON valide, puis rechargez Grafana :
python3 -m json.tool monitoring/grafana/dashboards/tasktracker.json > /dev/null && echo "JSON valide"
docker compose restart grafana
sleep 10
curl -s -u admin:admin localhost:3000/api/dashboards/uid/tasktracker | python3 -c "import json, sys; d = json.load(sys.stdin)['dashboard']; print(len(d['panels']), [p['title'] for p in d['panels']])"Si la dernière commande affiche une erreur Python, Grafana n'a pas fini de redémarrer : relancez-la quelques secondes plus tard. Un message comme Expecting ',' delimiter: line 28 column 5 signale la virgule oubliée entre les deux panneaux : Grafana ignorerait tout le fichier. Corrigez le fichier dans nano, puis relancez ces quatre commandes.
Enregistrez et poussez :
git add monitoring/grafana/dashboards/tasktracker.json
git commit -m "feat: panneau de latence p95"
git pushIci, vous modifiez le JSON directement, car la machine n'a pas de navigateur. Quand vous modifiez un tableau de bord dans le navigateur, comme ce matin, ./monitoring/export-dashboard.sh le récupère depuis Grafana et réécrit ce fichier ; vous le commitez ensuite. Dans les deux cas, c'est le fichier du dépôt qui fait foi : un panneau ajouté seulement dans Grafana disparaît au prochain déploiement, et l'Atelier ne le compte pas.
Résultat attendu : la dernière commande affiche 2 ['Requetes par seconde', 'Latence p95 (secondes)'], et le commit est poussé sur votre fork.
Votre fork est public. N'importe qui peut le forker et vous proposer une pull request, et une pull request peut modifier le workflow lui-même : c'est son code, pas le vôtre, qui décide alors où tournent ses jobs. Dès que votre runner existe, une pull request pourrait donc faire exécuter son code sur la machine de l'Atelier, qui contient aussi votre accès à GitHub (la connexion créée par atelier-init).
Sur votre ordinateur, dans votre fork sur GitHub : Settings, Actions, General, rubrique Approval for running fork pull request workflows from contributors, choisissez Require approval for all external contributors, puis Save. Avec ce réglage, aucun workflow d'une pull request venant d'un fork ne démarre sans votre approbation. La valeur par défaut, Require approval for first-time contributors, laisse passer toute personne dont une contribution a déjà été acceptée.
Vérifiez depuis la console de l'Atelier :
GH_USER=$(gh api user --jq .login)
gh api "repos/$GH_USER/devops-tasktracker/actions/permissions/fork-pr-contributor-approval" --jq .approval_policyLe playbook de la section 7 lit ce même réglage à chaque exécution, et refuse d'installer le runner tant qu'il ne vaut pas all_external_contributors.
Cette semaine, n'approuvez jamais une pull request venant d'un fork sur votre dépôt : son code tournerait sur cette machine.
Résultat attendu : la commande affiche all_external_contributors.
Le rôle deploy/roles/runner installe le runner sur les machines du groupe app de l'inventaire. Aujourd'hui, ce groupe contient une seule machine : celle de l'Atelier elle-même.
Créez l'inventaire deploy/inventaire.ini (nano deploy/inventaire.ini) :
[app]
projet ansible_connection=localprojet est le nom qu'Ansible donne à la machine (le runner s'appellera range-projet) ; ansible_connection=local exécute les tâches sur la machine où vous lancez Ansible, sans SSH. Au projet final, la cible sera une autre machine, jointe par SSH : c'est l'inventaire qui changera.
Créez deploy/group_vars/app.yml sur le modèle de app.yml.example, avec votre identifiant GitHub, et vérifiez qu'Ansible lit la variable :
GH_USER=$(gh api user --jq .login)
printf 'github_owner: %s\n' "$GH_USER" > deploy/group_vars/app.yml
cat deploy/group_vars/app.yml
ansible -i deploy/inventaire.ini app -m ansible.builtin.debug -a var=github_ownerLisez ensuite deploy/roles/runner/tasks/main.yml, tâche par tâche :
| Tâches | Ce qu'elles font |
|---|---|
| Lire le réglage d'approbation, Arrêter si GitHub n'a pas donné le réglage, Refuser d'enregistrer le runner | lisent le réglage de la section 6 avec votre connexion gh, sur le poste de contrôle (delegate_to: localhost, become: false), et arrêtent tout s'il ne vaut pas all_external_contributors. Elles passent avant toute installation : un refus ne modifie rien sur la machine |
| Installer les bibliothèques | les paquets dont le runner a besoin ; cache_valid_time évite de relancer apt update à chaque passage |
| Créer l'utilisateur du runner | l'utilisateur runner, qui n'est pas root, ajouté au groupe docker pour lancer la stack |
| Créer le dossier, Télécharger, Extraire | /opt/actions-runner, archive officielle vérifiée par sa somme de contrôle SHA-256, extraite une seule fois (creates:) |
| Savoir si le runner est déjà enregistré, Exiger le jeton | le jeton d'enregistrement n'est exigé qu'au premier passage |
| Enregistrer le runner | config.sh lancé en tant que runner : nom range-projet, étiquette range, --replace (un runner du même nom est remplacé, pas dupliqué). Le jeton passe par une variable d'environnement et no_log: true le cache : il n'apparaît ni dans la sortie d'Ansible ni dans la liste des processus |
| Installer le service systemd, Lire le nom du service, Démarrer | le runner devient un service, démarré maintenant et à chaque démarrage de la machine |
Le runner ne tourne pas en root, mais être membre du groupe docker revient à avoir les droits de root sur la machine : un job qui tourne sur ce runner peut tout faire sur la machine de l'Atelier, y compris lire votre connexion GitHub. C'est pourquoi le réglage de la section 6 passe avant, et pourquoi la section 8 ne laisse qu'un seul job atteindre le runner.
Lancez le playbook avec un jeton d'enregistrement. GitHub le fabrique à la demande, il n'est valable qu'une heure et ne sert qu'à enregistrer un runner : générez-le juste avant, dans la même commande.
RUNNER_TOKEN=$(gh api -X POST "repos/$GH_USER/devops-tasktracker/actions/runners/registration-token" --jq .token) \
ansible-playbook -i deploy/inventaire.ini deploy/site.ymlRelancez-le ensuite sans jeton : le runner est déjà enregistré, rien ne doit changer.
ansible-playbook -i deploy/inventaire.ini deploy/site.yml
systemctl list-units 'actions.runner.*'
gh api "repos/$GH_USER/devops-tasktracker/actions/runners" --jq '.runners[] | .name + " " + .status'Enregistrez et poussez l'inventaire et la variable (le jeton, lui, n'est écrit nulle part) :
git add deploy/inventaire.ini deploy/group_vars/app.yml
git commit -m "feat: inventaire du runner"
git pushSi le playbook s'arrête sur « Refuser d enregistrer le runner », le réglage de la section 6 n'est pas en place : corrigez-le, puis relancez la commande avec le jeton. S'il s'arrête sur « Arreter si GitHub n a pas donne le reglage », lisez le message : identifiant faux dans deploy/group_vars/app.yml, gh non connecté (relancez atelier-init), ou GitHub qui ne répond pas (relancez dans une minute). S'il s'arrête sur « config.sh a refusé l'enregistrement du runner », le jeton a le plus souvent expiré (il vaut 1 heure) : relancez la commande RUNNER_TOKEN=... ansible-playbook, qui en demande un nouveau.
Résultat attendu : la commande ansible ... -m ansible.builtin.debug affiche "github_owner" avec votre identifiant ; le récapitulatif PLAY RECAP du premier passage affiche failed=0, celui du second changed=0 et failed=0 ; systemctl montre un service actions.runner. en active running ; GitHub affiche range-projet online, et la page Settings, Actions, Runners de votre fork montre range-projet à l'état Idle.
Ajoutez à .github/workflows/ci.yml un quatrième job, deploy, au même niveau que build. Le job complet n'est pas donné ; voici ce qu'il doit contenir.
| Clé | Valeur | Pourquoi |
|---|---|---|
needs |
build |
on ne déploie que l'image construite et publiée par ce même run |
if |
exactement la ligne ci-dessous | le job ne part que sur un push vers main ou un lancement manuel, jamais sur une pull request |
runs-on |
[self-hosted, range] |
les étiquettes de votre runner : ce job, et lui seul, tourne sur la machine de l'Atelier |
permissions |
contents: read et packages: read |
lire le code et tirer l'image privée de ghcr.io, rien d'autre |
concurrency |
un group: fixe (par exemple deploy-range) et cancel-in-progress: false |
deux déploiements ne tournent jamais en même temps sur la machine ; un déploiement commencé va jusqu'au bout, et parmi ceux qui arrivent entre-temps, seul le plus récent attend son tour |
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')Les étapes, dans l'ordre :
actions/checkout@v7: le runner a besoin decompose.yml, denginx/et demonitoring/de ce commit.docker/login-action@v4versghcr.io, avecusername: ${{ github.actor }}etpassword: ${{ secrets.GITHUB_TOKEN }}, comme dans le jobbuild(sans sonif:).- Une étape
run:qui enchaîne ces quatre commandes :
| Commande | Rôle |
|---|---|
export APP_IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}:sha-${GITHUB_SHA}" |
l'image exacte de ce commit, celle que build vient de publier ; ${GITHUB_REPOSITORY,,} met le nom du dépôt en minuscules, obligatoire sur ghcr.io |
docker compose pull app |
télécharge cette image depuis ghcr.io |
docker compose up -d --no-build --force-recreate --remove-orphans --wait --wait-timeout 180 |
recrée les conteneurs avec l'image téléchargée (--no-build : on ne reconstruit jamais sur la machine de production) et attend qu'ils soient en bonne santé |
curl -fsS --retry 10 --retry-delay 3 --retry-all-errors http://localhost/health |
le job n'est vert que si l'application répond via Nginx |
Trois règles, que l'Atelier vérifie :
- Aucun autre job sur le runner.
lint,testetbuildrestent surubuntu-latest; seuldeployporteself-hostedourange. - Le
ifexact. Sans lui, le job partirait aussi sur les pull requests, avec le code de la pull request. - Pas d'autre déclencheur que
push,pull_requestetworkflow_dispatchdans ce workflow : unschedule, par exemple, lancerait le déploiement surmainsans aucun push. Et, comme au jour 3, jamaispull_request_target.
Poussez, puis suivez le run dans l'onglet Actions de votre fork, sur votre ordinateur : lint et test, puis build, puis deploy. Le job deploy affiche un instant Waiting for a runner to pick up this job, puis démarre ; l'étape Set up job de son journal indique Runner name: 'range-projet'. Une fois le job vert, vérifiez sur la machine que la version qui tourne est bien celle de votre dernier commit :
docker compose ps
curl -s localhost/health
git rev-parse HEAD
docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' $(docker compose ps -q app)Désormais, c'est le runner qui lance la stack, depuis sa copie de votre fork. Ne relancez plus docker compose up --build dans ~/devops-tasktracker : vous remplaceriez l'image de ghcr.io par une image construite sur la machine. Pour changer quoi que ce soit (code, compose.yml, tableau de bord), commitez et poussez : le pipeline déploie.
Machine neuve. Si vous rouvrez l'Atelier, la machine ne contient plus ni runner ni stack. Dans l'ordre :
atelier-init,cd ~/devops-tasktracker, les commandesGH_USER=...puisRUNNER_TOKEN=... ansible-playbook ...de la section 7, avec un nouveau jeton (--replacegarde un seul runnerrange-projet), puisgh workflow run ci.yml --ref main, qui relance le pipeline sans commit : le jobdeployinstalle la stack sur la nouvelle machine. Le réglage de la section 6 est sur GitHub : il reste en place.
Runner hors ligne. Un push fait pendant que le runner est hors ligne (lab terminé, par exemple) laisse le job
deployen attente, à l'état Queued : il démarre dès que le runner revient en ligne, et GitHub l'abandonne au bout de 24 heures.
Résultat attendu : les quatre jobs sont verts pour votre dernier commit, deploy a tourné sur range-projet, docker compose ps montre l'image ghcr.io/<votre-compte>/devops-tasktracker:sha-... pour app, et les deux dernières commandes affichent le même numéro de commit.
Générez un peu de trafic, puis demandez à Prometheus le débit par route :
for i in $(seq 1 30); do curl -s -o /dev/null localhost/tasks; done
sleep 20
curl -s localhost:9090/api/v1/query --data-urlencode 'query=sum by (route) (rate(http_requests_total[1m]))' | python3 -m json.tool
curl -s localhost:9090/api/v1/rules | python3 -m json.tool | grep -E '"name"|"state"'rate() transforme le compteur, qui ne fait que monter, en requêtes par seconde ; il absorbe aussi la remise à zéro du compteur quand l'application redémarre. L'alerte AppDown est chargée, à l'état inactive.
Simulez maintenant une panne : arrêtez l'application et observez l'alerte.
docker compose stop app
for i in $(seq 1 24); do date +%T; curl -s localhost:9090/api/v1/alerts | python3 -m json.tool | grep -E '"alertname"|"state"'; sleep 5; doneLa boucle affiche l'état de l'alerte toutes les 5 secondes pendant 2 minutes (Ctrl+C pour l'arrêter plus tôt). En 30 secondes au plus, Prometheus n'arrive plus à lire app:3000 (up{job="app"} vaut 0) : l'alerte apparaît à l'état pending. Elle doit rester vraie pendant 1 minute (for: 1m dans alerts.yml) avant de passer à firing : ce délai évite d'alerter pour un simple redémarrage. Redémarrez ensuite l'application, et vérifiez que l'alerte disparaît :
docker compose start app
docker compose ps
curl -s localhost/health
sleep 40
curl -s localhost:9090/api/v1/alerts | python3 -m json.tool | grep -E '"alertname"|"state"'Si curl répond 502 Bad Gateway alors que app est en bonne santé, Nginx cherche encore l'ancienne adresse du conteneur : docker compose restart nginx, puis relancez le curl. docker compose stop et start agissent sur les conteneurs lancés par le runner, parce que votre clone et sa copie partagent le nom de projet tasktracker.
Résultat attendu : vous avez vu AppDown passer de pending à firing ; après le redémarrage, curl -s localhost/health répond {"status":"ok"} et la dernière commande n'affiche plus rien (l'alerte est résolue).
Le mot de passe admin de Grafana est écrit dans ce guide, donc connu de tous. Remplacez-le par un secret du dépôt :
- Sur GitHub, dans votre fork : Settings, Secrets and variables, Actions, New repository secret. Nom :
GRAFANA_ADMIN_PASSWORD, valeur : un mot de passe de votre choix. - Dans le job
deploy, ajoutez à l'étaperun::
env:
GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }}- Poussez, et attendez la fin du job
deploy. Grafana, recréé sans volume de données, applique le nouveau mot de passe.
Vérifiez, en tapant le mot de passe sans qu'il s'affiche ni reste dans l'historique de la console :
curl -s -o /dev/null -w '%{http_code}\n' -u admin:admin localhost:3000/api/dashboards/uid/tasktracker
read -rsp 'Mot de passe Grafana : ' GF_PASS; echo
curl -s -o /dev/null -w '%{http_code}\n' -u "admin:$GF_PASS" localhost:3000/api/dashboards/uid/tasktrackerGitHub masque la valeur du secret dans les journaux, et ne la donne jamais aux workflows déclenchés par une pull request venant d'un fork. Dans les commandes des sections précédentes, remplacez désormais admin:admin par "admin:$GF_PASS". Si vous supprimez le secret, Grafana revient à admin au déploiement suivant.
Résultat attendu : la première commande affiche 401, la dernière 200.
- Kit récupéré,
npm testau vert avectests/metrics.test.js -
compose.ymlavecname: tasktracker,prometheusetgrafana; cibleappàupdans Prometheus - Tableau de bord à 2 panneaux ou plus, commité et poussé
- Réglage Require approval for all external contributors sur votre fork
- Runner installé avec Ansible, second passage à
changed=0,range-projetà l'état Idle - Job
deployvert surrange-projetpour le dernier commit demain, seul job sur le runner - Alerte
AppDownobservée dependingàfiring, application redémarrée -
git statuspropre etgit pushfait ; attendez que le jobdeployde ce dernier push soit vert, puis Évaluer : l'Atelier doit afficher 18/18 (les vérifications du jour 4 sont décrites dans son guide) - Terminer l'Atelier : ce qui n'est pas poussé est perdu, et votre runner apparaît ensuite Offline sur GitHub, ce qui est normal
Deux règles valent jusqu'à la fin du cours : n'approuvez jamais une pull request venant d'un fork sur votre dépôt, et à la fin du jour 5, retirez l'accès donné à atelier-init (sur GitHub : Settings, Applications, onglet Authorized OAuth Apps, ligne GitHub CLI, Revoke).
Si vous n'avez pas terminé, récupérez la correction le jour 5, avant de commencer le projet final, dans votre clone de devops-tasktracker. La branche start-j5 n'existe dans le dépôt du cours qu'à partir du matin du jour 5. Commencez par enregistrer votre travail, puis fusionnez-la en gardant sa version en cas de conflit, et créez un commit à vous :
git add -A
git commit -m "wip: avant rattrapage" # ignorez le message si rien n'est à committer
git fetch upstream
git merge -X theirs --no-edit upstream/start-j5
git commit --allow-empty -m "rattrapage du jour 4"
npm ci
npm test
git push origin mainAprès la fusion, ouvrez deploy/group_vars/app.yml : github_owner doit être votre identifiant GitHub, pas votre-compte-github. Sinon, réécrivez-le avec la commande printf de la section 7, commitez et poussez.
Le commit vide rattrapage du jour 4 garantit que votre fork contient au moins un commit à vous, même quand la fusion n'en crée aucun. Le rattrapage apporte la stack, le job deploy et le tableau de bord ; il n'apporte ni le réglage de votre fork (section 6), qui se fait sur GitHub, ni le runner (section 7), qui s'installe sur la machine.