Table of Contents
🔍 Introduction
Dans un environnement DevOps moderne, la supervision des services est essentielle pour garantir leur disponibilité et leurs performances. Uptime Kuma, une alternative open-source à Uptime Robot, permet de surveiller les services web et d’envoyer des alertes en cas de panne.
Dans cet article, nous allons voir comment :
✅ Superviser automatiquement les vhosts Apache et les Ingress Kubernetes
✅ Vérifier la disponibilité des backends définis en ProxyPass
✅ Automatiser l’ajout des services dans Uptime Kuma
✅ Configurer des alertes via Teams et Discord
✅ Optimiser l’authentification sur Ingress NGINX
💡 Public cible : DevOps, SysAdmins, SRE et toute personne gérant un environnement Kubernetes avec des services web.
🛠️ Architecture de la solution
Notre solution repose sur les éléments suivants :
- Apache : Héberge des vhosts et agit en tant que reverse proxy
- Kubernetes : Orchestre les services et expose des Ingress
- Uptime Kuma : Supervise les services et génère des alertes
- Webhook Teams & Discord : Notifie en cas de problème
- CronJob Kubernetes : Automatise la mise à jour des moniteurs
🔽 Vue d’ensemble du workflow :
1️⃣ Apache expose des vhosts avec ProxyPass vers des backends
2️⃣ Un script Python sur chaque RP analyse la config et envoie les vhosts valides à une API interne
3️⃣ Une API Flask en Kubernetes reçoit et enregistre les vhosts dans Uptime Kuma
4️⃣ Un CronJob Kubernetes détecte les nouvelles URLs Ingress et met à jour Uptime Kuma
5️⃣ Teams & Discord envoient des alertes en cas de panne
📌 Détection automatique des vhosts Apache
Nous utilisons un script Python exécuté périodiquement via Cron pour récupérer automatiquement les vhosts actifs sur Apache.
Ce script:
- Extrait les vhosts via
apachectl -S
- Filtre les vhosts non pertinents (port autre que 443, configurations par défaut, vhosts de développement)
- Vérifie l’accessibilité du backend défini par
ProxyPass
- Envoie la liste des vhosts valides vers l’API Kubernetes Uptime Kuma
📜 Script send_vhosts.py sur les reverse proxies
Ce script analyse la configuration Apache et détecte les vhosts utilisant ProxyPass.
import os
import requests
import subprocess
import re
# Récupérer les identifiants d'authentification Basic depuis les variables d'environnement
API_AUTH_USERNAME = os.getenv("API_AUTH_USERNAME", "uptime")
API_AUTH_PASSWORD = os.getenv("API_AUTH_PASSWORD", "password")
# 🌍 Adresses des serveurs API dans Kubernetes
API_URLS = [
"http://uptime-kuma-api.test.kube.test.fr/api/vhosts",
"http://uptime-kuma-api.prod.kube.test.fr/api/vhosts"
]
# 📡 Récupérer les vhosts Apache et leur fichier de config via apachectl -S
try:
result = subprocess.run("apachectl -S", shell=True, capture_output=True, text=True)
apachectl_output = result.stdout.strip().split("\n")
except Exception as e:
print(f"❌ Erreur lors de l'exécution d'apachectl -S : {e}")
exit(1)
vhosts = []
vhost_config_files = {} # mapping vhost -> fichier de config
# Capture également le port et ignorer :
# - Les vhosts en port 80
# - Les configurations issues de 000-default.conf
# - Les vhosts contenant "dev-www"
vhost_line_regex = re.compile(r'\s*port\s+(\d+)\s+namevhost\s+(\S+)\s+\(([^:]+):\d+\)')
for line in apachectl_output:
if "namevhost" in line:
m = vhost_line_regex.search(line)
if m:
port, vhost, config_file = m.groups()
if port != "443":
continue
if "000-default.conf" in config_file:
continue
if "dev-www" in vhost:
continue
if vhost.endswith(".test.fr"):
vhosts.append(vhost)
if vhost not in vhost_config_files:
vhost_config_files[vhost] = config_file
if not vhosts:
print("❌ Aucun vhost en '*.test.fr' trouvé sur le port 443 (excluant 000-default et les dev-www).")
exit(0)
# Dictionnaire pour indiquer si le vhost utilise une configuration de type Directory/DocumentRoot
vhost_has_directory = {}
# 🔍 Pour chaque vhost, ouvrir son fichier de config et extraire :
# - Le backend défini dans la directive ProxyPass (s'il existe)
# - La directive ProxyPreserveHost
# - Une éventuelle règle RewriteRule pour "^/$"
# - Et détecter la présence d'une directive DocumentRoot ou d'un bloc <Directory>
vhost_to_backend = {}
vhost_to_preserve_host = {}
vhost_to_rewrite_target = {}
for vhost in vhosts:
config_file = vhost_config_files.get(vhost)
if not config_file:
continue
try:
with open(config_file, 'r') as f:
config_content = f.read()
except Exception as e:
print(f"❌ Erreur lors de la lecture du fichier de config {config_file} pour {vhost} : {e}")
continue
# Retirer les blocs <Location ...>...</Location>
config_no_location = re.sub(r'<Location\b[^>]*>.*?</Location>', '', config_content, flags=re.DOTALL)
# Retirer les lignes commentées
config_no_comments = "\n".join(
line for line in config_no_location.splitlines() if not line.strip().startswith("#")
)
# Détecter la présence d'une directive DocumentRoot ou d'un bloc <Directory>
has_directory = bool(re.search(r'DocumentRoot\s+', config_no_comments, flags=re.IGNORECASE)) \
or bool(re.search(r'<Directory\b', config_no_comments, flags=re.IGNORECASE))
vhost_has_directory[vhost] = has_directory
# Vérifier si ProxyPreserveHost est activé
preserve = re.search(r'ProxyPreserveHost\s+On', config_no_comments, flags=re.IGNORECASE)
vhost_to_preserve_host[vhost] = bool(preserve)
# Extraire la directive ProxyPass (ou ProxyPassMatch) pour le chemin "/"
pp_match = re.search(
r'ProxyPass(?:Match)?\s+["\']?(/)["\']?\s+["\']?(\S+)["\']?',
config_no_comments,
flags=re.IGNORECASE
)
if pp_match:
local_path, backend = pp_match.groups()
if local_path.strip() == "/":
vhost_to_backend[vhost] = backend.strip()
else:
if vhost not in vhost_to_backend:
vhost_to_backend[vhost] = backend.strip()
# Rechercher une RewriteRule pour le chemin racine "^/$"
rewrite_match = re.search(
r'RewriteRule\s+["\']?\^/\$["\']?\s+["\']?([^"\s]+)["\']?',
config_no_comments,
flags=re.IGNORECASE
)
if rewrite_match:
rewrite_target = rewrite_match.group(1).strip()
vhost_to_rewrite_target[vhost] = rewrite_target
# 🔍 Tester le backend pour chaque vhost
valid_vhosts = []
for vhost in vhosts:
backend_url = vhost_to_backend.get(vhost)
if backend_url:
# Si une RewriteRule pour le root est définie, ajuster l'URL de test
rewrite_target = vhost_to_rewrite_target.get(vhost)
final_url = backend_url
if rewrite_target and rewrite_target.startswith("/"):
final_url = backend_url.rstrip("/") + rewrite_target
print(f"🔄 Test de {vhost} → {final_url}")
preserve_host = vhost_to_preserve_host.get(vhost, False)
host_header = f"-H 'Host: {vhost}'" if preserve_host else ""
curl_command = f"curl -k -s -o /dev/null -w '%{{http_code}}' {host_header} {final_url} --max-time 10"
try:
curl_result = subprocess.run(curl_command, shell=True, capture_output=True, text=True, timeout=15)
status_code_str = curl_result.stdout.strip()
try:
status_code = int(status_code_str)
except ValueError:
print(f"❌ {vhost} a renvoyé une réponse invalide : {status_code_str}")
continue
# Pour les vhosts avec ProxyPass, considérer valide si HTTP <400 et != 0
if status_code < 400 and status_code != 0:
valid_vhosts.append(vhost)
print(f"✅ {vhost} est valide (Backend {final_url} répond HTTP {status_code})")
else:
print(f"❌ {vhost} est invalide (Backend {final_url} a renvoyé HTTP {status_code})")
except subprocess.TimeoutExpired:
print(f"❌ Timeout lors du test du backend {final_url} pour {vhost}")
except Exception as e:
print(f"❌ Erreur lors du test du backend {final_url} pour {vhost} : {e}")
elif vhost_has_directory.get(vhost):
# Aucun ProxyPass, mais présence d'un Directory/DocumentRoot → ne pas tester et considérer valide par défaut
valid_vhosts.append(vhost)
print(f"✅ {vhost} est validé par défaut (Directory configuration)")
else:
print(f"❌ {vhost} n'a pas de ProxyPass configuré et pas de Directory défini.")
if not valid_vhosts:
print("❌ Aucun vhost valide trouvé avec ProxyPass ou Directory configuré.")
exit(0)
# 📡 Envoyer les vhosts validés aux API de test et prod avec authentification Basic
for api_url in API_URLS:
try:
response = requests.post(
api_url,
json={"vhosts": valid_vhosts},
auth=(API_AUTH_USERNAME, API_AUTH_PASSWORD)
)
if response.status_code == 200:
print(f"✅ Vhosts envoyés avec succès à {api_url} :", valid_vhosts)
else:
print(f"❌ Erreur API ({response.status_code}) sur {api_url} : {response.text}")
except Exception as e:
print(f"❌ Erreur lors de l'envoi des vhosts à {api_url} : {e}")
Ce script est lancé périodiquement grâce à une entrée dans /etc/cron.d/uptime-kuma :
0 * * * * root /usr/bin/python3 /usr/local/bin/send_vhosts.py
📌 API flask pour la gestion des vhosts
Nous utilisons Flask pour exposer une API REST qui reçoit les vhosts valides et les enregistre dans Uptime Kuma.
apiVersion: v1
kind: ConfigMap
metadata:
name: uptime-kuma-vhost-api-script
namespace: monitoring
data:
server.py: |
import os
import json
from flask import Flask, request, jsonify
from uptime_kuma_api import UptimeKumaApi, MonitorType
app = Flask(__name__)
# 📡 Configuration Uptime Kuma
KUMA_URL = os.getenv("KUMA_URL", "http://uptime-kuma.monitoring.svc.cluster.local")
KUMA_USER = os.getenv("KUMA_USER", "admin")
KUMA_PASSWORD = os.getenv("KUMA_PASSWORD", "password")
# 🔗 Connexion à Uptime Kuma
api = UptimeKumaApi(KUMA_URL)
api.login(KUMA_USER, KUMA_PASSWORD)
@app.route("/api/vhosts", methods=["POST"])
def receive_vhosts():
data = request.json
if not data or "vhosts" not in data:
return jsonify({"error": "Données invalides"}), 400
vhosts = data["vhosts"]
print(f"📡 Reçu {len(vhosts)} vhosts :", vhosts)
# 🔎 Récupérer les moniteurs existants
existing_monitors = {monitor['url'] for monitor in api.get_monitors() if 'url' in monitor}
# ➕ Ajouter les nouveaux moniteurs sans doublon
added = []
# Dédupliquer la liste de vhosts pour éviter les répétitions dans la même requête
unique_vhosts = set(vhosts)
for vhost in unique_vhosts:
full_url = f"https://{vhost}"
if full_url not in existing_monitors:
api.add_monitor(
type=MonitorType.HTTP,
name=vhost,
url=full_url,
interval=120,
expiryNotification=True,
maxretries=2,
notificationIDList=[1]
)
# Mise à jour de l'ensemble pour éviter d'ajouter plusieurs fois le même monitor
existing_monitors.add(full_url)
added.append(vhost)
return jsonify({"message": f"{len(added)} nouveaux moniteurs ajoutés.", "added": added}), 200
@app.route("/", methods=["GET"])
@app.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
📌 Automatisation avec un CronJob Kubernetes
Un CronJob détecte les Ingress exposés et les ajoute dans Uptime Kuma.
uptime-kuma-cronjob.yaml :
apiVersion: v1
kind: ConfigMap
metadata:
name: uptime-kuma-auto-monitor-script
namespace: monitoring
data:
auto_add_monitors.py: |
import os
from kubernetes import client, config
from uptime_kuma_api import UptimeKumaApi, MonitorType
# 🔐 Charger la configuration interne Kubernetes (TLS interne)
config.load_incluster_config()
# 🌐 Initialiser l'API Kubernetes
k8s_api = client.NetworkingV1Api()
# 📡 Configuration Uptime Kuma
KUMA_URL = os.getenv("KUMA_URL", "http://uptime-kuma.monitoring.svc.cluster.local")
KUMA_USER = os.getenv("KUMA_USER", "admin")
KUMA_PASSWORD = os.getenv("KUMA_PASSWORD", "password")
# 🔗 Se connecter à l'API Uptime Kuma
api = UptimeKumaApi(KUMA_URL)
api.login(KUMA_USER, KUMA_PASSWORD)
# 🏗️ Récupérer les URLs des Ingress
print("📡 Récupération des Ingress Kubernetes...")
ingresses = k8s_api.list_ingress_for_all_namespaces()
urls = []
for ingress in ingresses.items:
if ingress.spec and ingress.spec.rules:
for rule in ingress.spec.rules:
if rule.host and "*" not in rule.host:
urls.append(rule.host)
# 🚨 Vérifier s'il y a des URLs avant de continuer
if not urls:
print("⚠️ Aucune URL valide trouvée. Rien à ajouter dans Uptime Kuma.")
exit(0) # Arrêter le script proprement
print(f"🔍 {len(urls)} URLs détectées : {urls}")
# 🔎 Récupérer les moniteurs existants dans Uptime Kuma et stocker leurs URLs
existing_monitors = {monitor['url'] for monitor in api.get_monitors() if 'url' in monitor}
# ➕ Ajouter les nouveaux moniteurs uniquement s'ils n'existent pas déjà
for url in urls:
full_url = f"https://{url}"
if full_url not in existing_monitors:
api.add_monitor(
type=MonitorType.HTTP,
name=url,
url=full_url,
interval=120,
expiryNotification=True,
maxretries=2,
notificationIDList=[1]
)
print(f"✅ Moniteur ajouté pour {url}")
else:
print(f"🔍 Moniteur déjà existant pour {url}, pas d'ajout.")
print("🚀 Mise à jour terminée !")
---
apiVersion: v1
kind: Secret
metadata:
name: uptime-kuma-api-credentials
namespace: monitoring
type: Opaque
data:
username: XXXXXXXXXX
password: XXXXXXXXXX
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: uptime-kuma-monitor
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: uptime-kuma-monitor-role
namespace: monitoring
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: uptime-kuma-monitor-binding
namespace: monitoring
subjects:
- kind: ServiceAccount
name: uptime-kuma-monitor
namespace: monitoring
roleRef:
kind: ClusterRole
name: uptime-kuma-monitor-role
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: uptime-kuma-auto-monitor
namespace: monitoring
spec:
schedule: "*/60 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: uptime-kuma-auto-monitor
image: python:3.9
command:
- /bin/sh
- "-c"
- "pip install uptime-kuma-api && pip install kubernetes && python /scripts/auto_add_monitors.py"
volumeMounts:
- name: script-volume
mountPath: /scripts
env:
- name: KUMA_URL
value: "http://uptime-kuma.monitoring.svc.cluster.local"
- name: KUMA_USER
valueFrom:
secretKeyRef:
name: uptime-kuma-api-credentials
key: username
- name: KUMA_PASSWORD
valueFrom:
secretKeyRef:
name: uptime-kuma-api-credentials
key: password
serviceAccountName: uptime-kuma-monitor
restartPolicy: OnFailure
volumes:
- name: script-volume
configMap:
name: uptime-kuma-auto-monitor-script
defaultMode: 0777
Configuration ArgoCD et Kubernetes
Stockage et base de données
Uptime Kuma utilise MariaDB pour stocker ses données, avec une configuration spécifique Kubernetes :
apiVersion: v1
kind: Secret
metadata:
name: uptime-kuma-mariadb-secret
namespace: monitoring
type: Opaque
data:
MARIADB_ROOT_PASSWORD: XXXXXXXXX
MARIADB_USER: XXXXXXXXXXXXX
MARIADB_PASSWORD: XXXXXXXXXX
---
apiVersion: v1
kind: ConfigMap
metadata:
name: uptime-kuma-mariadb-config
namespace: monitoring
data:
my.cnf: |
[mysqld]
skip-host-cache
skip-name-resolve
max-connections=1000
!includedir /etc/mysql/mariadb.conf.d/
!includedir /etc/mysql/conf.d/
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: uptime-kuma-mariadb
namespace: monitoring
labels:
app.kubernetes.io/name: uptime-kuma-mariadb
app.kubernetes.io/part-of: uptime-kuma
app.kubernetes.io/component: database
spec:
serviceName: uptime-kuma-mariadb
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: uptime-kuma-mariadb
app.kubernetes.io/part-of: uptime-kuma
app.kubernetes.io/component: database
template:
metadata:
labels:
app.kubernetes.io/name: uptime-kuma-mariadb
app.kubernetes.io/part-of: uptime-kuma
app.kubernetes.io/component: database
spec:
containers:
- name: mariadb
image: mariadb:11.2.3
ports:
- containerPort: 3306
name: mariadb
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: uptime-kuma-mariadb-secret
key: MARIADB_ROOT_PASSWORD
- name: MARIADB_DATABASE
value: uptimekuma
- name: MARIADB_USER
valueFrom:
secretKeyRef:
name: uptime-kuma-mariadb-secret
key: MARIADB_USER
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: uptime-kuma-mariadb-secret
key: MARIADB_PASSWORD
- name: TZ
value: "Pacific/Noumea"
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: config
mountPath: /etc/mysql/my.cnf
subPath: my.cnf
readOnly: true
volumes:
- name: config
configMap:
name: uptime-kuma-mariadb-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: uptime-kuma-mariadb
namespace: monitoring
labels:
app.kubernetes.io/name: uptime-kuma-mariadb
app.kubernetes.io/part-of: uptime-kuma
app.kubernetes.io/component: database
spec:
type: ClusterIP
ports:
- name: mariadb
targetPort: 3306
port: 3306
protocol: TCP
selector:
app.kubernetes.io/name: uptime-kuma-mariadb
app.kubernetes.io/part-of: uptime-kuma
app.kubernetes.io/component: database
Déploiement de l’API Flask
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: uptime-kuma-vhost-api
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: uptime-kuma-vhost-api
template:
metadata:
labels:
app: uptime-kuma-vhost-api
spec:
containers:
- name: uptime-kuma-vhost-api
image: python:3.9
command: ["/bin/sh", "-c", "pip install flask uptime-kuma-api && python /app/server.py"]
volumeMounts:
- name: uptime-kuma-script-volume
mountPath: /app/server.py
subPath: server.py
env:
- name: KUMA_URL
value: "http://uptime-kuma.monitoring.svc.cluster.local"
- name: KUMA_USER
valueFrom:
secretKeyRef:
name: uptime-kuma-api-credentials
key: username
- name: KUMA_PASSWORD
valueFrom:
secretKeyRef:
name: uptime-kuma-api-credentials
key: password
ports:
- containerPort: 5000
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 3
periodSeconds: 5
volumes:
- name: uptime-kuma-script-volume
configMap:
name: uptime-kuma-vhost-api-script
---
apiVersion: v1
kind: Service
metadata:
name: uptime-kuma-vhost-api
namespace: monitoring
spec:
selector:
app: uptime-kuma-vhost-api
ports:
- protocol: TCP
port: 80
targetPort: 5000
---
apiVersion: v1
kind: Secret
metadata:
name: uptime-kuma-api-basic-auth
namespace: monitoring
type: Opaque
data:
# La valeur de "auth" doit être en base64. Pour obtenir cette valeur, exécutez :
# htpasswd -nb uptime password | base64
auth: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: uptime-kuma-vhost-api
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/auth-type: "basic"
nginx.ingress.kubernetes.io/auth-secret: "uptime-kuma-api-basic-auth"
nginx.ingress.kubernetes.io/auth-realm: "Accès à uptime-kuma-vhost-api"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: uptime-kuma-api.prod.kube.test.fr
http:
paths:
- path: /api/vhosts
pathType: Prefix
backend:
service:
name: uptime-kuma-vhost-api
port:
number: 80
tls:
- hosts:
- uptime-kuma-api.prod.kube.test.fr
secretName: uptime-kuma-api-tls
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: uptime-kuma-vhost-api-unprotected
namespace: monitoring
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: uptime-kuma-api.prod.kube.test.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: uptime-kuma-vhost-api
port:
number: 80
- path: /health
pathType: Prefix
backend:
service:
name: uptime-kuma-vhost-api
port:
number: 80
tls:
- hosts:
- uptime-kuma-api.prod.kube.test.fr
secretName: uptime-kuma-api-tls
---
Déploiement d’Uptime Kuma v2
Le déploiement d’Uptime Kuma suit un schéma Kubernetes classique :
apiVersion: apps/v1
kind: Deployment
metadata:
name: uptime-kuma
namespace: monitoring
labels:
app: uptime-kuma
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3001"
prometheus.io/path: "/metrics"
spec:
replicas: 1
selector:
matchLabels:
app: uptime-kuma
template:
metadata:
labels:
app: uptime-kuma
spec:
containers:
- name: uptime-kuma
image: louislam/uptime-kuma:beta
ports:
- containerPort: 3001
name: http
volumeMounts:
- name: data
mountPath: /app/data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2048Mi"
cpu: "500m"
env:
- name: DATABASE_URL
value: "mysql://$(MARIADB_USER):$(MARIADB_PASSWORD)@uptime-kuma-mariadb.monitoring.svc.cluster.local:3306/uptimekuma"
envFrom:
- secretRef:
name: uptime-kuma-mariadb-secret
livenessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
volumes:
- name: data
persistentVolumeClaim:
claimName: uptime-kuma-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: uptime-kuma-data
namespace: monitoring
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: Service
metadata:
name: uptime-kuma
namespace: monitoring
labels:
app: uptime-kuma
spec:
ports:
- port: 80
targetPort: 3001
protocol: TCP
name: http
selector:
app: uptime-kuma
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: uptime-kuma
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: uptime.prod.kube.test.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: uptime-kuma
port:
number: 3001
tls:
- hosts:
- uptime.prod.kube.test.fr
secretName: uptime-kuma-tls
---
📌 Exemple de configuration de l’authentification sur Ingress NGINX
Nous protégeons l’API avec Basic Auth, sauf pour /
et /health
.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: uptime-kuma-vhost-api
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/auth-type: "basic"
nginx.ingress.kubernetes.io/auth-secret: "uptime-kuma-api-basic-auth"
nginx.ingress.kubernetes.io/auth-realm: "Accès à uptime-kuma-vhost-api"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: uptime-kuma-api.prod.kube.test.fr
http:
paths:
- path: /api/vhosts
pathType: Prefix
backend:
service:
name: uptime-kuma-vhost-api
port:
number: 80
tls:
- hosts:
- uptime-kuma-api.prod.kube.test.fr
secretName: uptime-kuma-api-tls
📌 Envoi d’alertes vers Teams & Discord
Les moniteurs dans Uptime Kuma sont configurés pour envoyer des alertes en cas de panne.
🚀 Ajout du Webhook Teams
Dans Uptime Kuma :
- Aller dans Paramètres > Notifications
- Ajouter Microsoft Teams avec un webhook
https://outlook.office.com/webhook/...
🚀 Ajout du Webhook Discord
- Aller dans Paramètres > Notifications
- Ajouter Discord avec un webhook
https://discord.com/api/webhooks/...
🎯 Conclusion
Avec cette solution :
✅ Les vhosts Apache et les Ingress Kubernetes sont surveillés automatiquement
✅ Les backends sont testés avant ajout dans Uptime Kuma
✅ Les alertes sont envoyées en temps réel sur Teams & Discord
✅ L’authentification est optimisée sur Ingress NGINX