From d4d98da374c0eb82dbd3807a7427830582b7b55b Mon Sep 17 00:00:00 2001 From: RomeoCavazza Date: Sun, 14 Jun 2026 16:32:43 +0200 Subject: [PATCH] feat(ops): hardening from seminar + single Make runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/{smoke,load,soft-affinity}.sh (e2e, charge HPA, toggle anti-affinity) - traefik IngressClass + PDB ; server HPA/PDB (templates app-tier) - Makefile : cycle de vie complet (provision→deploy→harden→verify→destroy + minikube) - consolidation Make-only : recettes qualité repliées, Justfile supprimé --- Justfile | 24 ---- Makefile | 170 ++++++++++++++++++++++++++ README.md | 43 ++++++- k8s/server/deployment.yaml | 9 +- k8s/server/server.hpa.yaml | 24 ++++ k8s/server/server.pdb.yaml | 14 +++ k8s/traefik/traefik.ingressclass.yaml | 11 ++ k8s/traefik/traefik.pdb.yaml | 14 +++ scripts/load.sh | 36 ++++++ scripts/smoke.sh | 54 ++++++++ scripts/soft-affinity.sh | 57 +++++++++ 11 files changed, 427 insertions(+), 29 deletions(-) delete mode 100644 Justfile create mode 100644 Makefile create mode 100644 k8s/server/server.hpa.yaml create mode 100644 k8s/server/server.pdb.yaml create mode 100644 k8s/traefik/traefik.ingressclass.yaml create mode 100644 k8s/traefik/traefik.pdb.yaml create mode 100755 scripts/load.sh create mode 100755 scripts/smoke.sh create mode 100755 scripts/soft-affinity.sh diff --git a/Justfile b/Justfile deleted file mode 100644 index f0f74a4..0000000 --- a/Justfile +++ /dev/null @@ -1,24 +0,0 @@ -set shell := ["bash", "-cu"] - -# liste les recettes -default: - @just --list - -# format : prettier (yaml/md/json) + terraform fmt -fmt: - npx --yes prettier --write "**/*.{yaml,yml,md,json}" - terraform -chdir=terraform fmt - -# format check (ce que vérifierait la CI) -fmt-check: - npx --yes prettier --check "**/*.{yaml,yml,md,json}" - terraform -chdir=terraform fmt -check - -# validation des manifests k8s + terraform (nécessite kubeconform, terraform) -validate: - terraform -chdir=terraform validate || true - find k8s -name '*.yaml' -print0 | xargs -0 -I{} sh -c 'kubeconform -strict -summary "{}" || true' - -# lint terraform (nécessite tflint) -tf-lint: - cd terraform && tflint || true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c2df5b --- /dev/null +++ b/Makefile @@ -0,0 +1,170 @@ +# OpsWarden Ops — runner unique du repo (infra). Convention projet : ops = Make, +# app/web = Just (un seul runner par repo, jamais deux empilés). +# Cycle de vie : provision -> deploy -> harden -> verify -> destroy. +# Inclut aussi la qualité (fmt / validate / lint), repliée ici depuis l'ex-Justfile. +# +# Démarrage rapide (depuis ce dossier) : +# cp .env.example .env && $EDITOR .env # poser DIGITALOCEAN_TOKEN +# nix develop # ou: export $(grep -v '^#' .env | xargs) +# make all # infra + deploy (couche prête) +# make hosts && make smoke # DNS local + smoke test +# +# Placeholder : les services applicatifs (server/client-web/investigation/worker) +# ne sont pas encore déployés. `deploy` n'applique que la couche prête +# (observability + postgres + redis + traefik) ; la couche app est commentée. + +# Recettes POSIX sh (pas de dépendance dure à /bin/bash : NixOS / images minimales). +TF_DIR := terraform +# Kubeconfig par défaut : ~/.kube/config (ce que minikube, kubectl et k9s utilisent). +# DOKS est le cas particulier : Terraform écrit ./kubeconfig, utilisé par all/infra. +KUBECONFIG ?= $(HOME)/.kube/config +export KUBECONFIG +DOKS_KUBECONFIG := $(CURDIR)/kubeconfig + +# minikube écrit son contexte dans ~/.kube/config (le défaut). 2 nœuds reflètent +# DOKS pour que l'anti-affinity required (2 replicas) marche tel quel. +MINIKUBE_NODES ?= 2 + +# Hôtes publics (placeholders : .example ne résout pas, mappé via /etc/hosts). +WEB_HOST ?= app.opswarden.example +API_HOST ?= api.opswarden.example + +# Manifests groupés par phase de déploiement. +MONITORING := k8s/observability/cadvisor.daemonset.yaml +DATA := k8s/postgres/postgres.secret.yaml k8s/postgres/postgres.configmap.yaml \ + k8s/postgres/postgres.volume.yaml k8s/postgres/postgres.deployment.yaml \ + k8s/postgres/postgres.service.yaml \ + k8s/redis/redis.configmap.yaml k8s/redis/redis.deployment.yaml \ + k8s/redis/redis.service.yaml +LB := k8s/traefik/traefik.ingressclass.yaml k8s/traefik/traefik.rbac.yaml \ + k8s/traefik/traefik.deployment.yaml k8s/traefik/traefik.service.yaml +# Couche app (placeholders) — décommenter au fil des images publiées : +# APP := k8s/server/ k8s/client-web/ k8s/investigation/ k8s/worker/ + +.DEFAULT_GOAL := help +.PHONY: help all infra kubeconfig deploy db-check hosts smoke status destroy \ + fmt fmt-check validate tf-lint \ + metrics hpa pdb load harden soft-affinity hard-affinity \ + minikube minikube-up minikube-deploy minikube-hosts minikube-smoke minikube-down + +help: ## Affiche cette aide + @echo "OpsWarden Ops — cibles make :" + @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | \ + awk 'BEGIN{FS=":.*## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' + +all: infra ## DOKS : provisionne le cluster puis déploie la couche prête + $(MAKE) deploy KUBECONFIG=$(DOKS_KUBECONFIG) + +## --- Cœur ------------------------------------------------------------------ + +infra: ## Provisionne le cluster DOKS via Terraform (écrit ./kubeconfig) + cd $(TF_DIR) && terraform init -input=false && terraform apply -auto-approve + @echo ">> Attente des nœuds Ready..." + KUBECONFIG=$(DOKS_KUBECONFIG) kubectl wait --for=condition=Ready nodes --all --timeout=300s + +kubeconfig: ## (Re)génère ./kubeconfig depuis l'état Terraform + cd $(TF_DIR) && terraform apply -auto-approve -target=local_file.kubeconfig + +deploy: ## Applique la couche prête (observability + data + traefik), dans l'ordre + kubectl apply -f $(MONITORING) + kubectl apply $(addprefix -f ,$(DATA)) + @echo ">> Attente de postgres & redis..." + kubectl rollout status deploy/postgres --timeout=180s + kubectl rollout status deploy/redis --timeout=120s + kubectl apply $(addprefix -f ,$(LB)) + kubectl -n kube-public rollout status deploy/traefik --timeout=120s + @echo ">> Couche app (server/client-web/investigation/worker) : placeholders, non déployée." + +db-check: ## Vérifie la connectivité Postgres (le schéma est géré par opswarden-server) + @POD=$$(kubectl get pods -l app=postgres -o jsonpath='{.items[0].metadata.name}'); \ + echo ">> Test connexion sur le pod $$POD"; \ + kubectl exec -i $$POD -c postgres -- sh -c 'psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -c "SELECT 1;"' + +hosts: ## Mappe l'IP d'un nœud -> hôtes web/api dans /etc/hosts (sudo) + @NODES=$$(kubectl get nodes -o jsonpath='{ $$.items[*].status.addresses[?(@.type=="ExternalIP")].address }'); \ + IP=$$(echo $$NODES | awk '{print $$1}'); \ + echo ">> $$IP -> $(WEB_HOST) $(API_HOST)"; \ + echo "$$IP $(WEB_HOST) $(API_HOST)" | sudo tee -a /etc/hosts + +smoke: ## Smoke test bout-en-bout (Traefik + routes app best-effort) + WEB_HOST=$(WEB_HOST) API_HOST=$(API_HOST) ./scripts/smoke.sh + +status: ## État du cluster (pods / services / ingress, namespaces utiles) + kubectl get pods -o wide + kubectl get svc,ingress + kubectl -n kube-public get pods,svc -o wide + kubectl -n kube-system get ds cadvisor + +destroy: ## Détruit le cluster DOKS + cd $(TF_DIR) && terraform destroy -auto-approve + +## --- Qualité (ex-Justfile, ce que vérifie la CI) --------------------------- + +fmt: ## Formate yaml/md/json (prettier) + terraform fmt + npx --yes prettier --write "**/*.{yaml,yml,md,json}" + terraform -chdir=$(TF_DIR) fmt + +fmt-check: ## Vérifie le formatage sans rien modifier (miroir CI) + npx --yes prettier --check "**/*.{yaml,yml,md,json}" + terraform -chdir=$(TF_DIR) fmt -check + +validate: ## Valide les manifests k8s (kubeconform) + terraform + terraform -chdir=$(TF_DIR) validate || true + find k8s -name '*.yaml' -print0 | xargs -0 -I{} sh -c 'kubeconform -strict -summary "{}" || true' + +tf-lint: ## Lint terraform (tflint) + cd $(TF_DIR) && tflint || true + +## --- Durcissement production ----------------------------------------------- + +metrics: ## Assure la présence de metrics-server (les clusters managés l'ont souvent) + @kubectl top nodes >/dev/null 2>&1 && echo "metrics-server déjà présent" || \ + kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + +pdb: ## Applique les PodDisruptionBudgets (traefik prêt ; server si déployé) + kubectl apply -f k8s/traefik/traefik.pdb.yaml + @kubectl get deploy server >/dev/null 2>&1 \ + && kubectl apply -f k8s/server/server.pdb.yaml \ + || echo ">> deploy/server absent — k8s/server/server.pdb.yaml prêt à appliquer plus tard." + kubectl get pdb -A + +hpa: metrics ## Applique le HPA du server (sauté tant que le Deployment n'existe pas) + @kubectl get deploy server >/dev/null 2>&1 \ + && { kubectl apply -f k8s/server/server.hpa.yaml; kubectl get hpa; } \ + || echo ">> deploy/server absent — k8s/server/server.hpa.yaml prêt à appliquer plus tard." + +load: ## Génère de la charge HTTP (test autoscaling) contre l'hôte web + WEB_URL=http://$(WEB_HOST):30021 ./scripts/load.sh + +soft-affinity: ## Relâche l'anti-affinity required->preferred (clusters nodes < replicas) + ./scripts/soft-affinity.sh on + +hard-affinity: ## Restaure l'anti-affinity stricte (preferred->required) + ./scripts/soft-affinity.sh off + +harden: pdb hpa ## Applique PDB + HPA (ce qui est prêt) + +## --- Cluster local (minikube, sans DigitalOcean) --------------------------- + +minikube: minikube-up minikube-deploy ## One-shot : cluster local + couche prête + @echo ">> Fait. Ensuite : 'make minikube-smoke' pour vérifier." + +minikube-up: ## Démarre minikube (driver docker) + metrics-server + storage + minikube start --nodes $(MINIKUBE_NODES) --driver=docker \ + --addons=metrics-server,default-storageclass,storage-provisioner + +minikube-deploy: ## Déploie la couche prête sur le minikube courant + $(MAKE) deploy + +minikube-hosts: ## Mappe l'IP minikube -> hôtes web/api (/etc/hosts ; NixOS-aware) + @IP=$$(minikube ip); LINE="$$IP $(WEB_HOST) $(API_HOST)"; \ + echo "$$LINE" | sudo tee -a /etc/hosts 2>/dev/null \ + || { echo ">> /etc/hosts en lecture seule (NixOS). Ajouter dans configuration.nix :"; \ + echo " networking.extraHosts = \"$$LINE\";"; \ + echo ">> Ou vérifier sans /etc/hosts : make minikube-smoke"; } + +minikube-smoke: ## Smoke test minikube sans /etc/hosts (curl --resolve) + RESOLVE_IP=$$(minikube ip) WEB_HOST=$(WEB_HOST) API_HOST=$(API_HOST) ./scripts/smoke.sh + +minikube-down: ## Supprime le cluster minikube local + minikube delete diff --git a/README.md b/README.md index 0d63e84..d865f90 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,19 @@ cloud showcase** — it must never be a prerequisite to run or grade the product opswarden-ops/ │ ├── k8s/ -│ ├── server/ # OpsWarden server (Rust/Axum) — placeholder +│ ├── server/ # OpsWarden server (Rust/Axum) + HPA/PDB tmpl — placeholder │ ├── client-web/ # Next.js client (or Vercel) — placeholder │ ├── investigation/ # AI SRE agent (RAG/FastAPI) — placeholder │ ├── worker/ # async Redis workers — placeholder │ ├── postgres/ # PostgreSQL — ready │ ├── redis/ # Redis — ready -│ ├── traefik/ # ingress controller & LB — ready +│ ├── traefik/ # ingress controller & LB (+ IngressClass, PDB) — ready │ └── observability/ # cAdvisor (+ prom/grafana/loki) — partial │ ├── terraform/ # DOKS cluster provisioning (main/outputs/providers/variables.tf) +├── scripts/ # smoke.sh, load.sh, soft-affinity.sh (ops helpers) ├── docs/ # architecture + cluster screenshots +├── Makefile # single runner: provision → deploy → harden → verify → destroy + fmt/validate/lint ├── flake.nix / flake.lock # Nix dev shell (kubectl, terraform, k9s, helm…) ├── .env # API tokens (git-ignored) ├── LICENSE / NOTICE # Apache-2.0 @@ -136,6 +138,14 @@ screenshots will replace the application-level ones once it is deployed. ## Installation & Configuration +> **One-command path.** The whole lifecycle is automated by the +> [`Makefile`](Makefile): `make all` (provision + deploy the ready layer), +> `make hosts && make smoke` (local DNS + end-to-end check), `make harden` +> (PDB/HPA), `make destroy`. Run `make help` for every target. **No cloud +> account?** Run the same manifests for free on a local 2-node minikube: +> `make minikube`, then `make minikube-smoke`. The manual steps below spell out +> the same flow. + ### Prerequisites - [Nix](https://nixos.org/download.html) package manager @@ -185,11 +195,38 @@ kubectl apply -f k8s/traefik/ ### Teardown ```bash -cd terraform && terraform destroy +cd terraform && terraform destroy # or: make destroy ``` --- +## Production hardening + +Reusable patterns ported from the reference deployment, applied with `make harden`: + +- **Disruption budgets** — [`k8s/traefik/traefik.pdb.yaml`](k8s/traefik/traefik.pdb.yaml) + keeps ≥1 Traefik replica during node drain; [`k8s/server/server.pdb.yaml`](k8s/server/server.pdb.yaml) + is the template for the API server. +- **Autoscaling** — [`k8s/server/server.hpa.yaml`](k8s/server/server.hpa.yaml) + (CPU-based HPA template), enabled by `requests.cpu` + metrics-server + (`make metrics` / `make load`). +- **Modern ingress** — [`k8s/traefik/traefik.ingressclass.yaml`](k8s/traefik/traefik.ingressclass.yaml) + replaces the deprecated `kubernetes.io/ingress.class` annotation; app Ingresses + use `spec.ingressClassName: traefik`. +- **Strict HA** — replicated services use _required_ pod anti-affinity (one + replica per node). On clusters where `nodes < replicas`, + [`scripts/soft-affinity.sh`](scripts/soft-affinity.sh) (`make soft-affinity`) + relaxes it to _preferred_ without editing the manifests. +- **Smoke & load** — [`scripts/smoke.sh`](scripts/smoke.sh) checks the public + path (Traefik + app routes, NixOS/minikube aware) and + [`scripts/load.sh`](scripts/load.sh) drives autoscaling. + +> HPA/PDB for the app tier (`server`, `client-web`) are **templates** until those +> images are deployed — `make harden` applies what is ready (Traefik) and skips +> the rest with a notice. + +--- + ## License OpsWarden is distributed under the **Apache License 2.0**. See diff --git a/k8s/server/deployment.yaml b/k8s/server/deployment.yaml index a018400..96baad2 100644 --- a/k8s/server/deployment.yaml +++ b/k8s/server/deployment.yaml @@ -4,8 +4,13 @@ # Quand l'image existera (ghcr.io/romeocavazza/opswarden-server) et que l'app # sera stable, remplir ce dossier avec : # - deployment.yaml : Deployment (replicas, pod anti-affinity, env via -# ConfigMap/Secret, probes /health) +# ConfigMap/Secret, probes /health, resources.requests.cpu) # - service.yaml : Service ClusterIP sur :8080 -# - ingress.yaml : route Traefik (ex. api.opswarden.example) +# - ingress.yaml : route Traefik (ex. api.opswarden.example), +# spec.ingressClassName: traefik (cf. k8s/traefik/) +# +# Durcissement deja pret a appliquer (templates dans ce dossier) : +# - server.hpa.yaml : HorizontalPodAutoscaler CPU (besoin de requests.cpu) +# - server.pdb.yaml : PodDisruptionBudget (>=1 replica au drain) # # Exemple de service reel et complet : voir k8s/postgres/. diff --git a/k8s/server/server.hpa.yaml b/k8s/server/server.hpa.yaml new file mode 100644 index 0000000..d60d813 --- /dev/null +++ b/k8s/server/server.hpa.yaml @@ -0,0 +1,24 @@ +# HorizontalPodAutoscaler pour le server OpsWarden (basé CPU). +# TEMPLATE — requiert (1) le Deployment server avec resources.requests.cpu +# défini, et (2) metrics-server (DOKS le fournit ; en local `make metrics`). +# Scaler au-delà des replicas de base suppose une anti-affinity *preferred* +# (pas required) sur le Deployment server (cf. scripts/soft-affinity.sh). +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: server + namespace: default +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: server + minReplicas: 2 + maxReplicas: 6 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/k8s/server/server.pdb.yaml b/k8s/server/server.pdb.yaml new file mode 100644 index 0000000..90c11ff --- /dev/null +++ b/k8s/server/server.pdb.yaml @@ -0,0 +1,14 @@ +# PodDisruptionBudget pour le server OpsWarden (API Rust/Axum). +# TEMPLATE — le Deployment référencé (k8s/server/deployment.yaml) est un +# placeholder ; appliquer ceci une fois l'image du server déployée. Un PDB sans +# pod correspondant est inerte : l'appliquer en avance est sans danger. +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: server + namespace: default +spec: + minAvailable: 1 + selector: + matchLabels: + app: server diff --git a/k8s/traefik/traefik.ingressclass.yaml b/k8s/traefik/traefik.ingressclass.yaml new file mode 100644 index 0000000..9f613d0 --- /dev/null +++ b/k8s/traefik/traefik.ingressclass.yaml @@ -0,0 +1,11 @@ +# Remplaçant moderne de l'annotation dépréciée `kubernetes.io/ingress.class`. +# Les Ingress applicatifs (server/client-web) référencent ceci via +# `spec.ingressClassName: traefik`. Ressource cluster-scoped ; la ClusterRole +# Traefik (k8s/traefik/traefik.rbac.yaml) accorde déjà la lecture des +# `ingressclasses`. +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: traefik +spec: + controller: traefik.io/ingress-controller diff --git a/k8s/traefik/traefik.pdb.yaml b/k8s/traefik/traefik.pdb.yaml new file mode 100644 index 0000000..c73f830 --- /dev/null +++ b/k8s/traefik/traefik.pdb.yaml @@ -0,0 +1,14 @@ +# PodDisruptionBudget — garde >=1 replica Traefik en service pendant les +# perturbations volontaires (drain de nœud, rolling update, éviction). Traefik +# tourne en 2 replicas avec anti-affinity required ; ce PDB protège l'entrée +# publique du cluster. +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: traefik + namespace: kube-public +spec: + minAvailable: 1 + selector: + matchLabels: + app: traefik diff --git a/scripts/load.sh b/scripts/load.sh new file mode 100755 index 0000000..0a41639 --- /dev/null +++ b/scripts/load.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# OpsWarden Ops — générateur de charge pour déclencher l'autoscaling (HPA). +# Suivre l'effet dans un autre terminal : +# watch -n2 kubectl get hpa,deploy server +# kubectl get pods -l app=server -o wide -w +# +# Usage : +# ./scripts/load.sh # 60s, auto-détecte hey/ab, sinon boucle curl +# DURATION=120 CONCURRENCY=100 ./scripts/load.sh +# WEB_URL=http://api.opswarden.example:30021 ./scripts/load.sh +set -euo pipefail + +WEB_URL="${WEB_URL:-http://app.opswarden.example:30021}" +DURATION="${DURATION:-60}" +CONCURRENCY="${CONCURRENCY:-50}" + +echo ">> Cible : $WEB_URL" +echo ">> Durée : ${DURATION}s" +echo ">> Concurrence : $CONCURRENCY" + +if command -v hey >/dev/null 2>&1; then + echo ">> Avec hey" + hey -z "${DURATION}s" -c "$CONCURRENCY" "$WEB_URL" +elif command -v ab >/dev/null 2>&1; then + echo ">> Avec ApacheBench (ab) — ~$((CONCURRENCY * DURATION * 10)) requêtes" + ab -t "$DURATION" -c "$CONCURRENCY" "$WEB_URL/" +else + echo ">> Pas de hey/ab — repli sur une boucle curl parallèle" + end=$(( $(date +%s) + DURATION )) + for _ in $(seq 1 "$CONCURRENCY"); do + ( while [ "$(date +%s)" -lt "$end" ]; do curl -fsS "$WEB_URL" -o /dev/null || true; done ) & + done + wait +fi + +echo ">> Charge terminée. Vérifier : kubectl get hpa" diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..b26fb44 --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# OpsWarden Ops — smoke test bout-en-bout / infra. +# Prouve le chemin public (Traefik) et dumpe l'état du cluster. +# +# La couche prête (Traefik + cluster) est toujours vérifiée. Les routes +# applicatives (web / api) sont best-effort : elles restent "skip" tant que les +# images OpsWarden ne sont pas déployées (server/client-web sont des placeholders). +# +# Par défaut : s'appuie sur /etc/hosts mappant les hôtes vers une IP de nœud. +# Sans /etc/hosts (NixOS / minikube), résoudre via curl à la place : +# RESOLVE_IP=$(minikube ip) ./scripts/smoke.sh # ou: make minikube-smoke +set -euo pipefail + +PORT="${PORT:-30021}" +DASH_PORT="${DASH_PORT:-30042}" +WEB_HOST="${WEB_HOST:-app.opswarden.example}" +API_HOST="${API_HOST:-api.opswarden.example}" +WEB_URL="${WEB_URL:-http://$WEB_HOST:$PORT}" +API_URL="${API_URL:-http://$API_HOST:$PORT}" + +# Avec RESOLVE_IP, on envoie le bon Host header mais on résout vers l'IP du nœud +# (pas besoin de /etc/hosts). Pointe aussi le dashboard Traefik sur cette IP. +RESOLVE=() +if [ -n "${RESOLVE_IP:-}" ]; then + RESOLVE=(--resolve "$WEB_HOST:$PORT:$RESOLVE_IP" --resolve "$API_HOST:$PORT:$RESOLVE_IP") + TRAEFIK_PING="${TRAEFIK_PING:-http://$RESOLVE_IP:$DASH_PORT/ping}" +else + TRAEFIK_PING="${TRAEFIK_PING:-http://localhost:$DASH_PORT/ping}" +fi + +pass() { printf ' \033[0;32m✓\033[0m %s\n' "$1"; } +skip() { printf ' \033[0;33m•\033[0m %s\n' "$1"; } + +echo "== Couche prête (Traefik) ==" +code=$(curl -fsS -o /dev/null -w '%{http_code}' "$TRAEFIK_PING") \ + && pass "traefik /ping -> HTTP $code" \ + || echo " (traefik /ping injoignable d'ici — ok si pas de port-forward)" + +echo "== Routes applicatives (best-effort tant que non déployées) ==" +check_app() { + name="$1"; url="$2"; folder="$3" + code=$(curl -fsS "${RESOLVE[@]}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null) \ + && pass "$name ($url) -> HTTP $code" \ + || skip "$name ($url) pas encore servi — déployer $folder pour l'allumer" +} +check_app web "$WEB_URL" "k8s/client-web/" +check_app api "$API_URL" "k8s/server/" + +echo "== État du cluster ==" +kubectl get pods -o wide +kubectl get svc,ingress +kubectl -n kube-public get pods,svc -o wide + +echo "== Smoke test OK ==" diff --git a/scripts/soft-affinity.sh b/scripts/soft-affinity.sh new file mode 100755 index 0000000..879d74c --- /dev/null +++ b/scripts/soft-affinity.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env sh +# Bascule l'anti-affinity des pods entre dure (required, défaut des manifests) et +# souple (preferred) sur les déploiements répliqués — SANS éditer les manifests. +# +# Pourquoi : la base utilise une anti-affinity *required* (vraie HA : un replica +# par nœud). Cela suppose un node pool avec de la marge (nodes > replicas). Sur un +# petit cluster (<=2 nœuds, minikube), "required" laisse les pods en trop en +# Pending et bloque le surge en rolling-update / le scale-up HPA. Lancer ceci +# pour relâcher le temps d'une démo. +# +# Les déploiements applicatifs (server/client-web) sont des placeholders : ils +# sont sautés tant qu'ils n'existent pas. Seul traefik est patché aujourd'hui. +# +# Usage : +# ./soft-affinity.sh on # required -> preferred (petits clusters / local) +# ./soft-affinity.sh off # preferred -> required (restaure la base) +set -eu + +MODE="${1:-}" +[ "$MODE" = "on" ] || [ "$MODE" = "off" ] || { + echo "usage: $0 on|off" >&2; exit 2; } + +# app:namespace — déploiements répliqués avec anti-affinity. +TARGETS="server:default client-web:default traefik:kube-public" + +patch_for() { + app="$1" + if [ "$MODE" = "on" ]; then + cat </dev/null 2>&1; then + echo ">> skip deploy/$app (ns $ns) — absent (placeholder)" + continue + fi + echo ">> $MODE anti-affinity: deploy/$app (ns $ns)" + kubectl -n "$ns" patch deployment "$app" --type merge --patch "$(patch_for "$app")" +done + +echo ">> fait. Vérifier : kubectl get pods -o wide"