
Dans Kubernetes, le modèle déclaratif repose sur une promesse simple : vous décrivez l’état souhaité, la plateforme se charge de le rendre vrai et de le maintenir dans le temps. Les contrôleurs Kubernetes sont au cœur de ce mécanisme. Ils observent l’état du cluster, détectent les écarts avec l’intention déclarée, puis réconcilient le système jusqu’à ce que la réalité corresponde à ce qui a été demandé.
À petite échelle, écrire une boucle de réconciliation correcte semble souvent suffisant. À grande échelle, le problème change de nature. Les caches peuvent être incomplets, les pods apparaissent et disparaissent en continu, les objets Kubernetes évoluent rapidement, et certaines décisions ne peuvent pas être corrigées simplement au cycle de réconciliation suivant. Dans ces conditions, la difficulté n’est plus seulement de « faire fonctionner » un contrôleur, mais de garantir que l’intention reste correctement appliquée malgré le volume, la latence et les états transitoires.
Les enseignements tirés de l’exploitation de contrôleurs critiques dans Amazon EKS illustrent bien cette réalité. Deux cas sont particulièrement parlants : l’application des politiques réseau à l’intérieur du cluster et l’attribution de groupes de sécurité AWS directement à certains pods. Dans les deux cas, Kubernetes reçoit une intention de haut niveau, mais l’exécution réelle dépend de règles concrètes appliquées au niveau du datapath, du noyau Linux ou de l’infrastructure cloud.
Pourquoi les contrôleurs deviennent critiques dans Kubernetes
Kubernetes fournit la connectivité de base entre les pods. En revanche, il ne décide pas automatiquement quels services doivent pouvoir communiquer entre eux, ni quels workloads doivent être autorisés à accéder à une base de données ou à un service cloud externe. Cette logique de sécurité fine doit être exprimée, traduite, puis appliquée par des composants spécialisés.
Dans un environnement de test, une connectivité largement ouverte peut sembler pratique. En production, elle devient rapidement un risque. Une équipe veut par exemple pouvoir déclarer qu’un pod portant le label app=web ne peut recevoir du trafic que depuis des pods app=api, ou qu’un seul workload doit pouvoir accéder à une base RDS. Cette intention doit rester valable même lorsque des pods sont recréés, déplacés ou mis à l’échelle plusieurs fois par jour.
Dans Amazon EKS, deux contrôleurs répondent à ce type de besoin :
- Network Policy Controller : il gère l’application des politiques réseau à l’intérieur du cluster, en décidant quels pods peuvent communiquer entre eux.
- VPC Resource Controller : il permet à certains pods d’obtenir leur propre identité réseau côté AWS, notamment via des security groups dédiés.
Ces composants ne sont pas magiques. Ce sont des contrôleurs Kubernetes classiques : ils regardent des objets, conservent un état en mémoire et réconcilient ce qu’ils observent avec ce qui a été déclaré. Mais à l’échelle de milliers de nœuds et de dizaines de milliers de pods, chaque choix de conception devient déterminant.
Traduire une intention réseau en règles réellement applicables
Une NetworkPolicy Kubernetes exprime une règle en termes de labels. Par exemple : les pods app=web peuvent accepter du trafic uniquement depuis les pods app=api. C’est une abstraction très utile, car les labels restent stables même lorsque les pods changent d’adresse IP ou sont recréés.
Mais le datapath, lui, n’applique pas des labels. Il travaille avec des éléments concrets, comme des adresses IP de pods, des ports et des règles d’autorisation. Il faut donc résoudre en permanence les sélecteurs de labels vers l’ensemble actuel des adresses IP concernées.

Résoudre les sélecteurs au bon endroit
Un choix d’architecture important consiste à décider où cette résolution doit avoir lieu. Une approche possible consiste à laisser chaque nœud observer tous les pods, namespaces et NetworkPolicies, puis calculer localement les règles à appliquer. Cette méthode fonctionne, mais elle peut augmenter fortement la charge sur l’API server et dupliquer beaucoup de travail.
Dans l’approche décrite pour EKS, la résolution est centralisée. Le Network Policy Controller surveille les NetworkPolicies ainsi que les objets dont elles dépendent : pods, namespaces et services. Il calcule les correspondances entre labels et adresses IP, puis écrit le résultat dans une ressource qu’il possède, appelée PolicyEndpoint.
Lorsque beaucoup de pods correspondent à une même politique, ou lorsque les pods changent fréquemment, le résultat peut être réparti sur plusieurs PolicyEndpoints. Cela évite de faire grossir excessivement un seul objet ou de le réécrire trop souvent. Les agents présents sur les nœuds n’ont alors plus besoin de recalculer l’état global du cluster : ils lisent uniquement les PolicyEndpoints utiles pour les pods locaux.
Appliquer les règles dans le noyau avec eBPF
Une fois les décisions prises par le contrôleur, le travail descend au niveau des nœuds. Dans EKS, le VPC CNI s’exécute sur chaque nœud via le DaemonSet aws-node, accompagné d’un agent de politique réseau. Cet agent ne réécrit pas l’état dans le plan de contrôle : il applique localement ce que le contrôleur a décidé.
L’application des règles se fait dans le noyau Linux avec eBPF, plutôt qu’au moyen de longues chaînes iptables. L’agent programme les adresses autorisées dans des maps eBPF. Ensuite, chaque paquet entrant ou sortant d’un pod peut être évalué directement dans le datapath, avec une décision d’autorisation ou de rejet.
Cette séparation est essentielle : le contrôleur décide, le nœud applique. Elle limite les interactions inutiles avec le plan de contrôle et réduit les risques de boucles complexes entre observation, décision et enforcement.
Donner une identité firewall à un pod avec les security groups
Le second cas concerne l’accès des pods aux ressources AWS externes, comme RDS, ElastiCache ou OpenSearch. Par défaut, un pod n’a pas son propre security group. Il hérite indirectement de celui du nœud sur lequel il s’exécute.
Ce modèle pose un problème de précision. Si une base RDS autorise le security group du nœud pour permettre l’accès à un pod précis, elle autorise en réalité potentiellement tous les pods qui tournent sur ce même nœud. Pour des environnements de production, ce niveau de granularité est souvent insuffisant.
Avec le VPC Resource Controller, certains pods peuvent recevoir leur propre security group. Une règle RDS peut alors autoriser précisément le workload concerné, et non l’ensemble du nœud.

Le rôle de la SecurityGroupPolicy
Pour attribuer un security group à un pod, l’utilisateur crée une SecurityGroupPolicy. Celle-ci cible des pods via des labels et indique quel security group doit leur être associé.
Mais un security group AWS ne s’attache pas directement à un pod abstrait : il s’attache à une interface réseau. Le pod concerné doit donc disposer de sa propre interface. C’est ici qu’apparaît une contrainte matérielle importante : chaque type d’instance EC2 limite le nombre d’interfaces disponibles pour les pods. Une instance m5.large, par exemple, peut en supporter neuf dans ce contexte.
Le contrôleur expose cette capacité comme une ressource disponible sur chaque nœud. Lorsqu’un pod correspond à une SecurityGroupPolicy, le contrôleur modifie sa demande pour inclure le besoin d’une interface réseau. Le scheduler Kubernetes peut alors traiter cette contrainte comme il le ferait pour le CPU ou la mémoire : le pod n’est planifié que sur un nœud disposant encore d’une interface disponible.
Une isolation plus fine pour les accès cloud
Une fois le pod planifié, le contrôleur appelle l’API EC2 pour créer ou assigner une interface réseau au pod sur le nœud concerné. Il applique ensuite les security groups spécifiés à cette interface. Le résultat est une identité réseau beaucoup plus précise.
Dans un cas concret, une règle entrante sur une base RDS peut autoriser uniquement le security group attaché au pod applicatif qui doit s’y connecter. Les autres pods présents sur le même nœud ne bénéficient pas automatiquement de cette autorisation.
Pour les équipes sécurité, c’est une différence majeure : l’intention « seul ce service peut accéder à cette base » n’est plus diluée par le placement dynamique des pods.
Ce qui change quand le cluster atteint des milliers de nœuds
À petite échelle, un contrôleur peut se permettre de conserver beaucoup d’informations en mémoire, de recalculer largement ou de tolérer certains délais de synchronisation. À grande échelle, ces choix deviennent coûteux.
Un informer Kubernetes conserve localement une copie des objets qu’il surveille, afin que la réconciliation n’ait pas besoin d’interroger l’API server à chaque étape. Par défaut, cette copie correspond à l’objet complet : spécification du pod, conteneurs, variables d’environnement, annotations, métadonnées et autres champs.
Un seul pod mis en cache peut représenter plusieurs dizaines de kilo-octets. Sur un cluster comptant des dizaines de milliers de pods, cela peut vite se transformer en centaines de mégaoctets de données dont le contrôleur n’a parfois pas besoin.
Réduire le cache aux champs réellement utilisés
La leçon opérationnelle est simple : un contrôleur ne devrait pas nécessairement cacher tout ce qu’il observe. Il doit d’abord identifier les champs réellement nécessaires à sa logique de réconciliation.
Le VPC Resource Controller, par exemple, n’a besoin que d’un sous-ensemble d’informations sur les pods : nom du nœud, namespace, UID et certaines annotations spécifiques. Le Network Policy Controller a besoin d’autres champs, comme les labels, les adresses IP de pods, les ports de conteneurs et la phase du pod.
En filtrant les objets avant leur mise en cache, ou en utilisant les mécanismes Kubernetes prévus pour transformer les objets entrants, il devient possible de réduire fortement la consommation mémoire. Dans un grand cluster, ce type d’optimisation peut faire passer l’empreinte mémoire de plusieurs centaines de mégaoctets à moins d’une centaine.

Pour concevoir un contrôleur robuste, cette approche peut être résumée ainsi :
- définir précisément les champs lus par la logique métier ;
- supprimer ou ignorer le reste avant la mise en cache lorsque c’est possible ;
- tester la réconciliation avec ces objets réduits, pas seulement avec les objets complets ;
- éviter les transformations coûteuses exécutées sur chaque objet, car une milliseconde ajoutée par objet devient significative à grande échelle.
Le danger des actions irréversibles sur un état incomplet
Les contrôleurs Kubernetes sont généralement tolérants aux états légèrement obsolètes. S’ils prennent une décision imparfaite sur la base d’un cache en retard, le prochain cycle de réconciliation peut souvent corriger la situation.
Mais toutes les actions ne sont pas réversibles. Supprimer une ressource cloud, détacher une interface réseau ou programmer une règle qui coupe du trafic peut avoir un impact immédiat. Dans ces cas, agir sur une vue partielle du monde ne se corrige pas toujours automatiquement.
Le cas des interfaces réseau récupérées trop tôt
Le VPC Resource Controller doit périodiquement rechercher les interfaces qui ne sont plus associées à des pods actifs afin de les récupérer. En régime normal, cette opération est logique : si une interface appartient à un pod supprimé, elle peut être libérée.
Le risque apparaît au démarrage du contrôleur ou lors d’un changement de leader. Les interfaces créées par l’ancienne instance existent toujours sur les nœuds, tandis que la nouvelle instance commence avec un cache vide. Si le nettoyage s’exécute avant que le cache des pods soit entièrement chargé, un pod encore actif peut ne pas être visible. Le contrôleur peut alors conclure à tort que son interface est orpheline et la retirer alors qu’elle transporte du trafic réel.
Le cas des politiques réseau partiellement chargées
Le Network Policy Controller rencontre un problème similaire dans l’autre sens. Les adresses résolues peuvent être réparties sur plusieurs PolicyEndpoints. Si l’agent applique une politique avant d’avoir reçu l’ensemble complet, il peut autoriser temporairement un trafic qui aurait dû être bloqué, ou appliquer une règle incomplète.
La solution est la même dans les deux cas : attendre que tous les caches nécessaires soient complètement synchronisés avant d’exécuter une action irréversible. Le coût est souvent de quelques secondes au démarrage. Le bénéfice est d’éviter des suppressions d’infrastructure, des coupures réseau ou des fenêtres de sécurité incohérentes.
Pour les équipes qui développent leurs propres opérateurs Kubernetes, cette règle mérite d’être traitée comme un principe de conception : lorsqu’une action dépend de plusieurs sources d’état, aucune décision destructive ne doit partir tant que toutes ces sources ne sont pas prêtes.
Quand une optimisation déplace la contrainte au lieu de la supprimer
Les optimisations à grande échelle ont souvent un effet secondaire : elles ne font pas disparaître une limite, elles la déplacent. Le cas de la délégation de préfixes dans EKS l’illustre très bien.
Dans le modèle classique, chaque pod reçoit une adresse IP secondaire sur les interfaces réseau du nœud. Comme chaque adresse peut impliquer des interactions avec l’API EC2, la densité de pods peut être limitée avant même que le nœud n’ait épuisé son CPU ou sa mémoire.
La prefix delegation change l’unité d’allocation : au lieu de demander les adresses une par une, le nœud peut demander un bloc /28, soit seize adresses à la fois. Cette approche améliore la densité, avec jusqu’à 110 pods sur certaines instances, et réduit fortement le trafic vers l’API EC2.
Le piège de la fragmentation des sous-réseaux
Mais un bloc /28 exige seize adresses contiguës. Or, dans un cluster dynamique, les adresses utilisées par des pods classiques et par des pods disposant de leurs propres security groups peuvent se disperser dans le subnet. Au fil du temps, l’espace libre se fragmente.
Un subnet peut alors afficher des milliers d’adresses IP libres, tout en étant incapable de fournir un bloc contigu de seize adresses. Les allocations de préfixes échouent avec des erreurs de type InsufficientCidrBlocks, et des pods cessent d’être planifiés alors que les tableaux de bord indiquent encore que le subnet dispose d’espace.

La métrique « nombre d’IP libres » n’est donc plus suffisante. Elle était pertinente dans un modèle d’allocation adresse par adresse, mais elle ne reflète pas la contrainte réelle d’un modèle fondé sur des blocs contigus.
La réponse consiste à réserver de l’espace contigu pour ces préfixes, par exemple via des réservations CIDR de subnet VPC ou des subnets dédiés. Plus généralement, chaque optimisation de densité ou de performance doit s’accompagner d’une question : où la contrainte s’est-elle déplacée, et quelle métrique permet désormais de la voir ?
Bonnes pratiques pour des contrôleurs Kubernetes fiables à grande échelle
Les enseignements issus de ces contrôleurs EKS dépassent largement le seul contexte AWS. Tout contrôleur qui transforme une intention de haut niveau en application concrète rencontre les mêmes familles de problèmes : état, synchronisation, ressources limitées et observabilité.
Pour concevoir ou auditer un contrôleur Kubernetes destiné à tourner à grande échelle, plusieurs pratiques se dégagent :
- Limiter l’état en mémoire : ne conserver que les champs réellement nécessaires à la réconciliation.
- Éviter les calculs redondants : centraliser certaines résolutions lorsque cela réduit la charge globale sur l’API server.
- Séparer décision et application : laisser le contrôleur produire l’intention résolue, puis confier l’enforcement local aux agents de nœud.
- Attendre la synchronisation complète : ne jamais exécuter d’action destructive sur des caches partiellement chargés.
- Observer les vraies contraintes : adapter les métriques lorsque l’architecture change, notamment après une optimisation.
- Tester les scénarios de redémarrage : changement de leader, cache vide, reprise après incident et montée en charge doivent faire partie des tests.
Ces recommandations sont particulièrement importantes pour les contrôleurs liés à la sécurité, au réseau ou aux ressources cloud. Une erreur de réconciliation peut y provoquer une exposition de trafic, une coupure applicative ou la suppression d’une ressource encore utilisée.
Ce que les équipes plateforme doivent retenir
Les contrôleurs Kubernetes sont souvent présentés comme une abstraction élégante : une boucle observe, compare et corrige. Cette vision reste vraie, mais elle masque les difficultés qui apparaissent lorsque le système devient très grand.
À grande échelle, un contrôleur fiable n’est pas seulement un contrôleur qui sait réconcilier. C’est un composant qui sait réconcilier avec un état incomplet, sous pression mémoire, avec des objets qui changent constamment, et sans prendre d’actions dangereuses trop tôt.
Les exemples du Network Policy Controller et du VPC Resource Controller dans Amazon EKS montrent que la robustesse vient moins d’une infrastructure exceptionnelle que de choix d’ingénierie précis : réduire les caches, attendre le bon moment, découpler les responsabilités et mesurer les contraintes réelles.
Pour approfondir les fonctionnalités évoquées côté AWS, les documentations officielles sur les Network Policies dans Amazon EKS et les security groups pour pods détaillent les options disponibles et les conditions d’utilisation.
Au fond, la leçon vaut pour tout l’écosystème Kubernetes : plus l’intention déclarative s’approche de l’enforcement réel, plus la qualité du contrôleur devient critique. Dans un cluster moderne, la sécurité et la fiabilité ne dépendent pas seulement de ce que l’on déclare, mais de la manière dont cette déclaration est traduite, synchronisée et appliquée au bon moment.