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
|
- name: how to reach k3s
|
||||||
hosts: server
|
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:
|
tasks:
|
||||||
|
|
||||||
# Tag `longhorn` : rejouer CETTE tâche seule (`--tags longhorn`).
|
# Tag `longhorn` : rejouer CETTE tâche seule (`--tags longhorn`).
|
||||||
# ⚠ Sans tag, ce playbook exécute aussi « redeploy traefik » sur localhost,
|
# ⚠ Sans tag, ce playbook exécute aussi « redeploy traefik » sur localhost,
|
||||||
# qui SUPPRIME le Deployment traefik dans le contexte kubectl COURANT —
|
# qui SUPPRIME le Deployment traefik dans le contexte kubectl COURANT —
|
||||||
@@ -64,6 +78,173 @@
|
|||||||
vars:
|
vars:
|
||||||
longhorn_helm_values: {} # https://github.com/longhorn/longhorn/blob/master/chart/values.yaml
|
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
|
- name: customize k3s traefik configuration https://docs.k3s.io/helm
|
||||||
block:
|
block:
|
||||||
- name: Get my public IP
|
- name: Get my public IP
|
||||||
@@ -200,6 +381,15 @@
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /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`)
|
# 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
|
# current is https://github.com/traefik/traefik-helm-chart/blob/v37.4.0/traefik/values.yaml
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
@@ -260,10 +450,53 @@
|
|||||||
mountPath: /config
|
mountPath: /config
|
||||||
type: configMap
|
type: configMap
|
||||||
experimental:
|
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:
|
crowdsec-bouncer:
|
||||||
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin #https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin
|
moduleName: "{{ traefik_crowdsec_plugin_module }}" #https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin
|
||||||
version: v1.3.3
|
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:
|
additionalArguments:
|
||||||
- '--providers.file.filename=/config/dynamic.yaml'
|
- '--providers.file.filename=/config/dynamic.yaml'
|
||||||
- '--providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik'
|
- '--providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik'
|
||||||
|
|||||||
Reference in New Issue
Block a user