Aller au contenu
English
Cloud medium

Créer un cluster Kapsule, survivre à la perte d'un nœud, et tout détruire

Mesuré live le ·fr-par·scw 2.62.0

80 min de lecture

logo Scaleway

Un cluster Kapsule à trois nœuds est prêt en un peu plus de trois minutes, et une seule commande crée le cluster et son pool. Cette leçon construit ce cluster, y déploie une application exposée par un Load Balancer, puis provoque deux incidents pour mesurer ce qui tient : le drain d'un nœud pendant que le service reçoit du trafic, et le remplacement d'un nœud, dont vous verrez qu'il ne s'obtient pas avec la commande qu'on croit. Elle se termine par une destruction dans l'ordre des dépendances, vérifiée famille par famille. Toutes les durées données ici ont été chronométrées le 12 septembre 2026.

  • Créer un cluster et son pool en une commande, et savoir ce que chaque étape coûte en temps.
  • Prouver qu'un service survit au drain d'un nœud, chiffres à l'appui.
  • Mesurer la fenêtre de capacité réduite pendant le remplacement d'un nœud.
  • Détruire dans l'ordre qui ne laisse aucune ressource orpheline.
  • Un compte Scaleway avec un moyen de paiement validé et un projet dédié au lab.
  • La CLI scw 2.62.0 ou plus récente, sur un profil isolé (jamais votre profil courant).
  • kubectl installé localement, et jq pour lire les sorties JSON.
  • Les bases de Kubernetes, Pods, Deployments et Services : ce lab les manipule sans les expliquer, la formation Kubernetes du site s'en charge.
  • La lecture de Kubernetes managé sur Scaleway, qui pose les décisions irréversibles de ce volet.

Comment créer un cluster Kapsule en une commande ?

Section intitulée « Comment créer un cluster Kapsule en une commande ? »

Une seule commande crée le cluster et son premier pool, parce que scw k8s cluster create accepte des arguments pools.0.*. Créer le cluster puis le pool séparément fonctionne aussi, mais laisse un cluster sans nœud pendant l'intervalle, ce qui n'apporte rien.

  1. Créer le cluster et son pool, en capturant l'identifiant.

    Fenêtre de terminal
    CLUSTER_ID=$(scw k8s cluster create \
    name=demo-kapsule \
    region=fr-par \
    version=1.37.0 \
    cni=cilium \
    pools.0.name=demo-pool \
    pools.0.node-type=DEV1-M \
    pools.0.size=3 \
    pools.0.zone=fr-par-1 \
    -o json | jq -r '.id')
    echo "cluster : ${CLUSTER_ID}"

    La sortie doit afficher un identifiant UUID. La commande rend la main en 2 secondes : l'API accepte la demande et provisionne en arrière-plan.

  2. Attendre que le control plane réponde.

    Fenêtre de terminal
    until [ "$(scw k8s cluster get "${CLUSTER_ID}" region=fr-par -o json \
    | jq -r '.status')" = "ready" ]; do printf '.'; sleep 10; done
    echo " control plane prêt"

    La sortie doit finir par control plane prêt. Mesuré le 2026-09-12 : 106 secondes.

  3. Installer le kubeconfig, sans quoi kubectl ne sait pas à qui parler.

    Fenêtre de terminal
    scw k8s kubeconfig install "${CLUSTER_ID}" region=fr-par
    kubectl config current-context

    La sortie doit afficher demo-kapsule- suivi de l'identifiant du cluster. Le contexte porte le nom du cluster et son identifiant, et install le rend courant d'office : il n'y a rien à basculer à la main. Un kubectl config use-context admin@demo-kapsule, calqué sur d'autres fournisseurs, échouerait avec no context exists with the name. La commande fusionne la configuration dans votre ~/.kube/config existant, elle ne l'écrase pas.

  4. Attendre les trois nœuds, en comparant un champ plutôt qu'en cherchant une chaîne.

    Fenêtre de terminal
    until [ "$(kubectl get nodes --no-headers 2>/dev/null \
    | awk '$2=="Ready"' | wc -l)" = "3" ]; do printf '.'; sleep 10; done
    kubectl get nodes

    La sortie doit afficher trois nœuds Ready en version v1.37.0. Mesuré : 109 secondes de plus, soit un peu plus de trois minutes depuis la première commande.

Pourquoi certains types de nœuds sont-ils refusés ?

Section intitulée « Pourquoi certains types de nœuds sont-ils refusés ? »

Kapsule refuse les types d'Instance trop petits, et la liste n'est pas dans la documentation produit : elle est dans l'aide de la commande. scw k8s pool create -h la donne noir sur blanc :

node-type : Nodes with insufficient memory are not eligible (DEV1-S, PLAY2-PICO, STARDUST)

La mémoire est le seul critère d'exclusion, et il vaut la peine de le vérifier plutôt que de l'étendre. On lit souvent que le volume système disqualifierait DEV1-S une seconde fois, puisque la FAQ Kubernetes recommande 20 Go minimum. L'API dit autre chose :

Fenêtre de terminal
scw instance server-type list zone=fr-par-1 -o json \
| jq -r '.[] | select(.name | startswith("DEV1"))
| "\(.name) volume local max : \(.local_volume_max_size / 1000000000) Go"'

La sortie doit afficher 20 Go pour DEV1-S et 40 Go pour DEV1-M. DEV1-S est donc exactement au minimum recommandé, pas en dessous : l'argument du volume ne tient pas, seule la mémoire l'exclut.

Attention à l'unité en lisant ce champ. Scaleway compte en Go décimaux, pas en Gio : diviser par 2^30 donne 18 et 37, des valeurs justes mais dans une autre unité, qu'on publie alors à tort comme des Go. C'est une erreur facile, et elle change la conclusion.

Vérifiez la disponibilité réelle avant de choisir, car un type au catalogue peut être en pénurie :

Fenêtre de terminal
scw instance server-type list zone=fr-par-1 -o json \
| jq -r '.[] | select(.name | startswith("DEV1") or startswith("GP1"))
| "\(.name) \(.availability) \(.ram / 1073741824 | floor) Go"'

La sortie doit afficher available sur les gammes DEV1 et GP1. Un type en scarce ou shortage peut faire échouer la création : les gammes PLAY2 et PRO2 sortaient en pénurie le 2026-09-12.

DEV1-M est le plus petit type retenu pour ce lab : 3 vCPU, 4 Go, 0,020196 €/h, volume système en Local Storage donc sans Block Storage facturé en plus.

Un Service de type LoadBalancer fait naître un vrai Load Balancer facturé, que rien dans votre manifeste ne nomme. C'est le cloud controller de Scaleway qui le provisionne, et c'est ce qui rend l'ordre de destruction si important.

Déployez une application sur trois répliques, réparties entre les nœuds :

Fenêtre de terminal
cat > app.yaml <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
spec:
replicas: 3
selector:
matchLabels: { app: demo }
template:
metadata:
labels: { app: demo }
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: demo }
containers:
- name: web
image: nginx:1.29.1@sha256:8adbdcb969e2676478ee2c7ad333956f0c8e0e4c5a7463f4611d7a2e7a7ff5dc
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: demo
annotations:
# Sans cette annotation, le CCM renseigne une adresse IP et non un nom.
service.beta.kubernetes.io/scw-loadbalancer-use-hostname: "true"
spec:
type: LoadBalancer
selector: { app: demo }
ports:
- port: 80
targetPort: 80
YAML
kubectl apply -f app.yaml
kubectl rollout status deployment/demo --timeout=180s

La sortie doit finir par deployment "demo" successfully rolled out. Mesuré le 2026-09-12 : 7 secondes, l'image étant déjà en cache sur les nœuds.

Récupérez ensuite l'adresse attribuée par le Load Balancer :

Fenêtre de terminal
until [ -n "$(kubectl get svc demo -o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}')" ]; do
printf '.'; sleep 5
done
ADRESSE=$(kubectl get svc demo \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}')
echo "adresse : ${ADRESSE}"

La sortie doit afficher une adresse en .lb.fr-par.scw.cloud. Mesuré : 10 secondes après l'application du manifeste.

C'est l'annotation du manifeste qui produit ce nom. Par défaut, le cloud controller renseigne status.loadBalancer.ingress[0].ip, donc une adresse IP brute ; service.beta.kubernetes.io/scw-loadbalancer-use-hostname: "true" lui fait renseigner un nom à la place. Préférez le nom : une adresse IP de Load Balancer se recycle, un enregistrement DNS qui la cite en dur finit par pointer ailleurs.

Oui, et la preuve se fait en envoyant du trafic pendant l'opération plutôt qu'en le supposant. Lancez la boucle de mesure, puis drainez un nœud pendant qu'elle tourne.

  1. Démarrer la mesure dans un terminal, sur 60 requêtes.

    Fenêtre de terminal
    for i in $(seq 1 60); do
    curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 "http://${ADRESSE}/"
    sleep 1
    done | sort | uniq -c
  2. Drainer un nœud dans un second terminal.

    Fenêtre de terminal
    NOEUD=$(kubectl get nodes --no-headers | awk '$2=="Ready"{print $1; exit}')
    kubectl drain "${NOEUD}" --ignore-daemonsets --delete-emptydir-data --timeout=120s

    La sortie doit afficher node/... drained. Résultat mesuré le 2026-09-12 : 60 requêtes, 60 codes 200, aucune coupure.

  3. Remettre le nœud en service.

    Fenêtre de terminal
    kubectl uncordon "${NOEUD}"
    kubectl get nodes

    La sortie ne doit plus afficher SchedulingDisabled.

drain refuse de partir sans --ignore-daemonsets, et il nomme exactement ce que Scaleway injecte :

ignoring DaemonSet-managed Pods: kube-system/cilium-xxxxx,
kube-system/csi-node-xxxxx, kube-system/konnectivity-agent-xxxxx

Ces trois-là appartiennent au nœud, pas à votre application : cilium porte le réseau, csi-node attache les volumes Block, konnectivity-agent maintient le lien avec un control plane qui vit ailleurs. Les évincer n'aurait aucun sens, puisqu'ils seraient recréés immédiatement sur le même nœud.

Lisez ce tableau par la colonne du milieu : elle dit dans quelle situation chaque réglage est le bon, et aucun des deux n'est meilleur dans l'absolu. Le choix dépend du nombre de nœuds de marge dont vous disposez, pas d'une bonne pratique générale.

RéglageQuand le choisirCe que vous acceptez en échange
whenUnsatisfiable: ScheduleAnywaycluster sans marge, moins de nœuds que de répliques plus unune répartition temporairement déséquilibrée après un incident
whenUnsatisfiable: DoNotSchedulecluster avec au moins un nœud de margedes pods Pending dès que la contrainte devient insatisfiable
DoNotSchedule + PodDisruptionBudgetproduction, charge qui ne tolère pas le déséquilibreun drain qui bloque plutôt que de dégrader la répartition

Le troisième cas est celui qu'on vise en production, et il change la nature de l'incident : au lieu de laisser un pod Pending en silence, le PodDisruptionBudget fait échouer le drain, ce qui force une décision humaine au lieu d'une dégradation invisible.

Supprimer un nœud ou le remplacer : deux commandes, deux effets

Section intitulée « Supprimer un nœud ou le remplacer : deux commandes, deux effets »

scw k8s node delete ne remplace rien : il retire le nœud et réduit le pool de 1. C'est scw k8s node replace qui remplace. La confusion est facile, parce que l'aide de delete porte encore une ligne d'argument périmée, « node-id : ID of the node to replace », qui contredit sa propre description.

Lisez la première phrase de l'aide, celle qui décrit la commande :

Fenêtre de terminal
scw k8s node delete -h | head -1

La sortie doit afficher Delete a specific Node. Pool size is reduced by 1.

Mesuré le 2026-09-12 sur un cluster 1.37.0 à trois nœuds DEV1-M, en relevant la taille du pool avant et après, ce qui est le seul relevé qui tranche :

OpérationPool avantPool aprèsAncien sorti de l'API3 nœuds ReadyRemplacement
scw k8s node delete3210 sjamais en 600 snon
scw k8s node replace3311 s125 soui

Les dix premières secondes sont identiques, et c'est ce qui rend l'erreur si facile : les deux commandes drainent le nœud et le sortent de l'API Kubernetes au même rythme. La divergence n'apparaît qu'ensuite, quand l'une provisionne un remplaçant et l'autre vous laisse avec un nœud de moins.

Fenêtre de terminal
POOL_ID=$(scw k8s pool list cluster-id="${CLUSTER_ID}" region=fr-par -o json | jq -r '.[0].id')
NODE_ID=$(scw k8s node list cluster-id="${CLUSTER_ID}" region=fr-par -o json | jq -r '.[0].id')
scw k8s node replace node-id="${NODE_ID}" region=fr-par -o json | jq -r '.status'
scw k8s pool get "${POOL_ID}" region=fr-par -o json | jq -r '"pool à \(.size) nœud(s)"'

La première sortie doit afficher deleting, et la seconde pool à 3 nœud(s) : le pool ne bouge pas avec replace. Avec delete, la même lecture afficherait pool à 2 nœud(s).

Le remplacement complet a pris 125 secondes, dont une centaine où le cluster tourne avec un nœud de moins. C'est cette fenêtre qui décide du dimensionnement.

Le choix entre les deux commandes est en réalité un choix d'exploitation, et il mérite d'être fait consciemment plutôt que subi. replace est l'outil du remplacement planifié : renouveler une machine, sortir un nœud suspect, appliquer une image système plus récente, sans jamais perdre de capacité durable. delete est l'outil de la réduction volontaire : vous décidez que le pool est trop grand et vous l'assumez. Les employer l'un pour l'autre produit deux incidents opposés. Un delete pris pour un remplacement laisse silencieusement un cluster sous-dimensionné, et personne ne le remarque tant que la charge reste modérée ; un replace appliqué là où l'on voulait réduire fait revenir un nœud qu'on croyait supprimé, et la facture avec lui. Dans les deux cas, la seule vérification qui tranche est la même : relire la taille du pool après l'opération.

Le plafond est de 150 nœuds sur un control plane mutualisé, et l'API le dit elle-même sans rien provisionner. Provoquez le refus, c'est gratuit et instructif :

Fenêtre de terminal
POOL_ID=$(scw k8s pool list cluster-id="${CLUSTER_ID}" region=fr-par -o json | jq -r '.[0].id')
scw k8s pool update "${POOL_ID}" region=fr-par size=200 -o json

La sortie doit afficher un refus contenant :

a kapsule cluster can't have more than 150 nodes

Rien n'a été créé, rien n'a été facturé, le pool n'a pas bougé. C'est la signature d'une contrainte structurelle du produit, refusée à la validation des arguments, et non d'un quota de compte qui se négocierait avec le support. La distinction compte : une contrainte produit impose de passer à un control plane dédié ou de répartir sur plusieurs clusters.

L'ordre n'est pas arbitraire et ne se devine pas : le Service part en premier, sinon le Load Balancer devient orphelin. Supprimer le cluster d'abord laisse un Load Balancer sans personne pour le réclamer, et sa facturation continue.

  1. Supprimer le Service, pour que le cloud controller reprenne son Load Balancer.

    Fenêtre de terminal
    kubectl delete -f app.yaml --ignore-not-found=true
    until [ "$(scw lb lb list zone=fr-par-1 -o json | jq -r 'length')" = "0" ]; do
    printf '.'; sleep 10
    done
    echo " load balancer libéré"

    La suppression est asynchrone : on attend la disparition effective, on ne la suppose pas.

  2. Supprimer le cluster, avec ses ressources associées.

    Fenêtre de terminal
    scw k8s cluster delete "${CLUSTER_ID}" region=fr-par \
    with-additional-resources=true -o json | jq -r '.status'

    La sortie doit afficher deleting. L'aide de la commande dit exactement ce que ce drapeau emporte : tous les volumes, y compris ceux de type retain, les Private Networks devenus vides, et les Load Balancers dont le nom commence par l'identifiant du cluster. Sans lui, ces ressources survivent au cluster et se facturent en silence.

  3. Vérifier le retour à zéro, famille par famille.

    Fenêtre de terminal
    for f in "k8s cluster list region=fr-par" "lb lb list zone=fr-par-1" \
    "instance ip list zone=fr-par-1" "block volume list zone=fr-par-1" \
    "block snapshot list zone=fr-par-1" "vpc private-network list region=fr-par"; do
    n=$(eval "scw ${f} -o json" | jq -r 'if type=="array" then length else 0 end')
    printf '%-40s : %s\n' "${f%% *} ${f#* }" "${n}"
    done

    Chaque ligne doit afficher 0. Une seule valeur non nulle signifie qu'une ressource facturée a survécu.

Ces chiffres décident du dimensionnement bien avant la première commande. Ils viennent de la documentation produit, de la page des quotas, ou d'un relevé daté sur ce lab.

PlafondValeurSource
Nœuds par cluster, control plane mutualisé150refus de l'API, 2026-09-12
Taille d'etcd, control plane mutualisé55 Modoc des offres de control plane
Clusters Kapsule par Organisation20, ou 40 identité validéepage des quotas
Volume système recommandé20 Go minimum, 100 Go confortableFAQ Kubernetes
Volume local maximum sur DEV1-M40 Go décimauxscw instance server-type list, 2026-09-12
Volume local maximum sur DEV1-S20 Go décimaux, soit le minimum recommandéscw instance server-type list, 2026-09-12
Types refusés par KapsuleDEV1-S, PLAY2-PICO, STARDUSTaide de scw k8s pool create

Ces quatre symptômes couvrent l'essentiel de ce qui bloque un premier cluster. Les deux premiers portent le message exact tel que l'API ou kubectl le renvoient.

SymptômeCauseSolution
a kapsule cluster can't have more than 150 nodescontrainte produit du control plane mutualisépasser en control plane dédié, ou répartir sur plusieurs clusters
Le pool a un nœud de moins après un scw k8s node deletedelete réduit le pool de 1, il ne remplace pasemployer scw k8s node replace, ou remonter la taille du pool
Un pod reste Pending après un draintopologySpreadConstraints en DoNotSchedule devient insatisfiablepasser à ScheduleAnyway, ou ajouter un nœud de marge
L'aide de delete dit « ID of the node to replace »ligne d'argument périmée, contredite par la description de la commandelire la première phrase : « Pool size is reduced by 1 »
Le curl échoue alors que l'adresse existeles backends n'ont pas fini leurs contrôles de santéattendre une réponse HTTP, pas l'attribution de l'adresse

Ces quatre erreurs traitent un cluster managé comme un cluster qu'on possède. Kapsule remplace, redémarre et supprime les nœuds quand il le juge utile : toute pratique qui suppose un nœud stable finit par échouer.

AntipatternConséquenceDiscipline
Corriger un nœud en SSHla correction disparaît au premier remplacementtout passer par l'API Kubernetes, et désactiver le serveur SSH des nœuds
Attendre une adresse plutôt qu'une réponsele déploiement se déclare fini avant que le service ne répondesonder l'application en HTTP jusqu'au premier code 200
Supprimer le cluster avant le Servicele Load Balancer devient orphelin et continue d'être facturésupprimer le Service, attendre, puis le cluster
Conclure d'un code de retour que tout est détruitune suppression asynchrone rend 0 sans avoir aboutirelister chaque famille jusqu'à obtenir zéro

Les trois piliers posent les questions qu'un auditeur poserait après avoir lu ce lab.

Combien de temps le cluster fonctionne-t-il en capacité réduite ? La mesure de cette leçon répond : environ 115 secondes lors d'un node replace, entre la sortie de l'ancien à la 10e seconde et le retour à trois nœuds à la 125e. Et indéfiniment après un node delete, qui ne remplace rien.

Discipline : dimensionner pour N-1 nœuds, poser un PodDisruptionBudget sur toute charge qui compte, et préférer ScheduleAnyway tant qu'il n'y a pas de nœud de marge.

Vos scripts d'attente mesurent-ils l'état que vous croyez ? Ce lab a produit deux chiffres faux à la suite avant de produire le bon, sans jamais lever d'erreur.

Discipline : comparer un champ plutôt que chercher une sous-chaîne, attendre la disparition d'une identité plutôt que le bon compte, et se méfier d'une mesure qui tombe juste du premier coup.

Qui paiera le Load Balancer que personne n'a créé à la main ? Un Service de type LoadBalancer en fait provisionner un vrai, facturé à l'heure, absent de tous vos manifestes.

Discipline : supprimer les Service avant le cluster, employer with-additional-resources=true, et terminer par une vérification de sortie sur les six familles que liste la boucle de nettoyage de cette page.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Une seule commande suffit : scw k8s cluster create accepte des arguments pools.0.* pour créer le pool dans la foulée.
  • Un cluster à trois nœuds est prêt en un peu plus de trois minutes : 106 s pour le control plane, 109 s de plus pour les nœuds.
  • scw k8s kubeconfig install fusionne la configuration dans votre ~/.kube/config, il ne l'écrase pas.
  • DEV1-S, PLAY2-PICO et STARDUST sont refusés par Kapsule, faute de mémoire suffisante.
  • drain exige --ignore-daemonsets et cite cilium, csi-node et konnectivity-agent, qui appartiennent au nœud.
  • scw k8s node replace remplace en 125 secondes et laisse le pool à sa taille ; node delete réduit le pool de 1 et ne remplace jamais.
  • Un pool créé par cluster create sort avec autoscaling et autohealing à false : aucun automatisme sans le demander.
  • Le plafond de 150 nœuds se vérifie gratuitement, l'API refusant à la validation des arguments.
  • Le Service se supprime avant le cluster, sinon le Load Balancer reste orphelin et facturé.
  • Comparer un champ avec awk '$2=="Ready"', jamais chercher Ready avec grep, qui attrape aussi NotReady.
  • Kosmos : Explique ce que le même control plane devient quand les nœuds ne sont plus chez Scaleway.
  • Volumes persistants : Ajoute l'état à ce cluster, et montre ce que le remplacement d'un nœud lui fait.
  • Monter un cluster de version : Reprend ce cluster et le fait changer de version pendant qu'il reçoit du trafic.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn