Tutoriel : Comment déployer Kubernetes chez Exoscale avec Packer et Terraform ? Création d'une image de machine virtuelle (2/3)

Publié le

Dans le précédent article, nous avions mis en place le nécessaire pour héberger le code de notre infrastructure dans un dépôt git. Maintenant que nous avons une base solide pour versionner ce code, nous pouvons commencer à réfléchir à l'installation de notre infrastructure.

Cette installation va se faire avec Kubeadm, pour des raisons de simplicité. Une procédure d'installation basée sur cet outil a déjà été décrite dans cet article "dans le cadre de Vagrant" (https://easyadmin.tech/tutoriel-comment-installer-kubernetes-cri-o-avec-kubeadm).

Les fonctionnalités attendues sont les suivantes :

  • un nœud de control-plane (pas de haute disponibilité sur le control-plane)
  • un nombre de workers variable, facilement modifiable
  • si possible l'autoscaling du cluster

Nous n'allons pas aborder les fonctionnalités suivantes :

  • le stockage via le mécanisme de PV/PVC
  • l'installation d'un Ingress Controller

L'installation d'un Ingress Controller ou d'une solution de stockage est générique à tous clusters Kubernetes. Dans le cas d'Exoscale, il n'y a pour le moment pas d'offre de block storage, donc pas d'intégration particulière avec Kubernetes de ce côté là.

Installation des prérequis :

Nous partirons de l'image de base Ubuntu 20.04 fournie par Exoscale, à laquelle nous allons rajouter les paquets suivants : kubeadm, kubelet, kubectl, et crio.

Nous optons pour la mise en place d'un réseau privé pour la communication entre les nœuds du cluster. Le cloud d'Exoscale permet de réserver de tels réseaux. Ils se comportent comme si les instances qui y sont connectées étaient reliées en Ethernet, ce sont des segments de niveau 2, d'après la documentation.

Il est facile de gérer l'adressage avec le mode managé. Dans ce mode, un serveur DHCP géré par Exoscale fournit une IP automatiquement à nos machines virtuelles (il suffit alors de configurer l'interface réseau eth1 pour qu'elle récupère sa configuration via DHCP). Avec ce système, il est possible d'attribuer une IP statique. C'est ce que nous ferons pour le control-plane. Les workers auront une IP définie dans un pool. Il faut donc prévoir la configuration de l'interface privée en DHCP.

Préparation de l'image

Le processus de création de l'image nécessite l'accès à votre compte Exoscale, et cette image sera liée à une zone. Nous définissons donc les deux variables pour l'API Exoscale, et une variable pour la zone dans le fichier packer/kubernetes.pkr.hcl :

variable "api_key" {
}

variable "api_secret" {
}

variable "zone" {
}

Vient ensuite la source. Nous partons de l'image Ubuntu 20.04 et le nom de l'image finale sera Kubernetes 1.20 - Linux Ubuntu 20.04 LTS 64-bit :

source "exoscale" "base" {
    api_key = var.api_key
    api_secret = var.api_secret
    instance_template = "Linux Ubuntu 20.04 LTS 64-bit"
    instance_disk_size = 10
    template_zone = var.zone
    template_name = "Kubernetes 1.20 - Linux Ubuntu 20.04 LTS 64-bit"
    template_username = "ubuntu"
    ssh_username = "ubuntu"
}

Nous préparons ensuite quelques autres fichiers de configuration. Ils sont nécessaires pour le bon fonctionnement de Kubernetes, et la mise en place du cluster avec Kubeadm :

Le fichier packer/kubernetes/etc/default/kubelet est nécessaire lors du bootstrap du cluster :

KUBELET_EXTRA_ARGS=--cgroup-driver=systemd --container-runtime=remote --container-runtime-endpoint="unix:///var/run/crio/crio.sock"

Les modules pour la CRI sont à définir dans packer/kubernetes/etc/modules-load.d/cri-o.conf :

overlay
br_netfilter

Nous définissons quelques paramètres sysctl supplémentaires pour la CRI, dans packer/kubernetes/etc/sysctl.d/99-kubernetes-cri.conf :

net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1

Pour plus d'information au sujet de cri-o.conf et 99-kubernetes-cri.conf, vous pouvez aller consulter les liens suivants :

La mise en place du DHCP sous Ubuntu 20.04 passe par netplan et nécéssite la création d'un fichier de configuration pour eth1 dans packer/kubernetes/etc/netplan/eth1.yaml :

# Private network configuration:
#   - On control-plane node, the IP is statically set via Exoscale Terraform provider, from the var.control_plane_ip_address variable.
#   - On worker nodes, the IP is dynamically set by the managed private network from Exoscale.
network:
  version: 2
  ethernets:
    eth1:
      dhcp4: true

Lorsque l'interface est "UP", la mise à jour de la configuration du Kubelet pour utiliser l'interface privée va se faire par un script appelé par networkd-dispatcher. C'est le script packer/kubernetes/etc/networkd-dispatcher/routable.d/50-ifup-hooks :

#!/bin/sh
# Private network up hook: set the Kubelet's node IP

set -e

PRIVATE_IP=$(ip -f inet addr show eth1 |awk '/inet / {print $2}' |cut -d/ -f1)
echo "KUBELET_EXTRA_ARGS=--node-ip=$PRIVATE_IP" > /etc/default/kubelet

Sans cet ajustement, le kubelet va tenter d'utiliser eth0 pour communiquer avec le control-plane, ce qui n'est pas souhaitable.

Lors de son exécution, ce script va écraser le fichier par défaut (/etc/default/kubelet) qui a été créé plus tôt. Ce n'est pas un souci, parce qu'une fois que Kubeadm a fait rejoindre un nœud dans le cluster, ce fichier n'est plus nécessaire, nous avons donc la possibilité de le réutiliser pour nos propres besoins.

Un autre script va permettre de mettre en place un providerID et une annotation définissant la zone sur chaque nœud, quand ce dernier rejoindra le cluster. Nous en profitons aussi pour ajouter le label node-role.kubernetes.io/worker=worker.

Le script se trouve dans packer/kubernetes/usr/local/bin/exo-set-worker-node :

#!/bin/sh
# Helper script called by worker nodes when joining the cluster via SSH.
# This script set the worker node's providerID, and the region (zone) of this node.

set -e

if [ "$#" -ne 3 ]; then
    echo "usage: set-worker-node <node-hostname> <vm-id> <zone>"
    exit 2
fi

VM_HOSTNAME=$1
VM_ID=$2
VM_AVAILABILITY_ZONE=$3

PATCH={\"spec\":{\"providerID\":\"exoscale://$VM_ID\"}}
kubectl --kubeconfig /root/.kube/config patch node $VM_HOSTNAME -p $PATCH
kubectl --kubeconfig /root/.kube/config label node $VM_HOSTNAME node-role.kubernetes.io/worker=worker
kubectl --kubeconfig /root/.kube/config label node $VM_HOSTNAME topology.kubernetes.io/region=$VM_AVAILABILITY_ZONE

Pour faire simple, ces informations vont permettre au Cluster Autoscaler de faire le lien entre un nœud worker de Kubernetes et une machine virtuelle dans le cloud d'Exoscale. Normalement, cette tâche devrait être accomplie par le Cloud Controller Manager, mais la partie "Node Controller" de celui-ci ne semble pas encore fonctionner dans le cas d'Exoscale.

Tous ces fichiers vont être copiés dans l'image de machine virtuelle. Pour ça, nous ajoutons les directives suivantes dans le fichier packer/kubernetes.pkr.hcl :

build {
    sources = ["source.exoscale.base"]

    provisioner "file" {
        source = "/root/kubernetes/etc/default/kubelet"
        destination = "/tmp/etc_default_kubelet"
    }

    provisioner "file" {
        source = "/root/kubernetes/etc/modules-load.d/cri-o.conf"
        destination = "/tmp/etc_modules-load.d_cri-o.conf"
    }

    provisioner "file" {
        source = "/root/kubernetes/etc/netplan/eth1.yaml"
        destination = "/tmp/etc_netplan_eth1.yaml"
    }

    provisioner "file" {
        source = "/root/kubernetes/etc/networkd-dispatcher/routable.d/50-ifup-hooks"
        destination = "/tmp/etc_networkd-dispatcher_routable.d_50-ifup-hooks"
    }

    provisioner "file" {
        source = "/root/kubernetes/etc/sysctl.d/99-kubernetes-cri.conf"
        destination = "/tmp/etc_sysctl.d_99-kubernetes-cri.conf"
    }

    provisioner "file" {
        source = "/root/kubernetes/usr/local/bin/exo-set-worker-node"
        destination = "/tmp/usr_local_bin_exo-set-worker-node"
    }

    # update system and install required components for Kubernetes
    provisioner "shell" {
        environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
        inline = [
            # fix most warnings from apt during image preparation
            "echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections",

            # run unattended upgrade and wait for it completion
            "sudo systemd-run --property='After=apt-daily.service apt-daily-upgrade.service' --wait /bin/true",

            # update system
            "sudo apt-get update",
            "sudo apt-get upgrade -y",

            # required for custom apt repositories
            "sudo apt-get install -y dialog apt-utils curl gnupg2 software-properties-common apt-transport-https ca-certificates",

            # Network configuration
            "sudo mv /tmp/etc_netplan_eth1.yaml /etc/netplan/eth1.yaml",
            "sudo mv /tmp/etc_networkd-dispatcher_routable.d_50-ifup-hooks /etc/networkd-dispatcher/routable.d/50-ifup-hooks",
            "sudo chown root:root /etc/networkd-dispatcher/routable.d/50-ifup-hooks",
            "sudo chmod 0700 /etc/networkd-dispatcher/routable.d/50-ifup-hooks",

            # Helper script
            "sudo mv /tmp/usr_local_bin_exo-set-worker-node /usr/local/bin/exo-set-worker-node",
            "sudo chown root:root /usr/local/bin/exo-set-worker-node",
            "sudo chmod 0700 /usr/local/bin/exo-set-worker-node",

            # custom Kubernetes CRI & network configuration
            "sudo mv /tmp/etc_sysctl.d_99-kubernetes-cri.conf /etc/sysctl.d/99-kubernetes-cri.conf",
            "sudo mv /tmp/etc_modules-load.d_cri-o.conf /etc/modules-load.d/cri-o.conf",
            "sudo mv /tmp/etc_default_kubelet /etc/default/cri-o.conf",
            "sudo mv /tmp/etc_modprobe.d_kubernetes-blacklist.conf /etc/modprobe.d/kubernetes-blacklist.conf",
        ]
    }
}

Ce script Packer démarre un machine virtuelle, et envoie les fichiers que nous avons créés dans /tmp, puis les déplace aux bons endroits.

Nous passons ensuite à l'installation de CRI-O, kubelet, et kubeadm :

build {
    # ...

    # update system and install required components for Kubernetes
    provisioner "shell" {
        environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
        inline = [
            # ...
            
            # install CRI-O (as a replacement for Docker)
            "curl -s https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/Release.key | sudo apt-key add -",
            "curl -s https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.20/xUbuntu_20.04/Release.key | sudo apt-key add -",
            "sudo apt-add-repository \"deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04 /\"",
            "sudo apt-add-repository \"deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.20/xUbuntu_20.04/ /\"",

            "sudo apt-get update",
            "sudo apt-get install -y cri-o cri-o-runc cri-tools cri-o-runc runc",
            
            "sudo systemctl daemon-reload",
            "sudo systemctl start crio",
            "sudo systemctl enable crio",

            # install Kubernetes and Kubeadm components
            "curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -",
            "sudo apt-add-repository \"deb https://apt.kubernetes.io/ kubernetes-xenial main\"",

            "sudo apt-get update",
            "sudo apt-get install -y kubectl=1.20.0-00 kubeadm=1.20.0-00 kubelet=1.20.0-00",

            # preload Kubernetes container images
            "sudo kubeadm config images pull",

            # removed because of conflicts with (later) calico installation
            "sudo rm /etc/cni/net.d/100-crio-bridge.conf"
        ]
    }
}

Création de l'image

Pour information, le code du projet est disponible sur GitHub.

Nous pouvons enfin créer l'image de machine virtuelle :

export EXOSCALE_API_KEY=EXOxxxxxxxxxxxxxxxxxxxxxxxx
export EXOSCALE_API_SECRET=my-secret

make packer.deps
make packer.build

Chez moi, la commande build met un peu plus de 14 minutes. Nous obtenons après ce délai la sortie suivante :

2021-02-10-Packer-Kubernetes-Template.png

Il faut bien mettre de côté l'ID de la machine virtuelle, car cette information sera importante pour la mise en place de l'infrastructure

Dans l'interface de gestion d'Exoscale, l'image de machine virtuelle est bien présente :

2021-02-10-Exoscale-Kubernetes-Template.png

Le stockage de cette image est facturé au tarif de l'offre "Simple Object Storage". C'est à dire 0.01800 €/GB/mois * 10 GB/mois = 0,18 €/mois (information à confirmer).

A suivre

Dans la dernière partie de ce tutoriel, nous verrons comment déployer cette image dans une véritable infrastructure, avec Terraform.
Nous utiliserons le mécanisme de cloud-init pour mettre en place le control-plane, et pour rejoindre celui-ci à partir des workers.

PHILIPPE CHEPY

Administrateur Système et Développeur