Créer 511 clusters Kubernetes interconnectés avec Cilium Cluster Mesh (Partie 3)

Joseph Ligier
17 min read3 days ago

--

Dans cette troisième partie, nous allons améliorer l’algorithme pour la création de connexions entre cluster, nous allons ensuite tester Cilium Cluster Mesh avec AWS EKS et quelles sont les contraintes de cette solution pour créer un maximum de cluster. Ceux qui ont regretté le peu de Cilium dans la partie 2 seront plus servis.

L’algorithme

Rappels

Lors de la précédente partie, nous avions expliqué que la création de connexions, une par une, commençait à être trop longue à partir d’une dizaine de clusters.

L’idée est donc de créer en simultanée les connexions de façon disjointe par exemple les clusters 1,2 et les clusters 3,4 mais pas les clusters 1,2 et les clusters 2,3.

Résolution

Le code source est ici :

L’idée de cet algorithme c’est de :

  • créer un tableau de x clusters, par exemple :
  • créer un tableau de toutes les possibilités de connexions, ce qui donne :
  • créer un tableau de tableau de connexions disjointe, voici en gros l’algorithme :

On prend le premier élément du tableau (connections_list) : (0, 1)

On le compare avec le suivant : (0, 2)

on ne peut pas le prendre car 0 est commun

On le compare avec le suivant : (0, 3)

on ne peut pas le prendre car 0 est commun

On le compare avec le suivant : (1, 2)

on ne peut pas le prendre car 1 est commun

On le compare avec le suivant : (1, 3)

on ne peut pas le prendre car 1 est commun

On le compare avec le suivant : (2, 3)

pas de chiffre commun => on le prend

Il n’y a pas d’autres éléments dans la liste, on peut créer le premier élément de la liste qu’on cherche à avoir :

On retire ces deux éléments du tableau (connections_list).

On prend le premier élément : (0, 2)

On le compare avec le suivant : (0, 3)

on ne peut pas le prendre car 0 est commun

etc.

Au final on trouve :

Ainsi en utilisant le script :

./connections-optim.py 4
0,1 2,3
0,2 1,3
0,3 1,2
number of steps:
3
number of parallel by step:
2 2 2
  • on voit chacune des étapes : 0,1 et 2,3 puis 0,2 et 1,3 enfin 0,3 et 1,2
  • Le nombre d’étapes est donc de 3
  • À chaque fois, on fait 2 connexions en parallèle.

Ainsi on voit assez rapidement si l’algorithme est optimisé ou non.

Si on regarde avec 6 clusters :

./connections-optim.py 6
0,1 2,3 4,5
0,2 1,3
0,3 1,2
0,4 1,5
0,5 1,4
2,4 3,5
2,5 3,4
number of steps:
7
number of parallel by step:
3 2 2 2 2 2 2

On voit que l’algorithme n’est pas complètement optimisé pour ce cas-là. Dans la deuxième étape, le choix de 1,3 n’est pas le bon car 4,5 a déjà été pris. Il aurait fallu choisir 1,5 puis 2,4. Mais ça reste tout de même intéressant (7 étapes au lieu de 15).

L’algorithme est naïf, il prend la première paire qui convient sans se poser la question : “et s’il y avait mieux ?”. Vous allez voir que cela reste acceptable pour mon cas d’utilisation. Si vous souhaitez me faire part d’optimisation via une petite PR, vous serez le bienvenu (et vous serez cité dans le blog si vous le souhaitez).

Par exemple pour 32 clusters :

00,01 02,03 04,05 06,07 08,09 10,11 12,13 14,15 16,17 18,19 20,21 22,23 24,25 26,27 28,29 30,31
00,02 01,03 04,06 05,07 08,10 09,11 12,14 13,15 16,18 17,19 20,22 21,23 24,26 25,27 28,30 29,31
00,03 01,02 04,07 05,06 08,11 09,10 12,15 13,14 16,19 17,18 20,23 21,22 24,27 25,26 28,31 29,30
00,04 01,05 02,06 03,07 08,12 09,13 10,14 11,15 16,20 17,21 18,22 19,23 24,28 25,29 26,30 27,31
00,05 01,04 02,07 03,06 08,13 09,12 10,15 11,14 16,21 17,20 18,23 19,22 24,29 25,28 26,31 27,30
00,06 01,07 02,04 03,05 08,14 09,15 10,12 11,13 16,22 17,23 18,20 19,21 24,30 25,31 26,28 27,29
00,07 01,06 02,05 03,04 08,15 09,14 10,13 11,12 16,23 17,22 18,21 19,20 24,31 25,30 26,29 27,28
00,08 01,09 02,10 03,11 04,12 05,13 06,14 07,15 16,24 17,25 18,26 19,27 20,28 21,29 22,30 23,31
00,09 01,08 02,11 03,10 04,13 05,12 06,15 07,14 16,25 17,24 18,27 19,26 20,29 21,28 22,31 23,30
00,10 01,11 02,08 03,09 04,14 05,15 06,12 07,13 16,26 17,27 18,24 19,25 20,30 21,31 22,28 23,29
00,11 01,10 02,09 03,08 04,15 05,14 06,13 07,12 16,27 17,26 18,25 19,24 20,31 21,30 22,29 23,28
00,12 01,13 02,14 03,15 04,08 05,09 06,10 07,11 16,28 17,29 18,30 19,31 20,24 21,25 22,26 23,27
00,13 01,12 02,15 03,14 04,09 05,08 06,11 07,10 16,29 17,28 18,31 19,30 20,25 21,24 22,27 23,26
00,14 01,15 02,12 03,13 04,10 05,11 06,08 07,09 16,30 17,31 18,28 19,29 20,26 21,27 22,24 23,25
00,15 01,14 02,13 03,12 04,11 05,10 06,09 07,08 16,31 17,30 18,29 19,28 20,27 21,26 22,25 23,24
00,16 01,17 02,18 03,19 04,20 05,21 06,22 07,23 08,24 09,25 10,26 11,27 12,28 13,29 14,30 15,31
00,17 01,16 02,19 03,18 04,21 05,20 06,23 07,22 08,25 09,24 10,27 11,26 12,29 13,28 14,31 15,30
00,18 01,19 02,16 03,17 04,22 05,23 06,20 07,21 08,26 09,27 10,24 11,25 12,30 13,31 14,28 15,29
00,19 01,18 02,17 03,16 04,23 05,22 06,21 07,20 08,27 09,26 10,25 11,24 12,31 13,30 14,29 15,28
00,20 01,21 02,22 03,23 04,16 05,17 06,18 07,19 08,28 09,29 10,30 11,31 12,24 13,25 14,26 15,27
00,21 01,20 02,23 03,22 04,17 05,16 06,19 07,18 08,29 09,28 10,31 11,30 12,25 13,24 14,27 15,26
00,22 01,23 02,20 03,21 04,18 05,19 06,16 07,17 08,30 09,31 10,28 11,29 12,26 13,27 14,24 15,25
00,23 01,22 02,21 03,20 04,19 05,18 06,17 07,16 08,31 09,30 10,29 11,28 12,27 13,26 14,25 15,24
00,24 01,25 02,26 03,27 04,28 05,29 06,30 07,31 08,16 09,17 10,18 11,19 12,20 13,21 14,22 15,23
00,25 01,24 02,27 03,26 04,29 05,28 06,31 07,30 08,17 09,16 10,19 11,18 12,21 13,20 14,23 15,22
00,26 01,27 02,24 03,25 04,30 05,31 06,28 07,29 08,18 09,19 10,16 11,17 12,22 13,23 14,20 15,21
00,27 01,26 02,25 03,24 04,31 05,30 06,29 07,28 08,19 09,18 10,17 11,16 12,23 13,22 14,21 15,20
00,28 01,29 02,30 03,31 04,24 05,25 06,26 07,27 08,20 09,21 10,22 11,23 12,16 13,17 14,18 15,19
00,29 01,28 02,31 03,30 04,25 05,24 06,27 07,26 08,21 09,20 10,23 11,22 12,17 13,16 14,19 15,18
00,30 01,31 02,28 03,29 04,26 05,27 06,24 07,25 08,22 09,23 10,20 11,21 12,18 13,19 14,16 15,17
00,31 01,30 02,29 03,28 04,27 05,26 06,25 07,24 08,23 09,22 10,21 11,20 12,19 13,18 14,17 15,16
number of steps:
31
number of parallel by step:
16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16

On voit que c’est parfait là.

Et pour 511, ça devient illisible, j’ai laissé la partie intéressante:

number of steps:
511
number of parallel by step:
255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255

On ne peut pas faire mieux non plus.

Notez également que pour créer toutes les combinaisons possibles, j’ai utilisé une bibliothèque itertools au lieu d’un algorithme “maison” car la création de ce dernier prenait un temps fou rien que pour 32 clusters.

Maintenant qu’on a un algorithme, j’avais différents choix qui m’offraient à moi :

  • l’intégrer à cilium-cli puis l’utiliser comme ressource Terraform puis comme ressource Pulumi ;
  • créer directement une ressource Terraform sans passer par cilium-cli ;
  • l’intégrer directement à mon code Pulumi, c’est plus simple et rapide à faire que les deux autres solutions.

Après je me suis posé une question : est-ce que ça peut intéresser de créer un réseau tout à plat entre n clusters ? Pas sûr… Au niveau sécurité, on voudra forcément éviter que certains clusters ne communiquent pas entre eux. Il faudrait faire des exclusions… Ça commence à se compliquer tout ça. C’est pour ça que je suis allé au plus simple.

Intégration à Pulumi

Comment dire à Pulumi de commencer par faire les connexions entre tels clusters puis d’autres connexions etc ?

Dans Pulumi, il y a deux options intéressantes pour cela :

  • depends_on: c’est exactement pareil que tout Terraform/opentofu : telle ressource depend de tels autres
  • parent: cela permet de dire cette ressource a pour parent tel autre (chaque ressource a un unique parent) et donc on crée la ressource parent avant l’autre. C’est également sympathique pour l’affichage pendant le déploiement.

On va créer une ressource parente pour chaque étape qui ne fait rien. Cette ressource sera créé une fois que le Cluster Mesh sera activé.

Exemple pour une étape avec 8 clusters, c’est assez clair

On utilisera depends_on pour établir les dépendances entre les groupes de connexions : le premier groupe n’aura pas de dépendance, le deuxième groupe aura comme dépendance le premier groupe, le troisième groupe aura comme dépendance le premier et le deuxième groupe, etc.

Le code est ici pour Kind:

Exemple pour 10 clusters

J’ai également comparé le temps de déploiement des deux algorithmes avec Kind

clusters Kind

On voit évidemment que l’algorithme permet de gagner du temps de façon non négligeable (surtout à partir de 10 clusters) mais que ça a l’air de suivre toujours une exponentielle ou un truc qui s’en rapproche, on gagne environ un cluster (Par exemple le temps avec 13 clusters en utilisant l’algorithme optimisé est aussi rapide qu’en utilisant l’algorithme simple avec 12 clusters).

Souviens-toi… l’été dernier

Nous allons maintenant étudier si le service AWS EKS est intéressant pour créer 511 clusters Kubernetes. J’ai choisi le Kubernetes managé par AWS car je le connais pas mal ; j’avais fait une autre série d’articles sur Cilium et EKS. Je n’avais pas réellement parlé de Cluster Mesh avec EKS. C’est l’occasion.

Pulumi

Pour déployer Cilium Cluster Mesh avec EKS, nous allons utiliser le code suivant :

Par défaut, il crée un Cluster Mesh de 2 clusters mais il est aisé d’en créer plus, par exemple pour 5 clusters :

pulumi config set clusterNumber 5

Nous allons en créer 2 pour simplifier.

Il ne se sert pas de l’algorithme “optimisé” mais nous allons voir qu’il y a d’autres limitations plus importantes.

pulumi login --local && pulumi stack init dev
Logged in to ip-172-31-27-167.ec2.internal as root (file://~)
Created stack 'dev'
Enter your passphrase to protect config/secrets:
Re-enter your passphrase to confirm:

On déploie ensuite :

pulumi up
C’est assez petit j’espère

Lors de ce déploiement, il y a l’exploitation de différents providers :

  • aws : c’est le provider traditionnel pour AWS qui récupère les bibliothèques du provider AWS de Terraform
  • awsx : c’est un “meta-provider” : il va aider à prendre en compte les bonnes pratiques d’AWS sans se prendre la tête, je l’utilise pour la création du réseau d’AWS (VPC, subnet, nat gateway, table de routage, etc)
  • eks : c’est un “meta-provider” : Même chose qu’awsx pour aws mais pour EKS.
  • kubernetes : c’est le provider traditionnel pour le déploiement sur Kubernetes, c’est le provider eks qui l’utilise (je n’ai rien demandé)
  • command : je l’utilise pour générer le kubeconfig pour les deux clusters (le provider eks n’a pas prévu la génération de kubeconfig pour plus d’un cluster c’est dommage)
  • cilium : c’est le provider que j’ai créé sur Terraform que j’ai exporté ses bibliothèques pour Pulumi (comme ce qui a été fait pour AWS par exemple).

J’ai utilisé des providers “haut niveau” pour éviter de devoir coder chaque ressource et ainsi gagner du temps.

Un peu d’archi AWS

Voilà ce qui a été déployé au sens AWS :

Architecture globale
  • On a créé une transit gateway (symbole violet) pour la communication intra-clusters (lien violet). Pour deux VPCs, il serait également possible de créer un vpc peering.
  • Par soucis d’économie et de limite, on a une Nat gateway (symbole orange dans le réseau public) par VPC permettant aux pods et aux nœuds d’avoir Internet et surtout aux agents cilium d’accéder au clustermesh-api des autres clusters. Il faudrait une Nat Gateway par sous-réseau publique si on voulait de la haute disponibilité.
  • Cilium a créé un Classic Load Balancer (CLB) via le service Load Balancer de Kubernetes dans les réseaux publics pour la communication des clustermesh-api intra-clusters (liens en rouge et en jaune). Dans l’idéal, il faudrait un Network Load Balancer (NLB) car le CLB est déprécié et également le mettre dans les réseaux privés pour la confidentialité.

L’architecture est extensible, si on veut 4 clusters :

Je vous laisse imaginer ce que ça ferait pour 511 clusters

Notez que je n’ai pas tracé les liens pour la communication pour les clustermesh-api, je pense que vous avez compris.

Si on voulait respecter les standards de sécurité AWS, il faudrait segmenter en compte c’est à dire avoir un VPC par compte :

Pour résumer : ma solution n’est pas prod ready :)

On a présenté la vue AWS. Nous allons maintenant voir la vue Kubernetes.

La magie de Cilium

On rappelle un schéma que j’avais fait dans la partie 1 pour Kind :

dayjavoo

L‘élément qui est intéressant est la communication entre cilium-cmesh-api et l’etcd (lien en vert). Comment Cilium va se débrouiller pour communiquer avec l’etcd alors qu’il est “masqué” sur EKS ? En effet, on ne peut pas requêter sur l’etcd d’EKS.

Avant de répondre à la question, regardons l’architecture de Kubernetes sur EKS avec Cilium Cluster Mesh.

L’architecture est globalement similaire, les principales différences :

  • le control plane est une boite noire (jeu de mot subtile ?) ;
  • des load balancers au lieu de Node Port ;
  • localpath n’est pas installé.

Voici l’architecture sur AWS :

Architecture à la loupe

Pour les deux clusters, on utilise un routage natif, c’est à dire que les IPs des pods sont dans le même range d’IPs (CIDR) que le VPC (on utilise des ENIs).

Alors comment Cilium récupère-t-il les données de l’etcd ?

En fait le schéma est un peu simplifié. Le pod cilium-clustermesh-api a 3 containers :

  • un container d’initialisation de la base de données etcd
  • un container etcd distinct de l’etcd de Kubernetes
  • un container qui contient l’apiserver qui va requêter sur l’api de Kubernetes et va ensuite synchroniser sur l’etcd de cilium.
  • optionnel : pour améliorer les performances on peut rajouter un container qui cache l’etcd (voir KVstoremesh).

Schématiquement, on a donc ça :

le pod clustermesh-api est représenté au microscope
  • Les agents Cilium récupèrent les données de l’etcd du pod clustermesh-api
  • Pour remplir cette base de données, le container apiserver récupère des informations de l’EKS (ou de n’importe quel Kubernetes) via l’API, un peu comme on le ferait avec kubectl puis les synchronise dans l’etcd de clustermesh-api

Mais alors, l’etcd de clustermesh-api contient-t-il exactement l’etcd de Kubernetes comme les Secrets ? Non, il est minimal, il comprend uniquement le strict nécessaire : les objets Kubernetes réseaux comme les Services, les objets propres à Cilium comme les ciliumendpoints, ciliumidentities, etc.

S’il y a beaucoup de nœuds et de clusters et donc d’agents Cilium (car c’est un DaemonSet), cela peut créer un DDOS sur l’etcd de clustermesh-api.

Pour résoudre ce problème, il est possible d’utilisé un cache :

Si avec ça, vous ne comprenez pas qu’un pod n’est pas toujours un unique container
  • Dans cette situation l’etcd de clustermesh-api est uniquement utilisé par deux entités : apiserver pour l’écriture sur la base de données, kvstoremesh pour la lecture sur la base de données. KVstoremesh va encaisser toute la charge, heureusement c’est son job.

D’après la documentation :

KVStoreMesh enables improved scalability and isolation, and targets large scale Cluster Mesh deployments.

Je crois qu’on devrait l’activer.

Depuis la version 1.16, KVStoreMesh est activé par défaut.

Les limites avec cette solution

Maintenant qu’on a vu comment fonctionnaient le Cluster Mesh et le service AWS EKS, nous allons voir ce qui pose soucis avec le déploiement précédent et quelques pistes pour résoudre cela.

Dure limite

Service quota est un service AWS qui permet de gérer les limites AWS. On a beau être sur le cloud, il y a des limites ; des soft limites qui peuvent être levées en contactant le support AWS et des hard limites qui ne peuvent pas être changées. Les soft limites ont le même but que le plafond d’une carte bleue : éviter qu’il y ait un excès non volontaire de consommation.

Avant d’établir le record, il va donc falloir faire la traque de toutes ces limites et faire la demande pour passer outre ces limites si possible.

Par exemple, le nombre de cluster EKS est limité à 100 par région. Il faut donc contacter le support pour lever cette limite à 511. Parfois cela pourrait être refusé car on n’a pas encore 100 clusters EKS. D’autres limitations soft que je vois comme :

Etc.

Donc avec ma solution, par défaut, on ne pourra pas aller au-delà de 5 clusters. C’est assez décevant surtout avec notre super algorithme.

Il faudrait adapter l’architecture pour qu’on ait le moins de problèmes avec ces limites quitte à négliger la sécurité et les bonnes pratiques.

Argent trop cher

On présume maintenant qu’on a toutes les autorisations possibles. Bien que mon blog me permet d’être multi-milliardaire ou presque, il y a une question qui fâche : combien ça coûte ?

On va supposer que ça dure environ 4 heures entre le déploiement, les tests et la suppression.

On va y aller à la louche (ça peut dépendre de la région qu’on choisi) avec les trucs qui coûtent chers :

  • cluster EKS : 0,10 $ / heure. On en a besoin de 511 => 205 $ pour 4 heures
  • nat gateway : 0.045 $ / heure. On en a besoin de 511 => 92 $ pour 4 heures
  • ec2 t3.medium (je pourrais voir du côté de ARM pour économiser) : 0.0416 $ / heure. On en a besoin de 511 => 85 $
  • classic load balancer : 0.025 $ / heure (on économise un peu si je “choisi” un NLB)/ On en a besoin de 511 => 52 $

À noter également le flux réseau, les IPs publiques, les EBS et la transit gateway qui coûtent un peu d’argent mais je pense que c’est “négligeable” pour 4 heures.

Donc on aurait un total de 434 $. Il faudrait voir comment réduire cette facture.

Temps à nouveau

On a supposé que ça allait durer 4 heures mais est-ce vraiment le cas ? Dans cette vidéo, j’ai déployé 5 clusters avec le script Pulumi :

Beau temps pour se jeter à l’eau

On voit que ça dure environ 52 minutes pour 5 clusters sans effectuer les connexions du Cluster Mesh… Peu de chance qu’on arrive à 4 heures avec 511 clusters. Si vous êtes bien attentifs dans la vidéo, Pulumi crée d’abord le premier cluster EKS puis il crée un deuxième cluster EKS, etc. Ça manque de parallélisation. Mais je n’ai pas réussi cela avec le provider eks de Pulumi.

66 secondes pour créer une connexion

Autre soucis plus inquiétant visible dans la vidéo, les connexions entre clusters mettent plus d’une minute à chaque fois alors qu’avec Kind on mettait environ 15 s (quand le serveur n’est pas trop chargé). Rien que pour les connexions on mettrait donc 511 minutes soit environ 8h30 avec l’algorithme optimisé. Avec 15 s, on mettrait environ 128 minutes soit un peu plus de 2 heures.

Voilà c’est fini

Dans la partie précédente, nous avions réussi à créer 13 clusters (14 en forçant un peu). Pour cette partie nous en avons créé que 5 avec un vrai cluster : EKS. J’espère que la déception n’est pas trop forte et que vous attendez également la partie suivante.

Cependant, nous avons vu :

  • comment paralléliser les connexions du Cilium Cluster Mesh ;
  • mettre en place le Cilium Cluster Mesh sur AWS EKS ;
  • comment fonctionne la communication des agents Cilium et l’etcd de Cilium
  • les limites de mon script Pulumi et d’AWS pour déployer 511 clusters

Dans la prochaine partie, nous verrons comment résoudre ces différentes limites.

--

--