Chapitre 03

Le socle du cluster

Un cluster nu ne sait ni router un pod, ni stocker un fichier, ni répondre en HTTPS. Six composants, installés dans le bon ordre, en font une plateforme.

Niveau avancé  ·  Lecture 15 min  ·  Base Talos 1.14, Kubernetes 1.36

Renseignez votre domaine et vos adresses : les commandes et les fichiers de la page s'adaptent. Les valeurs restent dans votre navigateur et ne nous sont jamais transmises.

01Un script, un ordre

Chaque cluster a son script de bootstrap. Il déclare les dépôts Helm, injecte les secrets depuis SOPS, puis installe les composants dans un ordre qui respecte leurs dépendances : pas de stockage avant le réseau, pas de certificat avant l'ingress.

OrdreComposantPourquoi à ce moment
1CiliumSans réseau des pods, rien d'autre ne démarre
2Approbateur des certificats kubelet, metrics-server, tupprLe minimum vital d'exploitation
3ceph-csi et contrôleur de snapshotsLes volumes, et leurs instantanés pour Velero
4cert-managerSes CRD doivent exister avant les Ingress
5CrowdSecLe middleware doit exister avant Traefik
6TraefikL'ingress et ses journaux d'accès
7external-dnsPublie les noms des Ingress
8Velero et talos-backupLes sauvegardes (chapitre 06)

Chaque installation suit le même motif, qui rend le script relançable à volonté :

Le motif d'installation
helm upgrade --install traefik traefik/traefik \
  --version "$(cat k8s/traefik/version)" \
  -n traefik --create-namespace \
  -f k8s/traefik/values.yaml -f k8s/traefik/values-demo.yaml \
  --wait

Un tour complet prend une à deux minutes. Et si un composant a été modifié à la main, il suffit de relancer le script pour revenir à l'état du dépôt.

02Cilium : réseau et adresses

Cilium fournit le réseau des pods, remplace kube-proxy, et attribue aux services de type LoadBalancer des adresses prises dans une plage du VNet, annoncées en ARP. Sur Talos, quelques réglages sont imposés par le système :

Extrait de k8s/cilium/values.yaml
ipam:
  mode: kubernetes
kubeProxyReplacement: true
k8sServiceHost: localhost      # KubePrism, le proxy local de l'API sur Talos
k8sServicePort: 7445
cgroup:
  autoMount: { enabled: false }
  hostRoot: /sys/fs/cgroup
l2announcements:
  enabled: true
Plage d'adresses du cluster demo
apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata:
  name: demo-pool
spec:
  blocks:
    - start: 10.20.1.100
      stop: 10.20.1.150
---
apiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata:
  name: demo-l2
spec:
  loadBalancerIPs: true

Dès que Cilium tourne, les noeuds passent en Ready.

03ceph-csi : des volumes Ceph

Le chart ceph-csi-rbd reçoit l'identifiant du cluster Ceph, ses moniteurs, et la clé de l'utilisateur client.k8s, injectée depuis SOPS. Il crée la classe de stockage par défaut :

Extrait de k8s/ceph-csi-rbd/values.yaml
csiConfig:
  - clusterID: <fsid du cluster Ceph>
    monitors: [172.16.0.11:6789, 172.16.0.12:6789, 172.16.0.13:6789]
storageClass:
  create: true
  name: ceph-rbd
  clusterID: <fsid du cluster Ceph>
  pool: k8s
  reclaimPolicy: Delete
  allowVolumeExpansion: true
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
Un namespace privilégié

Talos applique par défaut le niveau de sécurité baseline à tous les pods. Les pilotes CSI ont besoin de privilèges pour monter des disques sur le noeud : leur namespace reçoit le label pod-security.kubernetes.io/enforce=privileged, et lui seul.

On y ajoute le contrôleur de snapshots et une VolumeSnapshotClass pour ceph-rbd : Velero en aura besoin pour sauvegarder les volumes.

04Traefik : la porte d'entrée

Traefik reçoit tout le trafic HTTP et HTTPS du cluster. Sa configuration pour demo concentre plusieurs leçons apprises :

Extrait de k8s/traefik/values-demo.yaml
deployment:
  replicas: 2                  # un par worker

service:
  spec:
    externalTrafficPolicy: Local          # conserve l'IP réelle des visiteurs
  annotations:
    lbipam.cilium.io/ips: "10.20.1.100"   # cible du DNAT du routeur

# les Ingress affichent l'IP publique : external-dns publie cette adresse
providers:
  kubernetesIngress:
    publishedService: { enabled: false }
additionalArguments:
  - --providers.kubernetesingress.ingressendpoint.ip=203.0.113.47

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    nodeTaintsPolicy: Honor
    matchLabelKeys: [pod-template-hash]
    labelSelector:
      matchLabels: { app.kubernetes.io/name: traefik }

accessLog:
  enabled: true                # lus par CrowdSec
  • externalTrafficPolicy: Local : sans lui, chaque requête arrive avec l'adresse d'un noeud, et CrowdSec bannirait... votre propre cluster.
  • L'adresse publiée : par défaut, un Ingress affiche l'IP interne du service. external-dns créerait alors des noms pointant vers 10.20.1.100, injoignable depuis Internet.
  • La répartition : un Traefik par worker, mais sans nodeTaintsPolicy: Honor les control planes comptent dans le calcul, et sans matchLabelKeys une mise à jour progressive reste bloquée, l'ancien pod occupant la place du nouveau.

05cert-manager et external-dns

Un développeur qui écrit un Ingress avec un nom sous le domaine de la plateforme doit obtenir, sans rien demander, un enregistrement DNS et un certificat. Deux composants s'en chargent.

ClusterIssuer Let's Encrypt (défi HTTP-01)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@exemple.fr
    privateKeySecretRef: { name: letsencrypt-account }
    solvers:
      - http01:
          ingress: { ingressClassName: traefik }
Extrait de k8s/external-dns/values.yaml
provider:
  name: ovh
sources: [ingress]
domainFilters: [k8s.exemple.fr]
policy: upsert-only            # crée et met à jour, ne supprime jamais
txtOwnerId: demo
env:
  - name: OVH_APPLICATION_KEY
    valueFrom: { secretKeyRef: { name: ovh-credentials, key: application_key } }
  # ... OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY

Nous avons préféré le défi HTTP-01, un certificat par nom, à un certificat générique obtenu par défi DNS : la clé de l'API DNS n'a pas à créer d'enregistrements de validation, et un certificat compromis ne vaut que pour un nom. La clé OVH est limitée à l'IP de sortie 203.0.113.35, et le mode upsert-only garantit qu'une fausse manipulation ne vide pas la zone.

06CrowdSec : le videur

CrowdSec lit les journaux d'accès de Traefik, repère les comportements hostiles (scans, force brute, chemins connus des outils d'attaque) et partage ses décisions avec la communauté. Un plugin de Traefik, le bouncer, applique ces décisions en refusant les adresses bannies.

Extrait de k8s/traefik/values-demo.yaml : CrowdSec pour tous
ports:
  web:
    http:
      middlewares: [traefik-crowdsec@kubernetescrd]
  websecure:
    http:
      middlewares: [traefik-crowdsec@kubernetescrd]

Le middleware est posé sur les points d'entrée de Traefik, pas sur chaque Ingress : toute application publiée est protégée d'office, y compris celle qu'un développeur pressé met en ligne un vendredi soir. Notre page CrowdSec et Wazuh détaille le fonctionnement de l'outil.

07Vérifier le socle

Contrôles après le bootstrap
kubectl get nodes                                  # tous Ready
kubectl -n kube-system exec ds/cilium -- cilium status --brief
kubectl get storageclass                           # ceph-rbd (default)
kubectl -n traefik get svc traefik                 # EXTERNAL-IP 10.20.1.100
kubectl -n crowdsec exec deploy/crowdsec-lapi -- cscli bouncers list

Dernier test, le plus parlant : déployer une petite application avec un Ingress, et vérifier qu'elle répond en HTTPS sur son nom une à deux minutes plus tard.

Le test qui dit tout
curl -sI https://whoami.demo.k8s.exemple.fr | head -1
dig +short whoami.demo.k8s.exemple.fr              # 203.0.113.47

Une plateforme Kubernetes, sans y laisser vos nuits

Nous concevons, montons et exploitons des plateformes Kubernetes sur Proxmox VE pour des équipes qui veulent se consacrer à leur code : mises à jour nocturnes, sauvegardes testées, déploiement en GitOps. Reprise d'un cluster existant, plateforme clé en main ou astreinte : nous intervenons au forfait comme au long cours.