Proxmox v6 : Cluster K8S kubeadm via Terraform & Cloud-Init

Proxmox v6 : Cluster K8S kubeadm via Terraform & Cloud-Init

22 mai 2020 6 Par Mairien Anthony

Aujourd’hui, un peu de tout ! Terraform, Cloud-Init, Proxmox, Kubernetes, et un lab bien sympa !

Étant toujours dans mon apprentissage de Docker/K8s/Devops en général, je me suis dit que monter un petit cluster Kubernetes sur un bon vieux Proxmox serait sympathique, d’autant plus que j’avais déjà créé un article traitant de k8s mais c’était via Minikube, autant dire que c’était très sommaire. Ici, malgré que je ne sois pas encore un pro, on va essayer d’avancer un peu plus loin et de se monter un « vrai » cluster 😉

Commençons donc directement !

I) Le lab en détails

Ici nous allons donc partir sur un cluster comprenant 3 hôtes, un master et deux workers. Plutôt que de créer à la main et configurer nos 3 vms, je me suis dit que j’allais un peu automatiser ça.

On va donc, dans l’ordre :

  • Créer une template de machine virtuelle basée sur une image
    Cloud-Init de Debian 10 sur notre Proxmox ;
  • Créer un script Terraform pour déployer X fois notre template, en configurant une IP et tout ce qui va bien à chaque fois (installation des utilitaires de base, de docker, kubectl…) ;
  • Ensuite seulement nous mettrons en place notre cluster Kubernetes ;

II) Création du template via Cloud-Init

Ici pour la première étape nous allons télécharger un disque Cloud-Init d’une Debian 10 pré-configurée :

wget https://cdimage.debian.org/cdimage/openstack/current-10/debian-10-openstack-amd64.qcow2

Ah oui, juste ! Qu’est-ce que Cloud-Init avant toute chose… et bien je vais essayer de décrire cet outil assez grossièrement, car je ferai sûrement d’autres articles qui en parleront plus en détails, mais retenez ceci :

Cloud Init est un package (un binaire) permettant de réaliser la configuration initiale d’un template de machine virtuelle ou de conteneur. Par exemple, une fois exécuté il pourra changer le nom d’hôte, l’adresse IP, installer tel ou tel paquet, importer telle clé SSH, etc. Plus besoin donc de se connecter manuellement sur 100 VMs pour exécuter son script Bash fait maison…

Ensuite, il ne nous reste plus qu’à créer notre VM qui servira de template en CLI, en lui greffant le disque virtuel Cloud-Init précédemment téléchargé :

# Création d'une VM avec ID 1000 nommée debian-10-template, 2Go RAM, 1 proco, 2 coeurs
qm create 1000 -name debian-10-template -memory 2048 -net0 virtio,bridge=vmbr0 -cores 2 -sockets 1 -cpu cputype=kvm64 -kvm 1 -numa 1
# Import du disque
qm importdisk 1000 debian-10-openstack-amd64.qcow2 local-lvm
# Attachement du disque à la VM
qm set 1000 -scsihw virtio-scsi-pci -virtio0 local-lvm:vm-1000-disk-0
# Ajout lecteur CD pour cloudinit
qm set 1000 -ide2 local-lvm:cloudinit
qm set 1000 -serial0 socket
# Boot sur disque principal
qm set 1000 -boot c -bootdisk virtio0
qm set 1000 -agent 1
qm set 1000 -hotplug disk,network,usb,memory,cpu
# 2 cores x1 sockets = 2 Vcpus
qm set 1000 -vcpus 2
qm set 1000 -vga qxl
# Copie clé SSH publique de l'hôte proxmox-01
qm set 1000 --sshkey ~/.ssh/id_rsa.pub
# Passage en template
qm template 1000
# Passage taille du disque à 20Go et non plus 2Go
qm resize 1000 virtio0 +18G

N’étant pas un pro des commandes qm, j’avoue que certaines me sont encore un peu obscures, désolé 😅

A partir de là, on est bon !

Concernant la configuration IP, le user/password etc ce sera fait via notre script Terraform.

III) Installation et création du script Terraform

Ici je ne vais pas trop m’étendre sur ce qu’est Terraform et sur son installation complète et détaillée, sachez simplement que ce produit vient aussi d’Hashicorp, la même société qui propose Vagrant, et grosso modo :

  • Vagrant, permet de déployer une infra mais orientée pour le développement et non la production ;
  • Terraform, permet de déployer une infra mais orientée vers le Cloud et la production ;

Ici c’est du on-premise mais étant donné que je n’avais jamais testé Terraform auparavant, je me suis dit go for it 😛

Donc, pour l’installer :

sudo apt-get install unzip
wget https://releases.hashicorp.com/terraform/0.12.18/terraform_0.12.18_linux_amd64.zip
unzip terraform_0.12.25_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform --version 

Vérifiez bien entendu la version à télécharger pour votre wget, et le tour est joué !

Ensuite, comme pour Vagrant, Terraform utilise des Providers pour déployer ses vm/conteneurs. Ici, on va installer manuellement le provider Proxmox car par défaut il n’est pas inclus.

La première étape est donc d’installer le langage Go (ici je suis sur une Ubuntu 20.04, à vous d’adapter selon votre OS) :

sudo apt install golang

Et là on peut installer notre provider Proxmox (l’installation peut prendre plusieurs minutes) :

go get -v github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provider-proxmox
go get -v github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provisioner-proxmox
go install -v github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provider-proxmox
go install -v github.com/Telmate/terraform-provider-proxmox/cmd/terraform-provisioner-proxmox
sudo cp terraform-provider-proxmox /usr/local/bin/
sudo cp terraform-provisioner-proxmox /usr/local/bin/

Il faudra simplement réaliser un terraform init pour « reload » le tout, et prendre en charge nos deux nouveaux plugins.

Bien, une fois fait on peut se placer dans un dossier prévu à cet effet et créer notre fichier main.tf qui contiendra donc toutes les instructions pour déployer nos VMs. Ici aussi, je ne vais pas détailler l’entiéreté du script car ce serait trop long, le but de l’article est d’avant tout déployer un cluster k8s je vous rappelle 😅

Soit, voici notre script :

# Script permettant de déployer X VMs sur hôte Proxmox via Cloud-Init & Terraform
# Mairien Anthony - Notamax
# Mis à jour le 20-05-20
# Définition du provider (ici proxmox-01)
provider "proxmox" {
    pm_api_url = "https://proxmoxIP:8006/api2/json"
    pm_user = "root@pam"
    pm_password = "SuperPaSSw0rD"
    # Laisser à "true" si certificat auto-signé
    pm_tls_insecure = "true"
}

# Création variable pour nombre de VMs à déployer (récupéré via l'argument -var 'nombre=X')
variable "nombre" {
  type = number
}

# Définition de la VM à créer
resource "proxmox_vm_qemu" "proxmox_vm" {
  count             = var.nombre
  name              = "vm-0${count.index}"
  # Nom du node sur lequel le déploiement aura lieu
  target_node       = "pve-01"
  clone             = "debian-10-template"
  full_clone        = true
  os_type           = "cloud-init"
  cores             = 2
  sockets           = "1"
  cpu               = "host"
  memory            = 2048
  scsihw            = "virtio-scsi-pci"
  bootdisk          = "scsi0"
disk {
    id              = 0
    size            = 20
    type            = "scsi"
    storage         = "local-lvm"
    storage_type    = "lvm"
    iothread        = true
  }
network {
    id              = 0
    model           = "virtio"
    bridge          = "vmbr0"
  }

# Configuration relative à CloudInit
  # Clé SSH publique
  sshkeys =  <<EOF
  MaSuperCléSSH
  EOF

  # Setup de l'IP statique
  ipconfig0 = "ip=192.168.1.10${count.index + 1}/24,gw=192.168.1.1"
  ciuser = "it-anthony"
  cipassword = "SuperPaSSw0rD"
  
  # Déclaration du script de démarrage, en utilisant user it-anthony + clé SSH privée
  provisioner "file" {
    source      = "~/Documents/Terraform/startup.sh"
    destination = "/tmp/startup.sh"
      connection {
      type     = "ssh"
      user     = "it-anthony"
      private_key     = "${file("~/.ssh/id_rsa")}"
      host     = "${self.ssh_host}"
   }
  }
  # Exécution du script de démarrage
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/startup.sh",
      "/tmp/startup.sh",
    ]
    connection {
      type     = "ssh"
      user     = "it-anthony"
      private_key     = "${file("~/.ssh/id_rsa")}"
      host     = "${self.ssh_host}"
   }
  }
}

Je pense que le tout est assez bien commenté, et au passage, car je ne pense pas l’avoir dit en introduction, tous les scripts présentés ici sont d’ores et déjà disponibles sur mon Github, histoire de vous faciliter la vie :

https://github.com/IT-Anthony/terraform-deploy-vms

Bien, avant de déployer notre infrastructure il convient de créer un petit script bash permettant de faire les traditonnels apt-get update/apt-get update ainsi que l’installation de Docker :

#!/bin/bash
# startup.sh
# Mairien Anthony - Notamax
# Mis à jour le 20-05-20
# Les "sleep" sont obligatoires sinon délais trop courts et plantage

# Mise à jour des dépôts & paquets
sleep 30 && sudo apt-get update && sleep 10 && sudo apt-get upgrade -y && sleep 30 && sudo apt-get dist-upgrade -y && sleep 30

# Installation dépendances pour Docker
sudo apt -y install software-properties-common apt-transport-https ca-certificates curl gnupg2 software-properties-common && sleep 30

# Ajout du dépôt Docker et de la clé 
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"
   
# Ajout du dépôt Kubernetes et de la clé puis installation de kubelet/kube-adm/kubenetes-cni
sudo curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update && sleep 10 && sudo apt-get install -y kubelet kubeadm kubernetes-cni

# Seconde mise à jour des dépôts & installation Docker + paquets de base + démarrage docker au boot
sleep 30 && sudo apt-get update && sleep 10 && sudo apt -y install docker-ce docker-ce-cli htop vim qemu-guest-agent && sudo systemctl enable docker && sudo systemctl start docker

Je sais je sais, on se passera de commentaires sur ce script basique en bash… l’important est que ça fonctionne haha.

Ensuite pour déployer notre infrastructure, il ne nous reste plus qu’à exécuter un terraform apply avec comme argument la variable nombre ainsi que le nombre de VMs à créer :

terraform apply -var 'nombre=3'

On patiente un peu, et le tour est joué !

Tadaaaa ! Nous avons donc déployé 3 machines virtuelles basées sur Debian 10 avec une installation automatique des mises à jour ainsi que de Docker ! Pas mal non ?

Mais bref, passons enfin à ce cluster Kubernetes…

IV) Création du cluster Kubernetes

Ici nous allons donc utiliser kubeadm, qui est un utilitaire permettant de déployer rapidement et facilement son cluster. La première étape est donc d’installer Docker sur chaque noeud et désactiver la mémoire Swap (étant donné que nous avons utilisé une image OpenStack ainsi qu’un script shell plus haut, tout ça est déjà fait).

Il faut simplement éditer manuellement le fichier /etc/cloud/cloud.cfg de nos 3 VMs pour passer en commentaire le paramètre update_etc_hosts, et ensuite changer le hostname via un rapide hostnamectl set-hostname (on aurait pu l’automatiser ça aussi, mais bon…).

Une fois fait, nous obtenons la chose suivante :

  • master-01, en 192.168.1.101/24 ;
  • worker-01, en 192.168.1.102/24 :
  • worker-02, en 192.168.1.103/24 ;

On se connecte donc sur le master pour commencer et démarrer notre installation :

sudo kubeadm init --apiserver-advertise-address=192.168.1.101 --node-name $HOSTNAME --pod-network-cidr=10.244.0.0/16

Cette commande va donc permettre de renseigner l’adresse IP de notre master, puis de lui comme nom « master-01 » via la variable $HOSTNAME et enfin la partie pod-network permet de spécifier à K8S le réseau à utiliser pour la gestion des pods. Ici nous allons utiliser Flannel pour la gestion du réseau, il convient donc d’utiliser le range 10.244.0.0/16 spécifiquement.

On patiente un peu, puis nous allons ensuite créer un fichier de configuration que nous utiliserons via l’utilitaire kubectl, et qui permet de gérer le cluster (on notera d’ailleurs que les commandes sont données par l’installateur lui-même).

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

On se doit ensuite de modifier les paramètres bridge pour permettre à K8S de correctement utiliser le réseau de la machine (à réaliser sur nos trois VMs) :

sudo sysctl net.bridge.bridge-nf-call-iptables=1

Et enfin on déploie notre fameux pod Flannel qui servira à la gestion du réseau, comme dit précédemment :

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Promis, la partie « config bien longue » est presque terminée ! Il faut simplement exécuter une dernière petite commande pour modifier l’adressage réseau de Flannel et éviter des petits soucis par la suite :

kubectl edit cm -n kube-system kube-flannel-cfg

Cette commande nous ouvre l’éditeur Vim et nous permet de modifier précisément ce paramètre, changez-le en 10.10.0.0/16 :

net-conf.json: |
{
 "Network": "10.0.0.0/16",
 "Backend": {
   "Type": "vxlan"
 }
}

Une fois fait, on peut d’ores et déjà se connecter sur notre worker-01 et worker-02 et les faire rejoindre le cluster :

sudo kubeadm join 192.168.1.101:6443 --token gunt7b.xs5qti51ysbgizxb \
    --discovery-token-ca-cert-hash sha256:ba1269a6f0e2f007f2f06a912b372e20df8ac9180dba3390f2b3293af8cfd0f2 

Bien entendu cette commande sera différente de votre côté, déjà au niveau du token mais même au niveau de l’IP du master.

Si vous désirez rajouter un node après que le token ait expiré, car il n’a une durée de validité que de quelques minutes, vous pouvez exécuter cette commande :

kubeadm token create --print-join-command

Ensuite on peut réaliser le classique kubectl get nodes pour voir les membres du cluster, et même rajouter un label « worker » à nos deux workers justement via la commande kubectl lable node worker-01 node-role.kubernetes.io/worker=worker :

Bien, nos membres sont donc tous ready ! On va pouvoir déployer un rapide conteneur Nginx puis nous l’exposerons en dehors du cluster pour vérifier que tout soit ok.

V) Conteneur Nginx pour tests et vérificaton

Pour créer notre conteneur Nginx et l’exposer en dehors du cluster, rien de plus simple !

kubectl create deployment web-01 --image=nginx

Ici on créer donc un deployment nommé web-01 et basé sur l’image Nginx.

On peut vérifier qu’il est bien en status Running via la commande kubectl get pods :

Niquel, on poursuit !

Ensuite on va créer un service de type NodePort et qui nous permettra donc de toucher le Nginx depuis l’extérieur du cluster :

kubectl create service nodeport web-01 --tcp=80:80

Ici le port 80 pointe sur le port 80 du conteneur, mais on aurait très bien pu prendre un port aléatoire de l’hôte, le principe est le même.

Un petit kubectl get svc et on peut voir que notre service a bien un port-forwarding d’actif :

Et ensuite, si l’on se rend sur l’adresse IP du node sur lequel tourne notre conteneur, en rajoutant le port 32629 on devrait arriver sur notre serveur web Nginx :

It works ! D’ailleurs si vous aviez par exemple 200 nodes, il est bon de savoir sur quelle node notre pod a été déployé… pour cela on peut utiliser la commande kubectl describe pods web-01 :

On cherche ensuite la ligne Node: xxx et c’est tout bon.

VI) Conclusion

Et bien c’est déjà la fin de cet article, qui aura été bien plus long à rédiger que ce que je ne pensais… de base je pensais simplement à déployer un petit cluster sur mon Proxmox comme dit en début d’article, puis j’ai eut l’idée de vouloir « un peu automatiser » le tout, mais découvrant tout juste Terraform/CloudInit et même Kubernetes en général, j’ai dû faire face à plusieurs galères… bon le résultat est bien là, les scripts sont fonctionnels et le cluster aussi, mais il est clair que tout ceci peut être largement amélioré, c’est avant tout un lab donc le but est bel et bien d’expérimenter, alors ne me jugez pas trop sévèrement haha 😅

J’espère tout de même vous avoir appris quelques bricoles, et je vous souhaite une bonne journée/soirée !