hoortrend.com

Documentation · version du 17 octobre 2026

Documentation · as of 17 October 2026

La documentation du lab

The lab documentation

Un petit cluster Kubernetes à deux nœuds, quelques services qui tournent à côté, et un seul point d'entrée HTTPS. Cette page décrit chaque pièce, pourquoi elle est là, et comment l'exploiter.

A small two-node Kubernetes cluster, a few services running beside it, and a single HTTPS entry point. This page describes each piece, why it is there, and how to operate it.

Vue d'ensembleOverview

Le cluster est installé avec Kubespray (Kubernetes 1.35) sur deux machines Debian :

  • master1 : le plan de contrôle (API, etcd, scheduler). Il est protégé par un taint, donc aucune charge applicative n'y tourne.
  • worker1 : 4 vCPU, 16 Go de RAM. Il exécute toutes les applications, le contrôleur d'ingress, et héberge aussi deux services installés directement sur la machine : GitLab et Zabbix.

Les briques de base : Calico pour le réseau, ingress-nginx pour l'entrée HTTP(S), cert-manager pour les certificats, kube-prometheus-stack pour la supervision, Rancher pour la gestion, et le registry de GitLab pour les images. Les versions et le rôle de chaque brique sont sur la page Stack du portfolio.

The cluster is installed with Kubespray (Kubernetes 1.35) on two Debian machines:

  • master1: the control plane (API, etcd, scheduler). It carries a taint, so no application workload runs there.
  • worker1: 4 vCPU, 16 GB RAM. It runs every application and the ingress controller, and also hosts two services installed directly on the machine: GitLab and Zabbix.

The building blocks: Calico for networking, ingress-nginx for HTTP(S) entry, cert-manager for certificates, kube-prometheus-stack for monitoring, Rancher for management, and the GitLab registry for images. Versions and the role of each piece are on the portfolio's Stack page.

Domaines et servicesDomains and services

Tous les noms pointent vers l'IP publique du worker. Aucun port n'est à saisir : tout passe par 443.

Every name points to the worker's public IP. No port to type: everything goes through 443.

DomaineCe que c'estWhat it isOù ça tourneWhere it runs
hoortrend.comCette page d'accueil, sa documentation et la page « Comment ça marche »This home page, its documentation and the “How it works” pageKubernetes · ns landing · 2 pods nginx
portfolio.ilyes-litim.frLe portfolio SRE (FastAPI + JavaScript) et sa page de contactThe SRE portfolio (FastAPI + JavaScript) and its contact pageKubernetes · ns portfolio · 2 pods portfolio-web + 2 pods contact-api
grafanaTableaux de bord PrometheusPrometheus dashboardsKubernetes · ns monitoring
rancherConsole de gestion du clusterCluster management consoleKubernetes · ns cattle-system
gitlabForge Git et CI/CD (GitLab 17.9, installation Omnibus)Git forge and CI/CD (GitLab 17.9, Omnibus install)Machine worker1 · via upstreamworker1 host · via upstream
registryRegistre d'images : toutes les images du labContainer registry: every image of the labMachine worker1 · via upstreamworker1 host · via upstream
zabbixSupervision Zabbix 6.4Zabbix 6.4 monitoringMachine worker1 · via upstreamworker1 host · via upstream

Le www.hoortrend.com n'existe pas : seul le domaine nu est configuré.

www.hoortrend.com does not exist: only the bare domain is configured.

Le portfolio a son propre domaine, ilyes-litim.fr : son enregistrement A pointe vers le même worker et son certificat Let's Encrypt est obtenu comme les autres. L'ancien nom portfolio.hoortrend.com redirige de façon permanente vers le nouveau.

The portfolio has its own domain, ilyes-litim.fr: its A record points to the same worker and its Let's Encrypt certificate is obtained like the others. The old name portfolio.hoortrend.com permanently redirects to the new one.

Le chemin d'une requêteRequest path

DNS, ingress-nginx (qui termine le TLS et choisit la destination d'après le nom d'hôte), Service, puis pod. Pour les services hors cluster, l'ingress envoie directement vers la machine (section suivante). Le détail, avec un schéma et un test en direct, est sur la page Comment ça marche.

DNS, ingress-nginx (which terminates TLS and picks the destination from the host name), Service, then pod. For services outside the cluster, the ingress sends straight to the host (next section). The detail, with a diagram and a live test, is on the How it works page.

Le formulaire de contactThe contact form

La page /contact du portfolio envoie le message à POST /api/contact, sur le même domaine. L'ingress du portfolio route ce seul chemin vers un petit service Python (contact-api) qui valide les champs puis relaie le message par SMTP authentifié (STARTTLS) vers ma boîte mail. Le message n'est stocké nulle part.

Ce service est séparé du site exprès : il détient le mot de passe SMTP, que portfolio-web (qui parle à l'API Kubernetes) ne doit jamais voir, et il n'a aucun jeton Kubernetes. Le mot de passe vit dans un Secret créé à la main, dans le namespace portfolio, jamais dans Git.

Protections : champ piège contre les robots, vérification de l'origine, limite de débit (par visiteur et pour tout le site), taille maximale, refus des retours à la ligne dans les en-têtes (injection), et un journal qui ne contient jamais le nom, l'adresse ni le message.

The portfolio's /contact page posts the message to POST /api/contact on the same domain. The portfolio's ingress routes only that path to a small Python service (contact-api) which validates the fields and relays the message over authenticated SMTP (STARTTLS) to my mailbox. The message is stored nowhere.

The service is separate from the site on purpose: it holds the SMTP password, which portfolio-web (which talks to the Kubernetes API) must never see, and it has no Kubernetes token. The password lives in a Secret created by hand, in the portfolio namespace, never in Git.

Protections: a honeypot field against robots, origin check, rate limits (per visitor and site-wide), a size cap, rejection of line breaks in headers (injection), and a log that never contains the name, address or message.

Services hors KubernetesServices outside Kubernetes

GitLab et Zabbix sont installés directement sur worker1. Ils ne peuvent pas écouter sur 80 et 443, déjà pris par l'ingress. Chacun écoute donc sur un port interne, lié à une adresse privée de la boucle locale (jamais à l'adresse publique), et l'ingress joue le rôle de reverse proxy avec upstream :

GitLab and Zabbix are installed directly on worker1. They cannot listen on 80 and 443, which the ingress already owns. Each one listens on an internal port, bound to a private loopback address (never the public one), and the ingress acts as a reverse proxy with an upstream:

ServicePort interneInternal portParticularitéNote
GitLab8929external_url sans port, nginx Omnibus en listen_port 8929external_url without a port, Omnibus nginx on listen_port 8929
Registry5443Même principe pour registry_nginxSame principle for registry_nginx
Zabbix8444Site nginx dédié + PHP-FPM ; le serveur Zabbix garde le port 10051 pour les agentsDedicated nginx site + PHP-FPM; the Zabbix server keeps port 10051 for agents

Dans Kubernetes, chaque service externe est un Service sans sélecteur, dont l'Endpoints pointe vers la machine. L'Ingress l'utilise ensuite comme n'importe quel autre :

In Kubernetes, each external service is a selector-less Service whose Endpoints points at the host. The Ingress then uses it like any other:

# namespace external-hosts
apiVersion: v1
kind: Service
metadata: {name: gitlab, namespace: external-hosts}
spec: {ports: [{name: https, port: 8929, targetPort: 8929}]}
---
kind: Endpoints
metadata: {name: gitlab, namespace: external-hosts}
subsets: [{addresses: [{ip: 10.255.255.1}], ports: [{name: https, port: 8929}]}]
---
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"      # pas de limite sur les push Git
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"

Ces ports ne sont donc pas joignables depuis Internet : seul l'ingress, qui tourne sur la même machine, peut les atteindre. L'adresse est créée au démarrage par une unité systemd.

These ports are therefore not reachable from the Internet: only the ingress, running on the same machine, can reach them. The address is created at boot by a systemd unit.

Ajouter un nouveau service de ce type : un Service + Endpoints + Ingress, rien d'autre. Le certificat est demandé tout seul.

Adding another service of this kind takes a Service + Endpoints + Ingress, nothing else. The certificate is requested automatically.

Certificats TLSTLS certificates

Un seul ClusterIssuer, letsencrypt-prod, utilise le challenge HTTP-01. L'annotation cert-manager.io/cluster-issuer sur un Ingress suffit : cert-manager crée le certificat, le stocke dans le Secret nommé par tls.secretName, et le renouvelle avant l'expiration (environ 30 jours avant).

Le certificat vu par les visiteurs est toujours celui de l'ingress. Les services de la machine ont leur propre certificat interne, qui n'est utilisé que pour la liaison entre l'ingress et eux : l'ingress ne le valide pas.

A single ClusterIssuer, letsencrypt-prod, uses the HTTP-01 challenge. The cert-manager.io/cluster-issuer annotation on an Ingress is enough: cert-manager creates the certificate, stores it in the Secret named by tls.secretName, and renews it before expiry (about 30 days ahead).

The certificate visitors see is always the ingress one. The host services have their own internal certificate, used only for the link between the ingress and them: the ingress does not validate it.

kubectl get certificate -A
kubectl -n external-hosts describe certificate gitlab-hoortrend-com-tls

Construire et déployerBuild and deploy

Tout le code vit dans GitLab (groupe hoortrend : portfolio, landing, demo-app, sre-demo, infra). Chaque git push sur main lance un pipeline, et les images sont publiées dans le registry de GitLab.

  1. validate : syntaxe du code, des manifestes et des fichiers référencés par les pages.
  2. build : l'image est construite avec Kaniko dans un pod, sans accès privilégié, puis publiée sous le tag du commit.
  3. deploy : kubectl set image sur le Deployment, puis attente de la fin du déploiement progressif.
  4. verify : un test de fumée interroge le site public.

Sur une merge request, seules les étapes de validation et de build tournent, sans publier ni déployer. Avec maxUnavailable: 0, un nouveau pod doit être prêt avant qu'un ancien soit arrêté : le site reste disponible pendant la mise à jour.

Les jobs s'exécutent dans un runner Kubernetes. Son compte de service ne peut changer que l'image d'un seul Deployment nommé par namespace : la CI n'a pas de droits d'administration du cluster. Les pods tirent leurs images avec un jeton en lecture seule.

All code lives in GitLab (group hoortrend: portfolio, landing, demo-app, sre-demo, infra). Every git push to main starts a pipeline, and images are published to the GitLab registry.

  1. validate: syntax of the code, manifests and the files pages reference.
  2. build: the image is built with Kaniko inside a pod, without privileged access, then published under the commit tag.
  3. deploy: kubectl set image on the Deployment, then wait for the rolling update to finish.
  4. verify: a smoke test queries the public site.

On a merge request, only validate and build run, without publishing or deploying. With maxUnavailable: 0, a new pod must be ready before an old one is stopped: the site stays up during the update.

Jobs run in a Kubernetes runner. Its service account can only change the image of one named Deployment per namespace: CI has no cluster-admin rights. Pods pull their images with a read-only token.

git push origin main                                  # lance le pipeline / starts the pipeline
kubectl -n landing rollout status deploy/landing-web
kubectl -n landing rollout undo deploy/landing-web   # retour arrière / rollback

Fichiers de configurationConfiguration files

Ces fichiers sont copiés tels quels dans l'image au moment du build : ce sont exactement ceux qui font tourner la page d'accueil.

These files are copied as-is into the image at build time: they are exactly what runs the home page.


    
  • cert-manager.io/cluster-issuer demande le certificat Let's Encrypt, émis et renouvelé automatiquement.
  • cert-manager.io/cluster-issuer requests the Let's Encrypt certificate, issued and renewed automatically.
  • ssl-redirect renvoie tout le trafic HTTP vers HTTPS.
  • ssl-redirect sends all HTTP traffic to HTTPS.
  • limit-rps limite le débit par client pour protéger la page.
  • limit-rps rate-limits each client to protect the page.
  • La règle envoie hoortrend.com vers le Service, sans exposer les pods.
  • The rule routes hoortrend.com to the Service without exposing the pods.

Workflow de chaque projetEach project's workflow

Tous les projets suivent le même chemin, du poste du développeur jusqu'au site public. Choisis un projet, puis rejoue son pipeline : les étapes, les jobs et les images sont les vrais, et le .gitlab-ci.yml exact est affiché dessous.

Every project follows the same path, from the developer's machine to the public site. Pick a project, then replay its pipeline: the stages, jobs and images are the real ones, and the exact .gitlab-ci.yml is shown below.

  1. Branche. Le changement est fait sur une branche, jamais directement sur main.
  2. Branch. The change is made on a branch, never directly on main.
  3. Merge request. Le pipeline valide et construit l'image sans la publier : on sait que ça passe avant de fusionner.
  4. Merge request. The pipeline validates and builds the image without publishing it: we know it passes before merging.
  5. Fusion dans main. Le pipeline complet démarre : validation, construction, publication, déploiement.
  6. Merge into main. The full pipeline starts: validate, build, publish, deploy.
  7. Déploiement progressif. Kubernetes remplace les pods un par un, et ne coupe un ancien pod qu'une fois le nouveau prêt.
  8. Rolling update. Kubernetes replaces pods one by one, and only stops an old pod once the new one is ready.
  9. Vérification. Un test de fumée interroge le site public. En cas de problème : rollout undo, retour à la version précédente en quelques secondes.
  10. Verification. A smoke test queries the public site. If something is wrong: rollout undo, back to the previous version in seconds.


  

Le code affiché est une copie du fichier du dépôt, mise à jour avec cette page. Les durées sont l'ordre de grandeur mesuré le 17 octobre 2026 (démarrage du pod du job compris).

The code shown is a copy of the repository file, updated along with this page. Durations are the order of magnitude measured on 17 October 2026 (job pod start-up included).

SécuritéSecurity

Dans les pods

  • Les namespaces landing et playground imposent Pod Security « restricted » : le cluster refuse un pod non conforme. Seul gitlab-runner est en « baseline » (le build d'images).
  • Utilisateur non-root, système de fichiers en lecture seule, toutes les capabilities supprimées, profil seccomp.
  • Aucun jeton d'API Kubernetes monté dans les pods qui n'en ont pas besoin.
  • Limites de CPU et de mémoire sur chaque conteneur.

In the pods

  • The landing and playground namespaces enforce Pod Security “restricted”: the cluster rejects a non-compliant pod. Only gitlab-runner is “baseline” (image builds).
  • Non-root user, read-only file system, all capabilities dropped, seccomp profile.
  • No Kubernetes API token mounted in pods that do not need one.
  • CPU and memory limits on every container.

Sur le réseau

  • HTTPS partout, redirection automatique depuis HTTP, HSTS.
  • En-têtes de sécurité et Content-Security-Policy stricte sur chaque page.
  • Limitation de débit par client sur l'ingress.
  • Le bac à sable du portfolio est isolé par une NetworkPolicy : ses pods ne peuvent joindre que le strict nécessaire.

On the network

  • HTTPS everywhere, automatic redirect from HTTP, HSTS.
  • Security headers and a strict Content-Security-Policy on every page.
  • Per-client rate limiting at the ingress.
  • The portfolio sandbox is isolated by a NetworkPolicy: its pods can only reach what they strictly need.

Dans l'API du portfolio

  • Compte de service en lecture seule ; les actions du bac à sable ne visent que son propre namespace, avec une liste d'actions autorisées.
  • Le relais Slack refuse les destinations qui ne sont pas Slack (protection SSRF), limite le débit, et ne stocke ni ne journalise jamais l'URL du webhook.
  • CORS ouvert uniquement à cette origine, pour deux routes en lecture.

In the portfolio API

  • Read-only service account; sandbox actions only target their own namespace, from an allow-list of actions.
  • The Slack relay rejects any non-Slack destination (SSRF protection), rate-limits, and never stores or logs the webhook URL.
  • CORS opened only to this origin, for two read routes.

SupervisionMonitoring

Deux outils complémentaires :

  • Prometheus + Alertmanager + Grafana (kube-prometheus-stack) pour Kubernetes : nœuds, pods, API. Les alertes sont aussi lues par le portfolio, qui les affiche en direct et peut les relayer vers Slack.
  • Zabbix pour la machine worker1 et ses services. Les agents parlent au serveur sur le port 10051.

Two complementary tools:

  • Prometheus + Alertmanager + Grafana (kube-prometheus-stack) for Kubernetes: nodes, pods, the API. Alerts are also read by the portfolio, which shows them live and can relay them to Slack.
  • Zabbix for the worker machine and its services. Agents talk to the server on port 10051.

ProcéduresRunbook

Un site ne répond plus

A site stopped answering

kubectl get nodes
kubectl get pods -A | grep -v Running
kubectl -n ingress-nginx logs ds/ingress-nginx-controller --tail=50
kubectl get ingress -A

GitLab ou Zabbix : 502 ou 504

GitLab or Zabbix: 502 or 504

L'ingress est vivant, c'est l'upstream sur la machine qui ne répond pas. Sur worker1 :

The ingress is alive; the upstream on the host is not answering. On worker1:

sudo gitlab-ctl status                 # GitLab : tous les services en "run"
sudo systemctl status nginx php7.4-fpm zabbix-server
ss -ltn | grep -E ':(8929|5443|8444) '

Après un redémarrage de GitLab, la page « Waiting for GitLab to boot » (502) est normale pendant une à deux minutes.

After a GitLab restart, the “Waiting for GitLab to boot” page (502) is normal for a minute or two.

Un certificat ne se renouvelle pas

A certificate does not renew

kubectl get certificate,order,challenge -A
kubectl -n cert-manager logs deploy/cert-manager --tail=50

Cause fréquente : le port 80 ne mène plus à l'ingress, ou l'enregistrement DNS ne pointe plus vers le worker.

Common cause: port 80 no longer reaches the ingress, or the DNS record no longer points to the worker.

Exposer un nouveau sous-domaine

Expose a new subdomain

  1. Créer l'enregistrement DNS A vers l'IP du worker.
  2. Create the DNS A record to the worker's IP.
  3. Écrire un Ingress avec l'annotation cert-manager.io/cluster-issuer: letsencrypt-prod et un bloc tls.
  4. Write an Ingress with the cert-manager.io/cluster-issuer: letsencrypt-prod annotation and a tls block.
  5. Vérifier : kubectl get certificate passe à True en quelques secondes.
  6. Check: kubectl get certificate turns True within seconds.

Limites connuesKnown limits

Ce lab est une démonstration, pas une production à haute disponibilité :

  • Un seul worker. Les deux pods de chaque application tournent sur la même machine : elle protège d'un crash de pod, pas d'une panne de machine.
  • Un seul plan de contrôle (pas de haute disponibilité de l'API ni d'etcd).
  • GitLab et Zabbix vivent sur la machine, hors du cycle de vie Kubernetes : mises à jour et sauvegardes sont manuelles.
  • Le terminal k9s du portfolio est une émulation alimentée par l'API, pas un vrai terminal distant.

This lab is a demonstration, not a highly available production:

  • One worker. Both pods of each application run on the same machine: that guards against a pod crash, not a machine failure.
  • One control plane (no API or etcd high availability).
  • GitLab and Zabbix live on the host, outside the Kubernetes lifecycle: updates and backups are manual.
  • The portfolio's k9s terminal is an emulation fed by the API, not a real remote terminal.