hoortrend.com

Documentation

Architecture

Architecture

Comment le cluster a été monté, les domaines, le chemin d'une requête, les services hors Kubernetes et les certificats.

How the cluster was built, the domains, a request's path, the services outside Kubernetes and the certificates.

Comment le cluster a été montéHow the cluster was built

Cinq étapes, dans cet ordre, chacune avec l'outil adapté : Kubespray pour Kubernetes lui-même, Helm pour les briques communes du cluster, un paquet sur la machine pour GitLab, et des manifestes versionnés plus la CI pour les applications.

  1. Kubernetes, avec Kubespray. Kubespray, un ensemble de playbooks Ansible, installe Kubernetes 1.35.4 sur les deux machines Debian 11 à partir d'un inventaire : le runtime containerd 2.2, etcd 3.6 (service système sur master1), le plan de contrôle sur master1 (via kubeadm), le réseau Calico 3.31, kube-proxy en mode IPVS, CoreDNS et un cache DNS sur chaque nœud (NodeLocal DNS).
  2. Les modules de base, avec Kubespray. Le même outil ajoute metrics-server (CPU et mémoire des pods), local-path-provisioner (volumes persistants sur le disque du nœud) et cert-manager 1.15 (certificats). Un émetteur Let's Encrypt (ClusterIssuer) est ensuite créé avec kubectl apply.
  3. Les briques du cluster, avec Helm. Trois charts, chacun dans son namespace et à une version figée (tableau ci-dessous). Les réglages de la supervision sont versionnés dans le dépôt infra et appliqués avec helm upgrade --reuse-values -f.
  4. Le service de la machine. GitLab (Omnibus) est installé directement sur worker1 et publié par l'ingress (voir Services hors Kubernetes).
  5. Les applications, avec des manifestes et la CI. Chaque application a ses manifestes dans son dépôt (k8s/), appliqués une première fois avec kubectl apply. Ensuite, le runner GitLab, installé dans le cluster, construit, analyse et déploie chaque nouvelle version (voir Construire et déployer).
  6. GitOps avec Argo CD. Argo CD, installé avec Helm, applique lui-même les manifestes de demo-app depuis Git et annule tout écart ; le pipeline garde le choix de l'image (voir GitOps avec Argo CD).

Five steps, in this order, each with the right tool: Kubespray for Kubernetes itself, Helm for the cluster's shared building blocks, a package on the host for GitLab, and versioned manifests plus CI for the applications.

  1. Kubernetes, with Kubespray. Kubespray, a set of Ansible playbooks, installs Kubernetes 1.35.4 on the two Debian 11 machines from an inventory: the containerd 2.2 runtime, etcd 3.6 (a system service on master1), the control plane on master1 (through kubeadm), Calico 3.31 networking, kube-proxy in IPVS mode, CoreDNS and a DNS cache on each node (NodeLocal DNS).
  2. The base add-ons, with Kubespray. The same tool adds metrics-server (pod CPU and memory), local-path-provisioner (persistent volumes on the node's disk) and cert-manager 1.15 (certificates). A Let's Encrypt issuer (ClusterIssuer) is then created with kubectl apply.
  3. The cluster's building blocks, with Helm. Three charts, each in its own namespace and at a pinned version (table below). The monitoring settings are versioned in the infra repository and applied with helm upgrade --reuse-values -f.
  4. The host service. GitLab (Omnibus) is installed directly on worker1 and published through the ingress (see Services outside Kubernetes).
  5. The applications, with manifests and CI. Each application keeps its manifests in its repository (k8s/), applied once with kubectl apply. After that, the GitLab runner, installed in the cluster, builds, scans and deploys every new version (see Build and deploy).
  6. GitOps with Argo CD. Argo CD, installed with Helm, applies demo-app's manifests itself from Git and reverts any drift; the pipeline keeps choosing the image (see GitOps with Argo CD).
Release HelmHelm releaseChartCe qu'elle installeWhat it installs
ingress-nginxingress-nginx 4.15.1Le contrôleur d'entrée HTTP(S) : un DaemonSet sur worker1, branché directement sur les ports 80 et 443 de la machine (hostNetwork), classe d'Ingress par défaut.The HTTP(S) entry controller: a DaemonSet on worker1, bound directly to the host's ports 80 and 443 (hostNetwork), default Ingress class.
monitoringkube-prometheus-stack 91.9.0Prometheus et son opérateur, Alertmanager, Grafana, node-exporter, kube-state-metrics et les règles d'alerte. Six mises à jour depuis, toutes par des fichiers de valeurs versionnés.Prometheus and its operator, Alertmanager, Grafana, node-exporter, kube-state-metrics and the alert rules. Six upgrades since, all through versioned values files.
argocdargo-cd 10.10.5Argo CD v3.5.5, en GitOps : il synchronise demo-app depuis son dépôt (correction automatique de tout écart fait à la main ; l'image reste posée par le pipeline). Accès administrateur seulement ; ni Dex ni notifications, un exemplaire de chaque composant.Argo CD v3.5.5, for GitOps: it syncs demo-app from its repository (any change made by hand is reverted; the image is still set by the pipeline). Admin access only; no Dex, no notifications, one copy of each component.

Pour le constater sur le cluster :

To see it on the cluster:

kubectl get nodes -o wide     # nœuds, version, runtime / nodes, version, runtime
helm list -A                  # releases Helm / Helm releases
ls /etc/kubernetes/addons/    # modules Kubespray / Kubespray add-ons (master1)

Pas encore dans Git : l'inventaire Kubespray. L'y mettre est la prochaine étape pour pouvoir reconstruire le cluster à l'identique depuis les dépôts.

Not in Git yet: the Kubespray inventory. Putting it there is the next step towards rebuilding the cluster identically from the repositories.

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
lab.hoortrend.comLe lab en direct (FastAPI + JavaScript) : architecture, état du cluster, observabilité, manifestes, bac à sable, notifications.The live lab (FastAPI + JavaScript): architecture, cluster state, observability, manifests, sandbox, notifications.Kubernetes · ns lab · 2 pods lab-web + 2 pods notify-api
portfolio.ilyes-litim.frLe portfolio professionnel (React + Next.js exporté en fichiers statiques) et son formulaire de contactThe professional portfolio (React + Next.js exported as static files) and its contact formKubernetes · ns portfolio · 2 pods portfolio-web + 2 pods contact-api
grafanaTableaux de bord PrometheusPrometheus dashboardsKubernetes · ns monitoring
argocdArgo CD : synchronisation GitOps de demo-app (administrateur seulement)Argo CD: GitOps sync of demo-app (admin only)Kubernetes · ns argocd
gitlabForge Git et CI/CD (GitLab, installation Omnibus)Git forge and CI/CD (GitLab, 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
auth, notes, board, apiLab d'authentification : serveur OAuth 2.0 / OIDC, deux clients, une APIAuthentication lab: OAuth 2.0 / OIDC server, two clients, an APIKubernetes · ns auth-lab
insideLe playground du lab vu de l'intérieur : ce que Kubernetes fait, en direct et chronométréThe lab's playground from the inside: what Kubernetes does, live and timedKubernetes · ns inside · 1 pod
demoApplication de démonstration : montre qu'une application déployée par la CI fonctionne et répond (pod, nœud, version)Demonstration application: shows that an application deployed by the CI works and answers (pod, node, version)Kubernetes · ns demo-app · 2 pods

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 remet le message directement au serveur de messagerie de ma boîte (celui de hoortrend.com, en STARTTLS avec certificat vérifié), sans compte SMTP ni mot de passe. Le message n'est stocké nulle part.

L'adresse d'envoi, contact@ilyes-litim.fr, passe le contrôle SPF : l'enregistrement SPF de ilyes-litim.fr autorise l'adresse de worker1, celle que voit le serveur de réception. Aucun secret n'est donc nécessaire. Le service reste séparé du site exprès : il reçoit les données de n'importe quel visiteur et n'a aucun jeton Kubernetes ; le site lui-même n'est que des fichiers statiques servis par nginx, sans jeton non plus.

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 hands the message straight to my mailbox's mail server (hoortrend.com's, over STARTTLS with a checked certificate), with no SMTP account or password. The message is stored nowhere.

The sending address, contact@ilyes-litim.fr, passes the SPF check: the SPF record of ilyes-litim.fr allows worker1's address, the one the receiving server sees. So no secret is needed. The service stays separate from the site on purpose: it takes input from any visitor and has no Kubernetes token; the site itself is only static files served by nginx, with no token either.

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 est installé directement sur worker1. Il ne peut pas écouter sur 80 et 443, déjà pris par l'ingress. GitLab et son registre écoutent donc chacun 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 is installed directly on worker1. It cannot listen on 80 and 443, which the ingress already owns. GitLab and its registry each listen 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

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.

Un second émetteur, letsencrypt-dns-ovh, sert aux certificats wildcard, que le challenge HTTP-01 ne peut pas obtenir : cert-manager prouve le contrôle du domaine par un enregistrement TXT temporaire (challenge DNS-01), posé dans la zone DNS par un webhook OVH, puis retiré. Il est réservé à un seul nom, *.web-ide.gitlab.hoortrend.com (voir Web IDE isolé), et sa clé d'API ne peut que lire, créer et supprimer des enregistrements de cette zone. L'image du webhook est reconstruite depuis ses sources et scannée par le pipeline, comme les nôtres.

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.

A second issuer, letsencrypt-dns-ovh, is for wildcard certificates, which the HTTP-01 challenge cannot get: cert-manager proves control of the domain with a temporary TXT record (DNS-01 challenge), set in the DNS zone by an OVH webhook, then removed. It is kept for a single name, *.web-ide.gitlab.hoortrend.com (see Isolated Web IDE), and its API key can only read, create and delete records of that zone. The webhook image is rebuilt from source and scanned by the pipeline, like ours.

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