fix(ci,crowdsec): écarter le runner de pi1, figer la résolution de la forge, couper le cache Redis du bouncer

Trois correctifs issus d'une même soirée de panne.

1. Runner hors du control-plane (03_cicd.yml)

`capacity: 1` ne borne que CHAQUE runner, pas le cluster : avec deux runners,
deux builds lourds tournent en parallèle. Le 2026-08-26 ils ont saturé pi1
(charge 7,38 sur 15 min, 5,7 Go/8), le démon Docker a cessé de répondre et les
DEUX jobs sont morts à la même seconde — 19:49:27, runs #152 et #154,
`context deadline exceeded` sur docker.sock. pi1 porte l'apiserver et
l'ingress. Même famille que 2026-07-23 et 2026-08-15.

Le parallélisme tombe à 1 (pi3 seul), assumé : le runner du Mac (label
`laptop`) absorbe les jobs lourds et va ~10× plus vite — mesuré sur kadans,
184 s contre 1 065 s sur pi3 et 1 915 s sur pi1.

2. Résolution de la forge figée (03_cicd.yml, group_vars/all/gitea.yml)

Les hôtes à runner résolvent `.lab` via un Pi-hole, puis retombent sur les
nameservers IPv6 du routeur, qui répondent NXDOMAIN — réponse valide, donc
retenue. D'où des pulls qui échouent par intermittence pendant que
`getent hosts` réussit. `extra_hosts` + `--add-host` court-circuitent le DNS.

L'IP est dérivée de l'inventaire (premier hôte du groupe `gitea`) plutôt
qu'écrite en dur. Vérifié que c'est équivalent : le registre répond 401 sur
/v2/ via .202 comme via .201, le svclb Traefik écoutant sur 443 de chaque nœud.

⚠ Ça ne couvre PAS le namespace réseau de buildkit, qui n'hérite d'aucun des
deux — corrigé séparément côté cms (pull côté démon, build sans `--pull`).

3. Cache Redis du bouncer coupé (roles/crowdsec)

`redisCacheUnreachableBlock: false` n'est pas honoré par le plugin : redis-0
étant 0/1, TOUTES les requêtes externes prenaient un 403 alors que
`cscli decisions list` était vide. Logs Traefik : `isBanned:false
redis:unreachable` → 403. Le cache mémoire suffit — en mode `stream` les
décisions viennent de la LAPI de toute façon.

⚠ NE PAS « réparer » Redis en `--save ""` : il sert aussi kadans-jobs (DB 1,
file de jobs) et telegram-gateway (DB 0, sessions d'auth 24 h).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-08-26 22:55:38 +02:00
co-authored by Claude Opus 5
parent 2e8b7c63d8
commit 93e7abfc57
3 changed files with 88 additions and 6 deletions
@@ -86,3 +86,37 @@ gitlab_personal_namespace_id: ~
# qu'ils n'annoncent pas. # qu'ils n'annoncent pas.
gitea_runner_image: "gitea/runner" gitea_runner_image: "gitea/runner"
gitea_runner_version: "2.3.0" gitea_runner_version: "2.3.0"
# ══════════════════════════════════════════════════════════════════════════
# RÉSOLUTION DE LA FORGE DEPUIS LA CI — FIGÉE, ET C'EST LE POINT.
#
# `gitea.arcodange.lab` est servi par l'ingress k3s (Traefik) sur pi1. Les
# hôtes à runner (pi1, pi3) le résolvent via un Pi-hole qui tourne... sur pi1
# et pi3. Leur /etc/resolv.conf liste ensuite les nameservers IPv6 du routeur,
# qui ignorent `.lab` et répondent NXDOMAIN — une réponse VALIDE, donc retenue.
#
# Résultat mesuré le 2026-08-26 (run #147, job 7373) : le pull de l'image de
# base a échoué en 16 s sur
# failed to fetch oauth token: dial tcp: lookup gitea.arcodange.lab
# on 127.0.0.11:53: no such host
# alors que `getent hosts` réussissait sur les deux hôtes au même moment. La
# panne est INTERMITTENTE : elle dépend de quel nameserver du chaînage répond
# en premier. Un `nslookup` qui réussit ne prouve donc rien.
#
# La CI de la forge ne peut pas dépendre d'un résolveur qui vacille : on fige
# la correspondance nom → IP dans les conteneurs (extra_hosts / --add-host,
# cf. playbooks/03_cicd.yml), ce qui court-circuite le DNS entièrement.
#
# L'IP est DÉRIVÉE de l'inventaire — premier hôte du groupe `gitea` — plutôt
# qu'écrite en dur : déplacer Gitea dans l'inventaire suffit alors à corriger
# la CI, sans qu'on ait à se souvenir de cette variable.
#
# Vérifié le 2026-08-26 que ce choix est valide : le registre répond
# identiquement sur les deux adresses (401 sur /v2/, réponse normale d'un
# registre non authentifié), le svclb Traefik écoutant sur 443 de chaque nœud.
# --resolve gitea.arcodange.lab:443:192.168.1.202 → 401 (pi2, groupe gitea)
# --resolve gitea.arcodange.lab:443:192.168.1.201 → 401 (pi1, ingress k3s)
# Passer par pi2 raccourcit d'ailleurs le chemin : on vise l'hôte de Gitea
# lui-même plutôt qu'un rebond par l'ingress.
gitea_lab_fqdn: "gitea.arcodange.lab"
gitea_lab_ingress_ip: "{{ hostvars[groups['gitea'][0]].preferred_ip }}"
@@ -1,7 +1,20 @@
--- ---
- name: Deploy Gitea Action - name: Deploy Gitea Action
hosts: raspberries:&local:!gitea # do not deploy on machine with gitea instance # `!gitea` : ne pas déployer sur la machine qui héberge Gitea (pi2).
# `!pi1` : NI sur le control-plane k3s. Ajouté le 2026-08-26 après que deux
# builds lourds simultanés — un par runner, `capacity: 1` ne borne
# que CHAQUE runner, pas le cluster — aient saturé pi1 : charge 7,38
# sur 15 min, 5,7 Go/8 utilisés, puis le démon Docker cesse de
# répondre et les DEUX jobs meurent à la même seconde (19:49:27,
# runs #152 et #154, `context deadline exceeded` sur docker.sock).
# pi1 porte l'apiserver et l'ingress : un `nuxt generate` n'a rien à
# y faire. Même famille d'incident que 2026-07-23 et 2026-08-15.
#
# Le parallélisme tombe donc à 1 (pi3 seul). C'est assumé : le runner du Mac
# (label `laptop`) absorbe les jobs lourds, et il est ~10× plus rapide —
# mesuré sur kadans : 184 s sur le Mac contre 1 065 s sur pi3 et 1 915 s sur pi1.
hosts: raspberries:&local:!gitea:!pi1
roles: roles:
- arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required - arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
@@ -49,6 +62,13 @@
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ci-node-playwright:docker://ci-node-playwright:latest GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ci-node-playwright:docker://ci-node-playwright:latest
ports: ports:
- "43707:43707" - "43707:43707"
# Résolution figée de la forge : le runner tire lui-même des
# images depuis gitea.arcodange.lab. Voir le commentaire de
# gitea_lab_ingress_ip (group_vars/all/gitea.yml) — le DNS du
# homelab répond NXDOMAIN par intermittence selon le nameserver
# du chaînage qui répond en premier.
extra_hosts:
- "{{ gitea_lab_fqdn }}:{{ gitea_lab_ingress_ip }}"
networks: networks:
- gitea_action_network - gitea_action_network
volumes: volumes:
@@ -143,7 +163,12 @@
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway). # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
# Plafonds durs : un build ne doit jamais pouvoir affamer l'hôte (incident 2026-07-23 : # Plafonds durs : un build ne doit jamais pouvoir affamer l'hôte (incident 2026-07-23 :
# nuxt generate à 3,5 Go RSS sur pi1 → load 150, ingress+API k3s morts → gitea.arcodange.lab injoignable). # nuxt generate à 3,5 Go RSS sur pi1 → load 150, ingress+API k3s morts → gitea.arcodange.lab injoignable).
options: "--memory=3g --memory-swap=3g --cpus=2 --pids-limit=512" # `--add-host` : même raison que l'`extra_hosts` du service
# ci-dessus, mais pour les conteneurs de JOB, qui sont ceux
# qui lancent `docker build --pull` contre la forge.
options: >-
--memory=3g --memory-swap=3g --cpus=2 --pids-limit=512
--add-host={{ gitea_lab_fqdn }}:{{ gitea_lab_ingress_ip }}
# The parent directory of a job's working directory. # The parent directory of a job's working directory.
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically. # NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
# If the path starts with '/', the '/' will be trimmed. # If the path starts with '/', the '/' will be trimmed.
@@ -142,10 +142,33 @@
captchaSiteKey: "{{ crowdsec_captcha_secret.resources[0].data.sitekey | b64decode }}" captchaSiteKey: "{{ crowdsec_captcha_secret.resources[0].data.sitekey | b64decode }}"
captchaSecretKey: "{{ crowdsec_captcha_secret.resources[0].data.secret | b64decode }}" captchaSecretKey: "{{ crowdsec_captcha_secret.resources[0].data.secret | b64decode }}"
captchaHTMLFilePath: "/data/captcha.html" captchaHTMLFilePath: "/data/captcha.html"
redisCacheEnabled: true # ⚠ CACHE REDIS DÉSACTIVÉ — le bouncer retombe sur son cache mémoire.
redisCacheHost: "redis.tools:6379" #
redisCacheDatabase: "0" # `redisCacheUnreachableBlock: false` est censé dire « ne bloque pas si
redisCacheUnreachableBlock: false # Redis est injoignable ». Le plugin ne l'honore PAS : mesuré le
# 2026-08-26, redis-0 étant 0/1, TOUTES les requêtes externes prenaient
# un 403 alors que `cscli decisions list` ne montrait aucune décision
# active. Les logs Traefik sont sans ambiguïté :
# ServeHTTP:Get ip:<ip> isBanned:false redis:unreachable
# ERROR ServeHTTP:Get ip:<ip> redis:unreachable → 403
# `isBanned:false` ET 403 : c'est l'indisponibilité du cache qui bloque,
# pas une décision.
#
# Redis stalle parce qu'il tourne avec `--save 60 1` sur un volume
# Longhorn (réplication réseau) monté sur un Pi : le BGSAVE bloque le
# process assez longtemps pour que `redis-cli ping` dépasse les 5 s de
# timeout, six fois de suite → le kubelet le tue. 41 échecs de liveness
# sur 11 jours.
#
# Le cache mémoire suffit ici : en mode `stream`, le bouncer récupère de
# toute façon les décisions de la LAPI périodiquement, et un redémarrage
# de Traefik les repeuple. On échange un cache partagé contre le fait de
# ne plus faire dépendre TOUT l'ingress de la santé d'un Redis.
#
# ⚠ NE PAS « réparer » ça en passant Redis en `--save ""` : il sert AUSSI
# kadans-jobs (DB 1, file de jobs) et telegram-gateway (DB 0, sessions
# d'auth, TTL 24 h). Supprimer sa persistance les impacterait tous deux.
redisCacheEnabled: false
- name: Supprimer les pods crowdsec en état Error pour forcer leur redémarrage - name: Supprimer les pods crowdsec en état Error pour forcer leur redémarrage
ansible.builtin.shell: | ansible.builtin.shell: |