Créer un cluster Kubernetes local avec Vagrant

Créer un cluster Kubernetes local avec Vagrant

Tester Kubernetes est assez facile grâce à des solutions telles que Minikube.

Toutefois, lorsqu'on souhaite tester des fonctionnalités propres à du cluster, comme de l'équilibrage de charge ou de la bascule, ce n'est plus forcément adapté.

Il est possible de monter son infrastructure Kubernetes sur des serveurs, ou en exploitant des services managés chez un fournisseur cloud (Kapsule chez Scaleway, AKS chez Azure, GKE chez GCP ou EKS chez AWS par exemple).

Néanmoins, ces solutions coûtent de l'argent. Lorsqu'on veut juste tester des fonctionnalités ou s'autoformer, ce n'est pas forcément adapté.

Dans ce billet, je vous propose de voir comment vous pouvez monter un cluster Kubernetes localement sur votre poste avec un fonctionnement similaire à un cluster classique.

Pour cela, nous allons utiliser plusieurs outils que je vais vous décrire un peu plus bas :

  • Vagrant
  • VirtualBox (ou VMWare)
  • K3S
  • Traefik

Vagrant : Provisionning de machines virtuelles

Vagrant est un outil d'HashiCorp (éditeur de Terraform dont j'ai déjà parlé).

Ce programme permet de déployer rapidement des machines virtuelles en exploitant des fichiers de description.

Ainsi en écrivant un fichier VagrantFile, il est possible de déployer en quelques minutes une ou plusieurs machines, en les provisionnant avec des scripts ou des outils tels qu'Ansible.

L'avantage de Vagrant est qu'il permet ainsi le partage de configuration pour permettre à une équipe complète de travailler dans les mêmes conditions en local en reproduisant un comportement de production à faible coût.

Un simple fichier texte suffit pour être partagé. De plus, il est de fait possible de versionner ce fichier sur outil de gestion de version.

K3S : Le kubernetes allégé

K3S est un outil créé par Rancher (qui a aussi créé un orchestrateur du même nom pour Docker).

Il s'agit d'un Kubernetes allégé pouvant fonctionner sur des configurations plus petites. Il peut mếme fonctionner sans soucis sur un raspberry pi.

Dans notre cas, il nous permettra de passer outre les limitations de K8S qui demande au minimum 2G de ram pour fonctionner.

Vu que nous allons créer plusieurs serveurs, l'idée est de limiter au maximum la mémoire nécessaire.

Dans ce billet, nous exploiterons le mode multimaster, très récent. Si vous voulez plus d'informations sur ce sujet, je vous invite à lire l'excellent article de mes collègues de WeScale.

Traefik : Encore et toujours le load balancer

Afin de reproduire de manière la plus fidèle le comportement d'un cluster classique, nous déploierons aussi un serveur Traefik en frontal, devant Kubernetes, qui équilibrera la charge entre les 3 nœuds maîtres.

Le choix de Traefik est arbitraire, n'importe quel reverse proxy fera l'affaire.

Une fois de plus, le but est d'avoir quelque chose de léger, d'où l'utilisation de Traefik qui est très peu consommateur en ressources.

Aperçu de déploiement cible

Nous allons avoir 3 couches distinctes dans notre déploiement :

  • front_lb : Une machine utilisant Traefik permettant de gérer le trafic entrant vers Kubernetes
  • kubemaster# : Les 3 serveurs kubernetes "master"
  • kubenode# : Les 3 serveurs kubernetes servant de nœuds d'exécution pour les pods

Prérequis et informations

Le déploiement que je vais décrire ci-dessous a été testé et validé avec la configuration suivante :

Système d'exploitation Parrot OS 4.10
Mémoire totale 16 Go
CPU Intel i7 10510u
Virtualisation VirtualBox 6.1.12
Version de Vagrant 2.2.9

Le déploiement ne fait normalement appel à aucune de ces informations en dur, mais il est possible que certains paramètres soient à ajuster en fonction de votre système hôte.

De plus, afin de permettre à mes nœuds de communiquer entre eux en SSH, j'ai créé une clé RSA que je déploie automatiquement.

Cette clé est dans le répertoire ./.ssh, et il nécessaire que vous la créiez vous-même avant de lancer le déploiement via Vagrant.

Vous pouvez le faire aisément avec la commande suivante :

ssh-keygen -f ./.ssh/id_rsa

À la demande de passphrase, il est nécessaire de ne pas en mettre, car nous allons utiliser cette clé pour avoir une connexion scriptée entre nos nœuds.

Il est temps de se salir les mains!

Bon, assez parlé, c'est quand qu'on déploie?

Pas tout de suite, d'abord, regardons ce que nous allons déployer.

L'OS de base

Dans nos machines, nous allons devoir déployer tout d'abord un système d'exploitation.

Vagrant utilise un système de "boxes" qui sont en fait des images préparées pour Vagrant. Il est possible d'avoir la liste des boxes sur le site officiel.

Dans notre cas, nous allons utiliser une box Ubuntu 18.04.

Je ne suis pas fan d'Ubuntu en serveur, je préfère une bonne vieille Debian ou Centos, mais Ubuntu est le système officiellement supporté par K3S.

Pour Traefik, j'utiliserai la même box, juste parce que je suis fainéant. En soi, rien ne m'empêche d'exploiter une autre box de base.

Les masters

Nous avons donc dans un premier temps les serveurs Kubernetes "masters".

Pour ces derniers, nous avons deux cas distincts.

Le premier serveur doit utiliser un paramètre pour indiquer que l'on veut initier un cluster K3S, avec le paramètre "--cluster-init".

Ensuite, les autres nœuds devront se connecter sur le premier nœud avec le secret généré par celui-ci.

Pour simplifier l'échange du secret, le second et troisième nœud iront donc télécharger le fichier contenant le secret via SCP (d'où la création de la clé SSH).

De plus, nous allons indiquer à K3S l'adresse IP des serveurs, car la VM ayant plusieurs cartes réseaux, K3S me montait la mauvaise IP à chaque fois.

Voici donc le bloc de configuration Vagrant associé :

MASTER_COUNT = 3
IMAGE = "ubuntu/bionic64"

...

Vagrant.configure("2") do |config|

  (1..MASTER_COUNT).each do |i|
    config.vm.define "kubemaster#{i}" do |kubemasters|
      kubemasters.vm.box = IMAGE
      kubemasters.vm.hostname = "kubemaster#{i}"
      kubemasters.vm.network  :private_network, ip: "10.0.0.#{i+10}"
      kubemasters.vm.provision "file", source: "./.ssh/id_rsa.pub", destination: "/tmp/id_rsa.pub"
      kubemasters.vm.provision "file", source: "./.ssh/id_rsa", destination: "/tmp/id_rsa"
      kubemasters.vm.provision "shell", privileged: true,  path: "scripts/master_install.sh"
    end
  end

...

end
  • Ligne 8 : Structure d'une boucle Vagrant
  • Ligne 11 : On nomme chaque machine avec un hostname différent
  • Ligne 12 : Chaque machine aura une IP fixe d'allouée (de 10.0.0.11 à 10.0.0.13)
  • Ligne 13/14 : On pousse notre clé SSH qui sera déployée par le script indiqué à la ligne 15

Comme vous pouvez le voir, la description du côté Vagrant est assez basique.

Maintenant, voyons le second côté de ce déploiement : le script shell de provisionning.

#!/bin/sh

# Deploy keys to allow all nodes to connect each others as root
mv /tmp/id_rsa*  /root/.ssh/

chmod 400 /root/.ssh/id_rsa*
chown root:root  /root/.ssh/id_rsa*

cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
chmod 400 /root/.ssh/authorized_keys
chown root:root /root/.ssh/authorized_keys

# Add current node in  /etc/hosts
echo "127.0.1.1 $(hostname)" >> /etc/hosts

# Get current IP adress to launch k3S
current_ip=$(/sbin/ip -o -4 addr list enp0s8 | awk '{print $4}' | cut -d/ -f1)

# If we are on first node, launch k3s with cluster-init, else we join the existing cluster
if [ $(hostname) = "kubemaster1" ]
then
    curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --cluster-init --tls-san $(hostname) --bind-address=${current_ip} --advertise-address=${current_ip} --node-ip=${current_ip} --no-deploy=traefik" sh -
else
    echo "10.0.0.11  kubemaster1" >> /etc/hosts
    scp -o StrictHostKeyChecking=no root@kubemaster1:/var/lib/rancher/k3s/server/token /tmp/token
    curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --server https://kubemaster1:6443 --token-file /tmp/token --tls-san $(hostname) --bind-address=${current_ip} --advertise-address=${current_ip} --node-ip=${current_ip} --no-deploy=traefik" sh -
fi

# Wait for node to be ready and disable deployments on it
sleep 15
kubectl taint --overwrite node $(hostname) node-role.kubernetes.io/master=true:NoSchedule

Encore une fois, découpons ce script :

  • Ligne 4 à 11 : On déploie notre clé SSH et on l'ajoute en hôte autorisé.
  • Ligne 19 à 27 : Si on est sur le nœud "kubemaster1", on lance un cluster-init, sinon on rejoint le cluster existant
  • Ligne 30 et 31 : on attend 15 secondes, le temps que le nœud soit fonctionnel, puis on le "taint" afin d'indiquer que le nœud ne doit pas exécuter de pods. En effet, par défaut, K3S étant prévu pour fonctionner sur de petits systèmes, le master fonctionne en mode standalone et peut aussi exécuter les pods.

On notera aussi que je désactive l'installation de Traefik, en effet, nos nœuds étant des masters, ils ne sont pas censés exécuter un Ingress Controller.

Les nœuds d'exécution

De la même manière, regardons le contenu du VagrantFile en ce qui concernant nos nœuds d'exécution.

MASTER_COUNT = 3
IMAGE = "ubuntu/bionic64"

...

Vagrant.configure("2") do |config|

...

  (1..NODE_COUNT).each do |i|
    config.vm.define "kubenode#{i}" do |kubenodes|
      kubenodes.vm.box = IMAGE
      kubenodes.vm.hostname = "kubenode#{i}"
      kubenodes.vm.network  :private_network, ip: "10.0.0.#{i+20}"
      kubenodes.vm.provision "file", source: "./.ssh/id_rsa.pub", destination: "/tmp/id_rsa.pub"
      kubenodes.vm.provision "file", source: "./.ssh/id_rsa", destination: "/tmp/id_rsa"
      kubenodes.vm.provision "shell", privileged: true,  path: "scripts/node_install.sh"
    end
  end

...

end

Comme vous pouvez le voir, le contenu est sensiblement le même que pour les masters.

Les seules différences sont donc :

  • Le pool d'IP, nous sommes cette fois entre 10.0.0.21 et 10.0.0.23
  • Le script exécuté pour le provisionning

Voyons justement le provisionning :

#!/bin/sh

# Deploy keys to allow all nodes to connect each others as root
mv /tmp/id_rsa*  /root/.ssh/

chmod 400 /root/.ssh/id_rsa*
chown root:root  /root/.ssh/id_rsa*

cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
chmod 400 /root/.ssh/authorized_keys
chown root:root /root/.ssh/authorized_keys

# Add current node in  /etc/hosts
echo "127.0.1.1 $(hostname)" >> /etc/hosts

# Add kubemaster1 in  /etc/hosts
echo "10.0.0.11  kubemaster1" >> /etc/hosts

# Get current IP adress to launch k3S
current_ip=$(/sbin/ip -o -4 addr list enp0s8 | awk '{print $4}' | cut -d/ -f1)

# Launch k3s as agent
scp -o StrictHostKeyChecking=no root@kubemaster1:/var/lib/rancher/k3s/server/token /tmp/token
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="agent --server https://kubemaster1:6443 --token-file /tmp/token --node-ip=${current_ip}" sh -

Cette fois, nous avons un script beaucoup plus simple!

La première partie consiste toujours à déployer nos clés SSH.

La dernière ligne lance donc K3S en mode "agent" en lui disant de se connecter au nœud kubemaster1 (j'ai choisi ce nœud de manière arbitraire, n'importe lequel des 3 masters aurait fait l'affaire).

Load balancer frontal : Traefik

Enfin, nous déployons notre load balancer frontal.

Cette installation est très basique, vu que nous n'avons pas de contraintes de production, le besoin n'est pas d'être aussi sécurisé qu'un environnement productif, mais simplement d'avoir un load balancer pour reproduire le comportement normal d'un cluster Kubernetes.

Côté Vagrant, on reste encore très basique :

MASTER_COUNT = 3
IMAGE = "ubuntu/bionic64"

...

Vagrant.configure("2") do |config|

...

  config.vm.define "front_lb" do |traefik|
      traefik.vm.box = IMAGE
      traefik.vm.hostname = "traefik"
      traefik.vm.network  :private_network, ip: "10.0.0.30"   
      traefik.vm.provision "file", source: "./scripts/traefik/dynamic_conf.toml", destination: "/tmp/traefikconf/dynamic_conf.toml"
      traefik.vm.provision "file", source: "./scripts/traefik/static_conf.toml", destination: "/tmp/traefikconf/static_conf.toml"
      traefik.vm.provision "shell", privileged: true,  path: "scripts/lb_install.sh"
      traefik.vm.network "forwarded_port", guest: 6443, host: 6443
  end
end

Cette fois, pas de clé SSH, on pousse plutôt la configuration de Traefik que je vais vous décrire ci-dessous.

Puis, on exécute l'installation. De plus, on indique de mapper le port 6443 de l'hôte (ma machine) au 6443 de la machine virtuelle. Cela me permettra de lancer des commandes kubectl depuis mon hôte en passant au travers de Traefik.

Pour Traefik, nous avons deux fichiers de configurations :

  • La configuration statique : Il s'agit là de la configuration de base de Traefik, c'est ici qu'on indiquera le port d'écoute par exemple
  • La configuration dynamique : On retrouve ici les informations que Traefik peut collecter à chaud, notamment la configuration des endpoints.

Voyons donc notre configuration.

Configuration statique :

[entryPoints]
  [entryPoints.websecure]
    address = ":6443"
    [entryPoints.websecure.http.tls]
      [[entryPoints.websecure.http.tls.domains]]
        main = "10.0.0.30"

[providers.file]
  directory = "/tmp/traefikconf/"

[serversTransport]
  insecureSkipVerify = true

On peut donc voir :

  • Que j'écoute en https sur le 6443 (port par défaut de Kubernetes)
  • J'indique ensuite dans quel répertoire se trouve ma configuration dynamique
  • J'indique de ne pas vérifier le certificat, en effet, mon serveur K3S utilise un certificat autosigné par défaut

Ensuite, la configuration dynamique, c'est ici que je vais définir mes endpoints. À noter que toute modification de celle-ci est prise en compte à chaud par Traefik instantanément.

[http]

  [http.routers]
    [http.routers.routerTest]
      service = "k3s"
      rule = "Host(`10.0.0.30`)"

  [http.services]
    [http.services.k3s]
      [http.services.k3s.loadBalancer]
        [[http.services.k3s.loadBalancer.servers]]
          url = "https://10.0.0.11:6443"
        [[http.services.k3s.loadBalancer.servers]]
          url = "https://10.0.0.12:6443"
        [[http.services.k3s.loadBalancer.servers]]
          url = "https://10.0.0.13:6443"

Rien d'original non plus par ici. Dans un premier temps, je définis un service Traefik k3s, auquel j'indique d'envoyer toute requête qui arrive avec l'hôte "10.0.0.30" (l'adresse IP que j'ai définie sur Vagrant).

Ensuite, je définis un loadbalancing sur les 3 masters Kubernetes. Le comportement par défaut de Traefik est le mode "round robbin", ce qui signifie qu'il enverra les requêtes sur chaque nœud les uns après les autres, sans tenir compte d'une éventuelle charge.

Enfin, voyons le script shell d'installation :

#!/bin/sh
# Download and deploy Traefik as a front load balancer
curl https://github.com/containous/traefik/releases/download/v2.2.11/traefik_v2.2.11_linux_amd64.tar.gz -o /tmp/traefik.tar.gz -L
cd /tmp/
tar xvfz ./traefik.tar.gz
nohup ./traefik --configFile=/tmp/traefikconf/static_conf.toml &> /dev/null&

Nous avons là le script le plus simple des trois. Comme on peut le voir, je télécharge directement le binaire Traefik (en version 2.2.11), car Traefik est lui aussi un binaire standalone écrit en GO.

Ensuite, je lance Traefik en le détachant du terminal et en lui indiquant simplement où se trouve son fichier de configuration statique.

Il est temps de lancer tout ça!

Vous pouvez retrouver l'intégralité du code utilisé sur GitHub :

teddy-ferdinand/vagrant-k3s-cluster
Déploiement d’une infrastructure K3S avec Vagrant. Contribute to teddy-ferdinand/vagrant-k3s-cluster development by creating an account on GitHub.

Installer Vagrant

Nous devons tout d'abord installer Vagrant.

Ici, nous avons plusieurs solutions de disponibles :

  • Installation via le gestionnaire de paquet : Souvent la solution la plus simple sous Linux, toutes les distributions n'ont pas forcément Vagrant et il faut noter que la version peut parfois être (très) en retard sur celle du site officielle. Pour ma part, c'est l'installation que j'ai faite.
  • Binaire autonome : Vagrant est un binaire autonome écrit en GO, ce dernier est aussi disponible sur le site officiel, vous pouvez aussi l'installer de cette manière.

Ces deux installations ne changeront rien à la suite, donc choisissez celle qui vous convient le mieux.

Lancer le déploiement

Une fois Vagrant installé, rendez-vous dans le répertoire racine, où se trouve notre fichier VagrantFile. Nous devrions donc avoir une structure similaire à celle-ci:

Nous pouvons maintenant lancer le déploiement avec un simple

vagrant up

Vous devriez normalement voir le déploiement commencer, avec le téléchargement de la box Ubuntu.

De là, le déploiement complet va prendre environ 10 minutes.

Se connecter à notre cluster

Une fois le déploiement terminé, Vagrant vous rend normalement la main.

Nous n'avons plus qu'à configurer le client Kubernetes sur notre poste.

J'ai laissé un petit script qui va vous permettre de télécharger kubectl, et de le configurer pour se connecter à notre cluster, sans toucher à une éventuelle configuration déjà présente sur l'hôte.

Voici son contenu :

#!/bin/sh

# Get kubectl
curl -L https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl -o /tmp/kubectlvagrant
chmod +x /tmp/kubectlvagrant

# Get password from master config file
PASSWORD=$(vagrant ssh kubemaster1 -c "sudo grep password /etc/rancher/k3s/k3s.yaml" | awk -F':' '{print $2}' | sed 's/ //g')

#Create kubectl config
cat << EOF > /tmp/kubectlvagrantconfig.yml
apiVersion: v1
clusters:
- cluster:
    server: https://10.0.0.30:6443
    insecure-skip-tls-verify: true
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: ${PASSWORD}
    username: admin
EOF

# Create temp vars to use kubectl with vagrant
export KUBECONFIG=/tmp/kubectlvagrantconfig.yml
alias kubectl="/tmp/kubectlvagrant"

Comme vous pouvez le voir, ce script va :

  • Télécharger le client kubectl et le placer dans /tmp/kubectlvagrant en le rendant exécutable.
  • Ensuite, nous récupérons le mot de passe de connexion admin de Kubernetes.
  • Nous insérons ensuite ce mot de passe dans un template de configuration kubeconfig. On notera le paramètre "insecure-skip-tls-verify" qui indique d'ignorer le certificat, car une fois encore nous sommes sur un certificat autosigné par Traefik. Le backend est quant à lui ma machine virtuelle portant Traefik.
  • Enfin, je crée une variable d'environnement pour indiquer à kubectl où aller chercher sa configuration et un alias pour que kubectl pointe sur celui que nous avons téléchargé.

Pour utiliser ce script, il suffit de le sourcer

source ./scripts/configure_kubectl.sh

Attention à ne pas exécuter le script, les alias et export ne fonctionneraient pas, il faut bien le sourcer.

Pour revenir à l'état initial, il suffit de rouvrir un nouveau terminal.

Il est temps de tester notre cluster!

Une fois notre script exécuté, essayons de voir nos nœuds.

kubectl get nodes -o wide

Il est possible que cette requête prenne un peu plus de temps que la normale vu que le cluster reçoit sa première connexion.

Bonne nouvelle! Nous avons bien nos 3 masters et 3 nœuds d'exécution!

Nous allons faire un petit test de base, à savoir déployer un 3 pods Nginx.

kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/master/content/fr/examples/controllers/nginx-deployment.yaml

En vérifiant les pods déployés, nous voyons donc 3 pods : 1 sur chacun de nos nœuds d'exécution Kubernetes.

kubectl get pods -o wide

Au bout d'une trentaine de secondes, ils passeront en "running"

Comme vous pouvez le voir, vous pouvez maintenant utiliser Kubernetes de manière similaire à un serveur de production.

Une fois que vous n'avez plus besoin du cluster, vous pouvez le détruire avec un simple

vagrant destroy -f

Au bout d'une petite minute, toutes vos machines sont maintenant détruites

Pour terminer

Comme vous pouvez le voir, Vagrant permet de créer aisément des environnements de travail, ce qui permet de faciliter les développements ou tests d'applications.

J'ai aussi fait le choix d'utiliser Traefik en tant que load balancer pour montrer qu'il existe aussi un binaire Traefik. En effet, on parle très souvent de son image Docker, mais c'est avant tout un binaire en GO.

De même, il est assez facile de créer des infrastructures cohérentes à l'aide de Vagrant pour reproduire un environnement de production.

Les avantages sont ceux que j'ai cités avant :

  • C'est rapide (10 minutes pour créer un cluster complet)
  • Ça ne coûte rien, vu que l'hôte est votre machine
  • Ça se transmet facilement, par exemple au sein d'une équipe

Si vous souhaitez en savoir plus sur Vagrant, je vous conseille la playlist YouTube de Xavki qui explore bien l'outil :

TUTORIALS VAGRANT - EXEMPLES
Quelques exemples utiles pour apprendre à utiliser vagrant

Si vous souhaitez en savoir plus sur K3S, vous pourrez trouver du contenu sur le blog de WeScale :

k3s - Le blog des experts WeScale - DevOps, Cloud et passion

Comme toujours, n'hésitez pas à commenter! Que pensez-vous de Vagrant + K3S pour avoir un cluster Kubernetes local ?