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

Joseph Ligier
10 min readJun 24, 2024

--

Dans cette deuxième partie, nous allons créer un maximum de clusters Kubernetes Kind qui sont interconnectés via Cilium Cluster Mesh sur un seul serveur. Avant cela, nous allons réfléchir sur l’outil le plus adapté pour déployer cela automatiquement.

Infrastructure As a Code

Terraform / OpenTofu

C’est le standard du marché. Il utilise comme langage le HCL. Dans la précédente partie, j’ai fait la démonstration avec cette technologie pour déployer 2 clusters Kubernetes. Mais comment faire pour en déployer plus ?

Nous allons regarder un peu plus en profondeur le code :

Notamment le main.tf, pour déployer les clusters Kind, pas de soucis, on a un seul module qui a une boucle foreach :

module "kind" {
source = "./modules/kind"

for_each = var.kind
name = each.value.name
pod_subnet = each.value.pod_subnet
service_subnet = each.value.service_subnet
nodes_number = 1
}

On peut facilement en créer plein.

Mais pour l’installation de Cilium, on a dû créer deux modules distincts qui utilisent un provider cilium différent (mesh1 et mesh2) :

module "cilium_clustermesh1" {
source = "./modules/cilium-clustermesh"
cluster_name = var.kind.mesh1.name
cluster_id = var.cilium.mesh1.cluster_id
release_version = var.cilium.mesh1.version
service_type = "NodePort"

providers = {
cilium = cilium.mesh1
}

depends_on = [module.kind]
}

module "cilium_clustermesh2" {
source = "./modules/cilium-clustermesh"
cluster_name = var.kind.mesh2.name
cluster_id = var.cilium.mesh2.cluster_id
release_version = var.cilium.mesh2.version
service_type = "NodePort"
extra_set = ["tls.ca.cert=${local.cert}", "tls.ca.key=${local.key}"]

providers = {
cilium = cilium.mesh2
}

depends_on = [module.kind]
}

Donc si je dois créer un troisième cluster Kubernetes, je dois rajouter une nouvelle section module :

module "cilium_clustermesh3" {
source = "./modules/cilium-clustermesh"
cluster_name = var.kind.mesh3.name
cluster_id = var.cilium.mesh3.cluster_id
release_version = var.cilium.mesh3.version
service_type = "NodePort"
extra_set = ["tls.ca.cert=${local.cert}", "tls.ca.key=${local.key}"]

providers = {
cilium = cilium.mesh3
}

depends_on = [module.kind]
}

Car on ne peut pas boucler sur les providers. Cette amélioration est même top 1 des améliorations demandées dans OpenTofu :

N’hésitez pas à aller voter !

Du côté de chez Hashicorp, ça fait depuis 2019 que la demande a été faite.

Une solution est de créer des templates jinja qui génèrent la boucle mais c’est assez crado je trouve.

Donc cette solution n’est pas idéale pour créer plein de clusters Kubernetes rapidement.

Pulumi

Pulumi est un autre outil pour l’infra as a code. Contrairement à Terraform, Pulumi est multi-langage, on peut utiliser différents langages de programmation comme Python, Go, Typescript, .Net ou java… Nous allons voir qu’il résout le problème de boucles de providers.

Le code est ici :

C’est écrit en Python, voici le code équivalent écrit en Pulumi pour déployer Cilium :

def cilium_clustermesh(i, kind):
cmesh_provider = cilium.Provider(f"cmesh{i}", context=f"kind-cmesh{i}", opts=pulumi.ResourceOptions(depends_on=kind))
cmesh_cilium = cilium.Install(f"cmesh{i}Install",
sets=[
f"cluster.name=cmesh{i}",
f"cluster.id={i}",
"ipam.mode=kubernetes",
],
version="1.15.5",
opts=pulumi.ResourceOptions(depends_on=kind, providers=[cmesh_provider]),
)
return {
"cmesh": cilium.Clustermesh(f"cmesh{i}Enable", service_type="NodePort", opts=pulumi.ResourceOptions(depends_on=[cmesh_cilium], providers=[cmesh_provider])),
"provider": cmesh_provider,
}

[...]

for i in cluster_ids:
c += [cilium_clustermesh(i, kind_list)]

Le code n’est pas super beau à mon sens, il mériterait d’être réécrit. Mais cette version ne comporte que 63 lignes de code pour pouvoir déployer une infinité de clusters ce qui est déjà mieux que Terraform :

pulumi config set clusterNumber 511
pulumi up # Et là on déploie 511 clusters Kind en Cluster Mesh (en théorie 🤡)
Exemple avec 3 clusters en accéléré

On peut également remarquer une fonction :

def combinlist(seq, k):
p = []
i, imax = 0, 2**len(seq)-1
while i<=imax:
s = []
j, jmax = 0, len(seq)-1
while j<=jmax:
if (i>>j)&1==1:
s.append(seq[j])
j += 1
if len(s)==k:
p.append(s)
i += 1
return p

[...]
combi = combinlist(cluster_ids, 2)
[...]
k = 0

for i, j in combi:
depends_on = [c[j-1]['cmesh']] + cmesh_connect
cmesh_connect += [cilium.ClustermeshConnection(f"cmeshConnect-{k}", destination_context=f"kind-cmesh{i}", opts=pulumi.ResourceOptions(depends_on=depends_on, providers=[c[j-1]['provider']]))]
k += 1

Il est également impossible d’écrire ce code en HCL car on boucle sur des providers. Si c’était faisable, bon courage pour l’écrire !

À quoi sert cette fonction ? Cela va permettre de lister les combinaisons possibles entre les clusters Kubernetes pour les connecter via Cilium Cluster Mesh. S’il y a 2 clusters (A et B), on doit faire une connexion :

  • Entre A et B

S’il y a 3 clusters (A, B et C), on doit faire 3 connexions :

  • Entre A et B
  • Entre A et C
  • Entre B et C

S’il y a 4 clusters (A, B, C et D), c’est là où ça se complique, on doit faire 6 connexions :

S’il y a 5 clusters, je vous laisse compter les connexions :

Etc.

Si vous avez des souvenirs de math de l’école, le nombre de connexions est le coefficient binomial (avec k=2 et n est le nombre de clusters):

Je sens que je viens de raviver des bons ou des mauvais souvenirs 🙂

Donc cela donne :

Ah oui c’est plus simple

Autres choix

Notons qu’il y a certainement d’autres choix possibles comme Crossplane ou Terraform CDK.

Pour Crossplane, j’ai créé récemment un provider Cilium pour cela. C’est donc en théorie possible. Mais je manque d’expérience dans Crossplane (je ne sais pas si c’est possible de faire l’algorithme précédent) et le provider Crossplane n’est pas encore mûr à mon avis.

Pour Terraform CDK, j’ai toujours eu des soucis avec quand j’ai voulu le tester. Et le drama Hashicorp ne m’encourage guère à aller plus loin. Donc je n’ai même pas pensé à le retester pour l’occasion.

Faire les tests

Pour faire valider un record, il faut vérifier que Cilium Cluster Mesh fonctionne bien entre les clusters.

Il y a différents tests :

  • Sur chaque cluster :
cilium status

Pour vérifier que Cilium fonctionne bien sur un cluster en particulier

  • Sur chaque cluster, vérifier que la connexion est là entre les clusters :
cilium clustermesh status
  • Pour aller plus en profondeur, il y a également :
cilium connectivity test --context kind-cmesh1 --multi-cluster kind-cmesh2

Cela crée une série de scenarii permettant de tester plein de cas de figure entre 2 clusters.

Dans l’idéal, il faudrait le faire entre chaque cluster (on se retrouve dans le nombre de combinaisons précédemment vu). Cette série de scenarii de tests dure environ 5 minutes. Avec 511 clusters, ça fait: 130305 séries de tests ! Soient 10858 heures (452 jours). La facture va faire mal et mon article de blog ne sera toujours pas écrit. On va déjà faire juste une série de tests de ce type entre 2 clusters. On verra après si ça vaut le coup d’en faire plus (par exemple on pourrait faire un unique test au hasard entre chaque clusters).

Tests automatisés

Pour faire des tests automatisés, j’ai choisi Github action. Pour une raison principale, la puissance de la VM :

C’est déjà pas mal pour monter à l’échelle. Le code github action est ici.

Github Action avec 12 clusters

Cluster Kind

Chaque cluster Kind est uniquement composé d’un control plane (pas de worker). Je conçois que ce n’est pas réaliste mais on y va au minimum.

Résultats

Qu’est-ce qui a planté en premier le CPU, la RAM ou le stockage ?

Roulement de tambour…

C’est le stockage, à partir de 10 clusters Kind, il n’y a plus d’espace disque. Heureusement il y a possibilité de nettoyer la VM pour aller plus loin :

          sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"

Voici les résultats bruts :

2 clusters : 2m 05s
3 clusters : 2m 50s
4 clusters : 4m 05s
5 clusters : 5m 50s
6 clusters : 8m 23s
7 clusters : 11m 29s
8 clusters : 14m 44s
9 clusters : 19m 44s
10 clusters: 25m 19s
11 clusters: 32m 39s
12 clusters: 40m 11s
13 clusters: 52m 49s
14 clusters: failed

Il faut lire : pour 5 clusters, on met 5 minutes 50 secondes pour le déployer.

On voit qu’à partir de 14 clusters, on n’y arrive plus. Je reviendrai plus tard de la raison du non-fonctionnement.

Création des clusters en cluster mesh (x) en fonction du temps (y)

J’ai demandé à ChatGPT, si c’était bien une exponentiel :

Il pense également cela.

Donc en supposant que ça n’échoue pas, il est donc possible de prévoir à la louche combien de temps ça mettrait pour créer 511 clusters. Sans même calculer le résultat, on peut sans crainte affirmer que ça risque d’être encore trop long (presqu’une heure pour seulement 13 clusters, c’est déjà beaucoup).

Tracing

Maintenant qu’on a fait quelques observations. On va essayer de comprendre pourquoi c’est si long dès qu’on a une dizaine de clusters. Voici comment se passe la création de 3 clusters :

Notice : on met 41 s pour créer les 3 clusters Kind
  • Les installations de Cilium sont faites une fois que tous les clusters Kind sont créés.
  • Les connexions sont créés une fois que toutes les installations sont déployés.

Les connexions ne sont parallélisées. On perd à chaque fois environ 15 s. Ainsi pour 13 clusters, on a 78 connexions (toujours ce coefficient binomial) à réaliser. En supposant que ça dure 15s en moyenne, ça donne 20 minutes. Soit environ 30% de la durée de déploiement.

Déploiement de 13 clusters en 35 minutes sur un serveur avec 16 CPU

Pour 511 clusters, on aurait (au minimum) : 130305 x 15 s = 626 h = 26h rien que pour les connexions entre les clusters (quelques soient le type de clusters). Le reste est a priori parallélisable.

Défi impossible ?

Comment paralléliser ces connexions ? Il n’est pas possible de connecter en même temps deux mêmes clusters (par exemple faire la connexion entre C1 et C2 et entre C1 et C3). Par contre il est possible de faire deux connexions de façon disjointe, en même temps entre cluster 1 et cluster 2 et entre cluster 3 et cluster 4 pour 4 clusters. Ce qui donne en schéma suivant :

On a 4 clusters on déploie les connexions en 3 fois

Pour 6 clusters, on aurait :

On a 6 clusters, on déploie les connexions en 5 fois

A priori, on passe d’un algorithme avec une complexité quadratique (O(n²)) à une complexité linéaire (O(n)).

Pas trop complexé avant l’été ?

Ainsi pour 511 clusters, cela prendrait environ 511 x 15 s = 7665 s = 127 minutes soit environ 2 heures au lieu des 26 heures ce qui est plus acceptable. Cela suppose également que Pulumi puisse paralléliser jusqu’à 511/2 ~ 256 (Par défaut, on a 16) pour les connexions entre cluster.

Pourquoi ça plante avec Kind ?

J’ai plusieurs messages de ce type au moment de l’installation de Cilium:

cilium:index:Install (cmesh8Install):
error: Missing Resource State After Create: The Terraform Provider unexpectedly returned no resource state after having no errors in the resource creation. This is always an issue in the Terraform Provider and should be reported to the provider developers.
The resource may have been successfully created, but Terraform is not tracking it. Applying the configuration again with no other action may result in duplicate resource errors. Import the resource if the resource was actually created and Terraform should be tracking it.

Il faut donc que je reporte au développeur du provider terraform. Ah mince c’est moi-même 😀

J’arrive à aller à un peu plus que 14 clusters si je ne parallélise pas trop les tâches.

Je teste sur un serveur plus puissant : 16 CPU, 32 Go de RAM pour voir si c’est un problème de ressource. Et mêmes messages d’erreur.

Comme j’ai accès au serveur, je remarque que l’agent Cilium n’arrive pas à démarrer. Je vois le message d’erreur suivant au niveau du log de Cilium pour le container config d’initialisation :

time="2024-06-20T16:24:39Z" level=info msg=Invoked duration="531.226µs" function="github.com/cilium/cilium/cilium-dbg/cmd.glob..func39 (cmd/build-config.go:32)" subsys=hive
time="2024-06-20T16:24:39Z" level=info msg=Starting subsys=hive
time="2024-06-20T16:24:39Z" level=info msg="Establishing connection to apiserver" host="https://172.20.2.1:443" subsys=k8s-client
time="2024-06-20T16:25:14Z" level=info msg="Establishing connection to apiserver" host="https://172.20.2.1:443" subsys=k8s-client
time="2024-06-20T16:25:44Z" level=error msg="Unable to contact k8s api-server" error="Get \"https://172.20.2.1:443/api/v1/namespaces/kube-system\": dial tcp 172.20.2.1:443: i/o timeout" ipAddr="https://172.20.2.1:443" subsys=k8s-client
time="2024-06-20T16:25:44Z" level=error msg="Start hook failed" error="Get \"https://172.20.2.1:443/api/v1/namespaces/kube-system\": dial tcp 172.20.2.1:443: i/o timeout" function="client.(*compositeClientset).onStart" subsys=hive
time="2024-06-20T16:25:44Z" level=info msg=Stopping subsys=hive
Error: Build config failed: failed to start: Get "https://172.20.2.1:443/api/v1/namespaces/kube-system": dial tcp 172.20.2.1:443: i/o timeout

L’agent Cilium n’arrive pas à établir la connexion avec l’API de kubernetes.

J’augmente des valeurs de configuration du noyau Linux :

sysctl -w fs.inotify.max_user_watches=2099999999
sysctl -w fs.inotify.max_user_instances=2099999999
sysctl -w fs.inotify.max_queued_events=2099999999

Ça fonctionne mais je tombe sur un timeout au moment de l’activation de Cluster Mesh. En relançant pulumi, ça fonctionne et maintenant je dois attendre la longue phase des connexions en serveur...

Ça fonctionne mais pas jusqu’au bout
Maintenant je vais pouvoir aller dormir

Quoi qu’il en soit, on voit ainsi que Kind n’est pas une solution idéale pour créer un nombre supérieur à 15 clusters.

Même en utilisant des clusters pour jouer, on peut déjà voir certaines limites qu’on aurait également sur des vrais clusters.

Pour la prochaine partie, nous allons implémenter l’algorithme et l’installer sur des vrais clusters.

--

--