Publié le: 2026-09-02

Changer le PodCIDR de Cilium à chaud

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.

Configuration de Cilium

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.

Principe du rollout

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.

Le script

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 "========================================"

Exécution du rollout

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.

Exemple de sortie

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.

Conclusion

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.