fix(traefik,crowdsec) — plugin bouncer lu sur disque : un hoquet de 10 s coupait tout l'ingress #62
@@ -18,8 +18,22 @@
|
||||
|
||||
- name: how to reach k3s
|
||||
hosts: server
|
||||
|
||||
vars:
|
||||
# Plugin bouncer CrowdSec de Traefik. Ces trois variables sont la SEULE
|
||||
# source de vérité : elles pilotent à la fois ce que la tâche
|
||||
# « vendorer le plugin CrowdSec » télécharge et ce que Traefik va lire sur
|
||||
# disque (cf. experimental.localPlugins plus bas). Monter de version = changer
|
||||
# `traefik_crowdsec_plugin_version` puis rejouer `--tags traefik-plugins`.
|
||||
traefik_crowdsec_plugin_module: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
traefik_crowdsec_plugin_version: v1.3.3
|
||||
# Sous-dossier du PVC monté sur /plugins-local/src/<module>. Un PVC par
|
||||
# plugin serait du gaspillage : on range chaque plugin dans son sous-dossier.
|
||||
traefik_crowdsec_plugin_subpath: crowdsec-bouncer
|
||||
traefik_plugins_pvc_name: traefik-plugins-local
|
||||
|
||||
tasks:
|
||||
|
||||
|
||||
# Tag `longhorn` : rejouer CETTE tâche seule (`--tags longhorn`).
|
||||
# ⚠ Sans tag, ce playbook exécute aussi « redeploy traefik » sur localhost,
|
||||
# qui SUPPRIME le Deployment traefik dans le contexte kubectl COURANT —
|
||||
@@ -64,6 +78,173 @@
|
||||
vars:
|
||||
longhorn_helm_values: {} # https://github.com/longhorn/longhorn/blob/master/chart/values.yaml
|
||||
|
||||
# Tag `traefik-plugins` : rejouer CES tâches seules (`--tags traefik-plugins`),
|
||||
# typiquement après avoir monté `traefik_crowdsec_plugin_version`.
|
||||
# ⚠ Ces tâches DOIVENT rester avant l'écriture du manifeste Traefik : avec
|
||||
# `abortOnPluginFailure: true`, un Traefik qui redémarre sur un PVC vide refuse
|
||||
# de démarrer. Peupler d'abord, reconfigurer ensuite.
|
||||
- name: vendorer le plugin CrowdSec sur un PVC
|
||||
tags: [traefik-plugins]
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
block:
|
||||
- name: Créer le PVC des sources de plugins Traefik
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: "{{ traefik_plugins_pvc_name }}"
|
||||
namespace: kube-system
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
# Les sources du bouncer font 574 Ko décompressées (147 fichiers,
|
||||
# vendor/ inclus). 128 Mi laisse de la place pour un second plugin
|
||||
# sans avoir à retoucher au PVC.
|
||||
storage: 128Mi
|
||||
|
||||
- name: Lancer le Job qui pose les sources du plugin
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: vendor-traefik-plugins
|
||||
namespace: kube-system
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
# Même nœud que Traefik : le PVC est RWO, donc attaché à UN nœud.
|
||||
# Plusieurs pods d'un même nœud peuvent le monter, ce qui permet
|
||||
# de repeupler sans couper l'ingress — Traefik ne lit ses plugins
|
||||
# qu'au démarrage, écrire pendant qu'il tourne est sans effet
|
||||
# jusqu'à son prochain redémarrage.
|
||||
nodeSelector:
|
||||
node-role.kubernetes.io/control-plane: 'true'
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
volumes:
|
||||
- name: plugins
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ traefik_plugins_pvc_name }}"
|
||||
containers:
|
||||
- name: vendor
|
||||
image: alpine:3.20
|
||||
env:
|
||||
- name: MODULE
|
||||
value: "{{ traefik_crowdsec_plugin_module }}"
|
||||
- name: VERSION
|
||||
value: "{{ traefik_crowdsec_plugin_version }}"
|
||||
- name: SUBPATH
|
||||
value: "{{ traefik_crowdsec_plugin_subpath }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- -eu
|
||||
- -c
|
||||
- |
|
||||
DEST="/plugins/$SUBPATH"
|
||||
|
||||
# Idempotence : on ne retélécharge pas si la version posée
|
||||
# est déjà la bonne ET que le manifeste est là (un PVC
|
||||
# à moitié écrit ne doit pas passer pour valide).
|
||||
if [ -f "$DEST/.vendored-version" ] \
|
||||
&& [ "$(cat "$DEST/.vendored-version")" = "$VERSION" ] \
|
||||
&& [ -f "$DEST/.traefik.yml" ]; then
|
||||
echo "déjà vendoré en $VERSION, rien à faire"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
URL="https://plugins.traefik.io/public/download/$MODULE/$VERSION"
|
||||
|
||||
# Ici, et seulement ici, on dépend du réseau. Contrairement
|
||||
# au téléchargement que faisait Traefik au démarrage, on
|
||||
# peut réessayer sans que l'ingress soit en jeu.
|
||||
n=0
|
||||
until wget -q -T 30 -O "$TMP/plugin.zip" "$URL"; do
|
||||
n=$((n + 1))
|
||||
if [ "$n" -ge 5 ]; then
|
||||
echo "échec du téléchargement de $URL après 5 tentatives" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tentative $n échouée, nouvel essai dans 10 s" >&2
|
||||
sleep 10
|
||||
done
|
||||
|
||||
mkdir -p "$TMP/x"
|
||||
unzip -q "$TMP/plugin.zip" -d "$TMP/x"
|
||||
|
||||
# L'archive du registre contient un unique dossier racine
|
||||
# horodaté (maxlerebourg-...-<sha>). On ne devine pas son
|
||||
# nom : on vérifie qu'il est bien unique.
|
||||
if [ "$(find "$TMP/x" -mindepth 1 -maxdepth 1 -type d | wc -l)" -ne 1 ]; then
|
||||
echo "archive inattendue : la racine n'est pas un dossier unique" >&2
|
||||
exit 1
|
||||
fi
|
||||
SRC="$(find "$TMP/x" -mindepth 1 -maxdepth 1 -type d)"
|
||||
|
||||
# Sans manifeste, Yaegi ne chargera rien et Traefik
|
||||
# refusera de démarrer (abortOnPluginFailure). Autant
|
||||
# s'en apercevoir ici.
|
||||
if [ ! -f "$SRC/.traefik.yml" ]; then
|
||||
echo "manifeste .traefik.yml absent de l'archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$DEST"
|
||||
mkdir -p "$DEST"
|
||||
cp -a "$SRC/." "$DEST/"
|
||||
# Traefik tourne en 65532 et ne fait que lire.
|
||||
chmod -R a+rX "$DEST"
|
||||
echo "$VERSION" > "$DEST/.vendored-version"
|
||||
|
||||
echo "plugin $MODULE@$VERSION posé dans $DEST"
|
||||
ls "$DEST/.traefik.yml" "$DEST/vendor" >/dev/null
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
|
||||
- name: Attendre la fin du Job
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: batch/v1
|
||||
kind: Job
|
||||
name: vendor-traefik-plugins
|
||||
namespace: kube-system
|
||||
register: vendor_job
|
||||
until: >-
|
||||
(vendor_job.resources[0].status.succeeded | default(0) | int) > 0
|
||||
or (vendor_job.resources[0].status.failed | default(0) | int) > 0
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
- name: Échouer si le Job n'a pas abouti
|
||||
ansible.builtin.assert:
|
||||
that: (vendor_job.resources[0].status.succeeded | default(0) | int) > 0
|
||||
fail_msg: >-
|
||||
Le plugin n'a pas pu être posé sur le PVC. Ne pas reconfigurer Traefik
|
||||
en l'état : avec abortOnPluginFailure il refuserait de démarrer.
|
||||
Journal : kubectl logs -n kube-system job/vendor-traefik-plugins
|
||||
|
||||
always:
|
||||
- name: Nettoyer le Job
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
kubernetes.core.k8s:
|
||||
state: absent
|
||||
api_version: batch/v1
|
||||
kind: Job
|
||||
name: vendor-traefik-plugins
|
||||
namespace: kube-system
|
||||
|
||||
- name: customize k3s traefik configuration https://docs.k3s.io/helm
|
||||
block:
|
||||
- name: Get my public IP
|
||||
@@ -200,6 +381,15 @@
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
# Sources du plugin CrowdSec, posées sur ce PVC par la tâche
|
||||
# « vendorer le plugin CrowdSec ». Le chart EXIGE que le volume
|
||||
# référencé par un localPlugin figure ici, sinon le rendu échoue
|
||||
# (helper `traefik.volumeExistsInAdditionalVolumes`) — garde-fou
|
||||
# utile : on ne peut pas déclarer le plugin sans son volume.
|
||||
additionalVolumes:
|
||||
- name: plugins-local
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ traefik_plugins_pvc_name }}"
|
||||
# default is https://github.com/traefik/traefik-helm-chart/blob/v25.0.0/traefik/values.yaml <- for v25 (`kubectl describe deployments.apps traefik -n kube-system | grep helm.sh/chart`)
|
||||
# current is https://github.com/traefik/traefik-helm-chart/blob/v37.4.0/traefik/values.yaml
|
||||
nodeSelector:
|
||||
@@ -260,10 +450,53 @@
|
||||
mountPath: /config
|
||||
type: configMap
|
||||
experimental:
|
||||
plugins:
|
||||
# ⚠ PLUGIN LU SUR DISQUE, PLUS TÉLÉCHARGÉ AU DÉMARRAGE.
|
||||
#
|
||||
# `experimental.plugins` (registre distant) fait passer Traefik par
|
||||
# RegistryDownloader.Download(), qui appelle plugins.traefik.io à
|
||||
# CHAQUE démarrage, avec 10 s de timeout. Avoir déjà l'archive dans
|
||||
# /plugins-storage ne dispense PAS de l'appel : le cache ne sert qu'à
|
||||
# envoyer un hash pour obtenir un 304. Persister ce cache (en faire un
|
||||
# PVC au lieu d'un emptyDir) n'aurait donc rien évité — vérifié dans
|
||||
# la source de Traefik v3.6.2, pkg/plugins/downloader.go.
|
||||
#
|
||||
# Échec mesuré le 2026-09-20 à 15:00:50 :
|
||||
# unable to install plugin crowdsec-bouncer: ... Get
|
||||
# "https://plugins.traefik.io/.../v1.3.3": context deadline exceeded
|
||||
# alors que le registre répondait en 214 ms depuis un pod de pi1 —
|
||||
# c'était un hoquet, pas une panne. Ça a suffi.
|
||||
#
|
||||
# Traefik ne s'arrête pas pour autant : il journalise « Plugins are
|
||||
# disabled because an error has occurred. » et sert quand même. Le
|
||||
# middleware `crowdsec` devient alors « invalid middleware type or
|
||||
# middleware does not exist » et Traefik JETTE tous les routeurs qui
|
||||
# le référencent — c'est presque tout l'ingress. Mesuré :
|
||||
# www.arcodange.lab et cms-rec.arcodange.lab en 404 pendant 3 h 18.
|
||||
#
|
||||
# `localPlugins` emprunte un autre chemin de code (SetupLocalPlugins),
|
||||
# qui ne fait AUCUNE I/O réseau : Yaegi lit les sources sous
|
||||
# /plugins-local/src/<module>. C'est la même famille de fragilité que
|
||||
# 2026-08-26 (résolution de la forge) : on ne fait pas dépendre l'edge
|
||||
# d'un résolveur ou d'un registre joignable à la seconde près.
|
||||
#
|
||||
# ⚠ Monter la version du plugin ne se fait plus en changeant un champ
|
||||
# que Traefik irait chercher : il faut REJOUER ce playbook
|
||||
# (`--tags traefik-plugins`) pour repeupler le PVC.
|
||||
localPlugins:
|
||||
crowdsec-bouncer:
|
||||
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin #https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin
|
||||
version: v1.3.3
|
||||
moduleName: "{{ traefik_crowdsec_plugin_module }}" #https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin
|
||||
mountPath: "/plugins-local/src/{{ traefik_crowdsec_plugin_module }}"
|
||||
type: localPath
|
||||
volumeName: plugins-local
|
||||
subPath: "{{ traefik_crowdsec_plugin_subpath }}"
|
||||
# Échouer franchement plutôt que servir sans bouncer. Sans ce drapeau,
|
||||
# un plugin qui ne charge pas dégrade l'edge EN SILENCE (c'est toute
|
||||
# l'histoire ci-dessus). Maintenant qu'il n'y a plus de réseau au boot,
|
||||
# il ne peut se déclencher que si le PVC est vide ou corrompu — et dans
|
||||
# ce cas on VEUT que Traefik refuse de démarrer plutôt qu'il ouvre
|
||||
# l'ingress sans filtrage ni captcha.
|
||||
# Requiert Traefik >= 3.3 (on est en 3.6.2).
|
||||
abortOnPluginFailure: true
|
||||
additionalArguments:
|
||||
- '--providers.file.filename=/config/dynamic.yaml'
|
||||
- '--providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik'
|
||||
|
||||
Reference in New Issue
Block a user