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.
- 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).
- 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éé aveckubectl apply. - 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. - Le service de la machine. GitLab (Omnibus) est installé directement sur worker1 et publié par l'ingress (voir Services hors Kubernetes).
- 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 aveckubectl apply. Ensuite, le runner GitLab, installé dans le cluster, construit, analyse et déploie chaque nouvelle version (voir Construire et déployer). - 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.
- 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).
- 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 withkubectl apply. - 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. - The host service. GitLab (Omnibus) is installed directly on worker1 and published through the ingress (see Services outside Kubernetes).
- The applications, with manifests and CI. Each application keeps its manifests in its repository (
k8s/), applied once withkubectl apply. After that, the GitLab runner, installed in the cluster, builds, scans and deploys every new version (see Build and deploy). - 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 Helm | Helm release | Chart | Ce qu'elle installe | What it installs |
|---|---|---|---|---|
ingress-nginx | ingress-nginx 4.15.1 | Le 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. | |
monitoring | kube-prometheus-stack 91.9.0 | Prometheus 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. | |
argocd | argo-cd 10.10.5 | Argo 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.
| Domaine | Ce que c'est | What it is | Où ça tourne | Where it runs |
|---|---|---|---|---|
| hoortrend.com | Cette page d'accueil, sa documentation et la page « Comment ça marche » | This home page, its documentation and the “How it works” page | Kubernetes · ns landing · 2 pods nginx | |
| lab.hoortrend.com | Le 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.fr | Le portfolio professionnel (React + Next.js exporté en fichiers statiques) et son formulaire de contact | The professional portfolio (React + Next.js exported as static files) and its contact form | Kubernetes · ns portfolio · 2 pods portfolio-web + 2 pods contact-api | |
| grafana | Tableaux de bord Prometheus | Prometheus dashboards | Kubernetes · ns monitoring | |
| argocd | Argo CD : synchronisation GitOps de demo-app (administrateur seulement) | Argo CD: GitOps sync of demo-app (admin only) | Kubernetes · ns argocd | |
| gitlab | Forge Git et CI/CD (GitLab, installation Omnibus) | Git forge and CI/CD (GitLab, Omnibus install) | Machine worker1 · via upstream | worker1 host · via upstream |
| registry | Registre d'images : toutes les images du lab | Container registry: every image of the lab | Machine worker1 · via upstream | worker1 host · via upstream |
| auth, notes, board, api | Lab d'authentification : serveur OAuth 2.0 / OIDC, deux clients, une API | Authentication lab: OAuth 2.0 / OIDC server, two clients, an API | Kubernetes · ns auth-lab | |
| inside | Le 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 timed | Kubernetes · ns inside · 1 pod | |
| demo | Application 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:
| Service | Port interne | Internal port | Particularité | Note |
|---|---|---|---|---|
| GitLab | 8929 | external_url sans port, nginx Omnibus en listen_port 8929 | external_url without a port, Omnibus nginx on listen_port 8929 | |
| Registry | 5443 | Même principe pour registry_nginx | Same 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