Quand on utilise Cilium en mode ipam: cluster-pool, c’est l’opérateur Cilium qui gère
lui-même un pool d’adresses global et qui découpe ce pool en blocs PodCIDR attribués à
chaque node via l’objet CiliumNode. Le mask appliqué à chaque bloc détermine directement
combien de pods peuvent tourner simultanément sur le node. Par défaut ce mask vaut /24, ce
qui autorise 254 IPs par node.
Sur des nodes costauds (beaucoup de CPU/RAM) cette limite est rapidement atteinte: il devient
alors impossible de scheduler de nouveaux pods tant que les pods existants n’ont pas été
reprogrammés ailleurs. La parade consiste à augmenter la taille du CIDR alloué par node (par
exemple passer à un /23 ou un /22) et à déployer le changement à chaud, node par node.
Le mask appliqué par défaut à chaque node se configure via la Helm values de Cilium, et plus
précisément via les paramètres ipam.operator.clusterPoolIPv4Mask et le pool
ipam.operator.clusterPoolIPv4PodCIDRList. Voici la configuration que j’utilise pour passer
d’un /24 à un /22 (1022 IPs par node):
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4Mask: "22"
clusterPoolIPv4PodCIDRList:
- 172.16.0.0/16
Si vous avez des CiliumNode existants avec un mask différent alloué via
spec.ipam.ipamConfig, l’opérateur respectera cet override et n’allouera pas un nouveau bloc
avec le mask par défaut tant que l’override reste présent. Il faut donc soit supprimer ces
overrides, soit prévoir un ipamConfig.podCIDR par node cohérent avec la nouvelle taille.
Attention, modifier clusterPoolIPv4Mask ne réalloue pas automatiquement les CIDRs déjà
attribués aux nodes existants: ces nodes conservent leur ancien bloc. Il faut donc procéder
à un rollout à chaud, node par node, et forcer la réallocation du PodCIDR pour chaque
node.
L’idée est de forcer, pour chaque node, la réallocation de son PodCIDR en supprimant la
première entrée du tableau spec.ipam.podCIDRs du CiliumNode. L’opérateur Cilium voit qu’il
n’y a plus de CIDR alloué et en réattribue un nouveau issu du pool, découpé selon le nouveau
mask configuré.
C’est précisément pour cela que le mode cluster-pool se prête bien à cette opération:
contrairement au mode kubernetes où le CIDR est alloué par kube-controller-manager sur
l’objet Node (et où Cilium n’a pas la main pour le réallouer de lui-même), ici tout est piloté
par l’opérateur Cilium au travers du CiliumNode, et la réallocation est immédiate.
Une fois le nouveau CIDR alloué, il reste à supprimer les pods qui possèdent encore une IP
dans l’ancien bloc (sinon Cilium ne pourra pas les reprogrammer tant qu’ils existent dans son
état), ainsi qu’à redémarrer le pod cilium lui-même pour qu’il recharge la nouvelle
configuration IPAM. On draine donc le node, on patche le CiliumNode, on supprime le pod
Cilium, on supprime les pods résiduels sur l’ancien bloc, puis on uncordonne.
L’opération est répétée pour chaque node un par un, ce qui permet de garder le cluster entièrement fonctionnel pendant le rollout.
J’utilise ce script, à exécuter sur un poste ayant kubectl et jq configurés. Il prend en
argument le nom du node à traiter:
#!/usr/bin/env bash
set -uo pipefail
NODE="${1:-}"
if [[ -z "$NODE" ]]; then
echo "Usage: $0 <node-name>"
exit 1
fi
echo "========================================"
echo "Target node: ${NODE}"
echo "========================================"
#
# 1. CORDON
#
echo
echo "[1/6] Cordoning node..."
if ! kubectl cordon "$NODE"; then
echo "ERROR: Failed to cordon node ${NODE}"
exit 1
fi
#
# 2. DRAIN
#
echo
echo "[2/6] Draining node..."
if ! kubectl drain "$NODE" \
--ignore-daemonsets \
--delete-emptydir-data \
--timeout=5m
then
echo "ERROR: Failed to drain node ${NODE}"
exit 1
fi
#
# 3. REMOVE FIRST POD CIDR
#
echo
echo "[3/6] Updating CiliumNode/${NODE}..."
POD_CIDR=$(kubectl get ciliumnode "$NODE" \
-o jsonpath='{.spec.ipam.podCIDRs[0]}' 2>/dev/null)
if [[ -z "$POD_CIDR" ]]; then
echo "ERROR: No podCIDR found in CiliumNode/${NODE}"
exit 1
fi
echo "Current first podCIDR: ${POD_CIDR}"
if ! kubectl patch ciliumnode "$NODE" \
--type='json' \
-p='[{"op":"remove","path":"/spec/ipam/podCIDRs/0"}]'
then
echo "ERROR: Failed to patch CiliumNode/${NODE}"
exit 1
fi
#
# 4. DISPLAY NEW POD CIDR
#
echo
echo "[4/6] Waiting 1 second..."
sleep 1
NEW_POD_CIDR=$(kubectl get ciliumnode "$NODE" \
-o jsonpath='{.spec.ipam.podCIDRs[0]}' 2>/dev/null)
echo "New first podCIDR: ${NEW_POD_CIDR:-<none>}"
#
# 5. DELETE CILIUM POD
#
echo
echo "[5/6] Deleting Cilium pod on ${NODE}..."
CILIUM_POD=$(kubectl get pods \
-n kube-system \
--field-selector "spec.nodeName=${NODE}" \
-l k8s-app=cilium \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [[ -n "$CILIUM_POD" ]]; then
echo "Deleting Cilium pod: ${CILIUM_POD}"
if ! kubectl delete pod "$CILIUM_POD" -n kube-system; then
echo "WARNING: Failed to delete Cilium pod"
fi
else
echo "WARNING: No Cilium pod found on ${NODE}"
fi
#
# 6. DELETE PODS WITH 172.x.x.x IP
#
echo
echo "[6/6] Finding pods with IP starting with 172..."
PODS=$(kubectl get pods \
--all-namespaces \
--field-selector "spec.nodeName=${NODE}" \
-o json 2>/dev/null)
if [[ -z "$PODS" ]]; then
echo "ERROR: Failed to retrieve pods from node ${NODE}"
exit 1
fi
echo "$PODS" |
jq -r '
.items[]
| select(.status.podIP != null)
| select(.status.podIP | startswith("172"))
| [
.metadata.namespace,
.metadata.name,
.status.podIP
]
| @tsv
' |
while IFS=$'\t' read -r NAMESPACE POD IP; do
echo "Deleting ${NAMESPACE}/${POD} (${IP})"
if ! kubectl delete pod "$POD" -n "$NAMESPACE"; then
echo "WARNING: Failed to delete ${NAMESPACE}/${POD}"
fi
done
echo "Uncordon ${NODE}"
kubectl uncordon ${NODE}
echo
echo "========================================"
echo "Script completed"
echo "========================================"
Une fois le script enregistré en /tmp/cilium_host_rollout.sh, il suffit de le lancer sur
chaque node, l’un après l’autre:
for NODE in $(kubectl get node | awk '{print $1}'); do /tmp/cilium_host_rollout.sh ${NODE}; done
La boucle traite un node à la fois et ne passe au suivant qu’une fois le script rendu. Vous pouvez évidemment retirer un node de la liste (control plane, nodes en maintenance, etc.) en filtrant sur les labels qui vont bien.
Sur un node kube-node801, on observe la transition d’un ancien /26 vers le nouveau /24
alloué par l’opérateur:
========================================
Target node: kube-node801
========================================
[1/6] Cordoning node...
node/kube-node801 cordoned
[2/6] Draining node...
node/kube-node801 already cordoned
Warning: ignoring DaemonSet-managed Pods: monitoring/node-exporter-m86h7, kube-system/cilium-bs7j6, logging/fluent-bit-rbtv7
evicting pod database/postgres-vacuum-cron-29802720-n4r6m
evicting pod database/postgres-vacuum-cron-29782560-czmj5
evicting pod database/postgres-vacuum-cron-29792640-p887q
pod/postgres-vacuum-cron-29782560-czmj5 evicted
pod/postgres-vacuum-cron-29802720-n4r6m evicted
pod/postgres-vacuum-cron-29792640-p887q evicted
node/kube-node801 drained
[3/6] Updating CiliumNode/kube-node801...
Current first podCIDR: 172.20.84.192/26
ciliumnode.cilium.io/kube-node801 patched
[4/6] Waiting 1 second...
New first podCIDR: 172.16.28.0/24
[5/6] Deleting Cilium pod on kube-node801...
Deleting Cilium pod: cilium-bs7j6
pod "cilium-bs7j6" deleted from kube-system namespace
[6/6] Finding pods with IP starting with 172...
Deleting monitoring/node-exporter-m86h7 (172.20.84.227)
pod "node-exporter-m86h7" deleted from monitoring namespace
Deleting logging/fluent-bit-rbtv7 (172.20.84.194)
pod "fluent-bit-rbtv7" deleted from logging namespace
Uncordon kube-node801
node/kube-node801 uncordoned
Le drain ré-évince les pods non DaemonSet, le DaemonSet Cilium est ensuite supprimé manuellement
pour forcer la reprise du nouvel état IPAM, et enfin les pods résiduels (typiquement les
autres DaemonSets comme node-exporter ou fluent-bit) dont l’IP appartient encore à l’ancien
bloc sont supprimés afin que Cilium puisse leur réattribuer une IP dans le nouveau bloc au
prochain scheduling.
Cette procédure permet de basculer un cluster Cilium en mode IPAM cluster-pool d’un mask
PodCIDR à un autre sans interruption de service globale. En traitant les nodes un par un et
en jouant sur le cordon/drain, seul le node en cours de migration perd momentanément ses
pods applicatifs, ce qui est largement acceptable sur la plupart des clusters de production
dès lors qu’on dispose d’au moins deux réplicas par workload critique.