Créer des boxes Vagrant facilement en utilisant Packer

Créer des boxes Vagrant facilement en utilisant Packer

Il y a quelques mois, j’ai écrit un billet pour expliquer comment créer facilement un cluster Kubernetes local en exploitant Vagrant et Traefik. Vous pouvez d’ailleurs le retrouver ici :

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émentadapté. Il est possible de monter son infrastructure Kubernetes su…

Aujourd’hui, je vous propose de voir comment on peut accélérer encore cette création en construisant nous-mêmes la box utilisée par Vagrant, préconfiguré avec nos outils. Ce billet est donc la suite de celui cité au-dessus. Certaines notions ne seront donc pas de nouveau abordées.

Packer, qu’est-ce que c’est ?

Packer, c’est aussi un outil HashiCorp, comme Vagrant. Son rôle est de packager (d’où son nom) des machines virtuelles.

Il permet ainsi de créer des AMI AWS, des images Docker, des machines virtuelles VirtualBox et j’en passe. Vous pouvez retrouver la liste complète des providers gérés par Packer ici.

Les forces principales de Packer sont ainsi :

  • La simplicité de la configuration : un simple fichier JSON ou HCL permet de décrire le build voulu
  • La parallélisation : on peut créer la même image sur plusieurs providers en parallèle, très utile dans une approche multicloud
  • La reproductibilité : Il est simple de pouvoir recréer une image OS from scratch en repartant uniquement des fichiers Packer, ce qui permet de partager uniquement ces fichiers, plutôt que des images volumineuses

Aujourd’hui, nous allons donc créer une box Vagrant en partant de l’ISO de base Ubuntu serveur.

Pourquoi utiliser Packer pour créer des boxes Vagrant ?

Si on revient sur mon précédent billet, nous avions vu comment démarrer rapidement un cluster K3S avec Vagrant. Toutefois, sur chacune des machines virtuelles :

  • Je devais télécharger K3S ou Traefik
  • Je devais faire ma configuration SSH (pour le trust entre les nœuds)
  • Je ne pouvais pas m’assurer que la version de K3S et de l’ensemble des binaires était rigoureusement la même d’une exécution à l’autre. (Je reviendrais sur ce point)

Du coup, pour résoudre ce souci, je vais pouvoir, en exploitant Packer, créer des box directement configurées pour répondre à mon besoin.

Cela me permettra de démarrer plus rapidement mon cluster et de ne pas devoir tout télécharger et configurer à chaque lancement.

Quelles sont les étapes de la création de la box ?

Dans un premier temps, nous allons créer un fichier de configuration pour Packer. Ce fichier peut être écrit en JSON ou en HCL (depuis peu), le langage propre aux outils HashiCorp. Pour ce projet j’ai fait le choix, arbitrairement, de choisir le HCL. Il est à noter que le JSON peut réaliser exactement les mêmes actions. Ce fichier permettra d’indiquer l’ISO de base que nous allons utiliser, ainsi que toutes les actions nécessaires jusqu’à la création de la box.

Ensuite, nous allons fournir ce fichier de configuration en entrée de Packer qui fera les opérations suivantes :

  • Téléchargement de l’image de l’OS demandée
  • Contrôle du checksum de l’image
  • Création d’une machine virtuelle pour lancer l’image
  • Exécution des commandes d’installation
  • Exécution des commandes de provisionning de la machine
  • Arrêt de la VM
  • Export en tant que box Vagrant
  • Suppression de la VM temporaire

Ces opérations vont être réalisées de manière complètement transparente, de notre côté, nous ne lancerons qu’une seule commande.

Les mains dans le cambouis !

En préambule, vous pouvez retrouver l'ensemble des scripts décrits ci-dessous sur le repository associé :

teddy-ferdinand/packer-vagrant-k3s-cluster
Contribute to teddy-ferdinand/packer-vagrant-k3s-cluster development by creating an account on GitHub.

Dans un premier temps, il vous faudra Packer et Vagrant. Pour Vagrant, je vous invite à regarder mon post précédent pour savoir comment l’installer. Concernant Packer, je vous invite à regarder la page dédiée sur le site de l’éditeur afin d’avoir l’installation la plus adaptée à votre système d’exploitation.

Pour réaliser ma box, j’ai choisi d’avoir 3 fichiers :

├── http
│   └── preseed.cfg
├── packer_installer.sh
└── packer.pkr.hcl

Nous avons donc :

  • Un répertoire http : ce répertoire sera exposé en tant que serveur web par Packer pour pouvoir exploiter directement une installation réseau pour préconfigurer l’OS. Je reviendrais sur ce point dans la configuration.
  • Un script packer_installer.sh : Ce script comporte le provisionning de base de l’image, c’est-à-dire l’installation de K3S, traefik et le déploiement de mes clés SSH
  • Un fichier de configuration packer.pkr.hcl : Ce fichier est le fichier de configuration que nous allons exploiter avec Packer pour créer notre box. Attention : l’extension .pkr.hcl est nécessaire pour que Packer détecte le bon format, sans quoi il essaiera de le traiter en tant que JSON.

La configuration Packer

Commençons par le fichier de configuration Packer. C’est ce dernier qui portera donc la définition même de notre box.

Dans un premier temps, nous avons donc la première section : la source. Comme son nom l’indique, cet élément permet d’indiquer de quelle image nous devons partir pour le build, ainsi que la configuration de base (login, commandes à exécuter au boot, etc.).

source "virtualbox-iso" "ubuntu" {
  guest_os_type = "Ubuntu_64"
  iso_url = "http://cdimage.ubuntu.com/ubuntu/releases/bionic/release/ubuntu-18.04.5-server-amd64.iso"
  iso_checksum = "sha256:8c5fc24894394035402f66f3824beb7234b757dd2b5531379cb310cedfdf0996"
  ssh_username = "packer"
  ssh_password = "packer"
  ssh_port= 22
  shutdown_command = "echo 'packer' | sudo -S shutdown -P now"
  http_directory = "http"
  guest_additions_mode = "disable"
  boot_command = [
        "",
        "",
        "",
        "/install/vmlinuz",
        " auto",
        " console-setup/ask_detect=false",
        " console-setup/layoutcode=us",
        " console-setup/modelcode=pc105",
        " debconf/frontend=noninteractive",
        " debian-installer=en_US",
        " fb=false",
        " initrd=/install/initrd.gz",
        " kbd-chooser/method=us",
        " keyboard-configuration/layout=USA",
        " keyboard-configuration/variant=USA",
        " locale=en_US",
        " netcfg/get_domain=vm",
        " netcfg/get_hostname=packer",
        " grub-installer/bootdev=/dev/sda",
        " noapic",
        " preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg",
        " -- ",
        ""
      ]
}

Dans cette partie, nous retrouvons donc :

  • L’image de base : Ubuntu 18.04.5
  • Le checksum de l’image, cela permettra à Packer de contrôler l’intégrité de l’image après son téléchargement
  • Les logins et mots de passe à utiliser en SSH (qui seront utilisés dans la seconde partie)
  • Quelle commande doit être lancée pour éteindre la machine proprement
  • Le répertoire http, cela permet d’exposer un fichier de preseed qui porte la configuration de base de mon image. On peut d’ailleurs le retrouver à la ligne 32.
  • Le fait que je désactive l’installation des guests additions. Les guests additions permettent une meilleure intégration entre l’hôte et la VM, dans notre cas, je n’en ai pas besoin, donc autant ne pas les installer.
  • Enfin, on retrouve les commandes lancées au boot. Ces commandes seront lancées en mode "émulation". Cela signifie que Packer va émuler un clavier pour taper ces commandes dans votre machine virtuelle.

Concernant les commandes lancées au build, je me suis inspiré d’un repository contenant des configurations de base Packer :

geerlingguy/packer-boxes
Jeff Geerling’s Packer build configurations for Vagrant boxes. - geerlingguy/packer-boxes

Toujours dans ce même fichier de configuration, nous avons ensuite un second bloc, le "builder". Comme son nom l’indique, le rôle de ce dernier va être de construire notre box. Voyons son contenu.

build {
    sources = ["source.virtualbox-iso.ubuntu"]

    provisioner "file"{
        sources = [".ssh/id_rsa", ".ssh/id_rsa.pub"]
        destination = "/tmp/"
    }

    provisioner "shell"{
        script = "./packer_installer.sh"
        execute_command = "echo 'packer' | sudo -S -E sh -c '{{ .Vars }} {{ .Path }}'"
    }

    post-processor "vagrant" {
        keep_input_artifact = false
        provider_override = "virtualbox"
    }
}

Ceux qui sont habitués au HCL verront que la source est une référence directe. Cela signifie que Packer résout ainsi nativement un lien de dépendance entre les deux éléments.

Ensuite nous pouvons voir que j’utilise des "provisionners", le rôle de ces éléments est justement de faire le provisionning, il en existe des dizaines. Il est par exemple possible d’exploiter Ansible pour faire sa configuration.

Vous pouvez retrouver la liste complète des provisionners ici : https://www.packer.io/docs/provisioners.

Pour ma part, j’ai choisi d’en mettre deux :

  • Je copie mes clés SSH que j’ai générées dans ma machine virtuelle
  • Je lance ensuite un script d’installation que j’ai fait, notez l’invocation de sudo pour le lancement. Par défaut Packer exécute les commandes via l’utilisateur indiqué en source.

Enfin, j’ai un "post-processor", le rôle de cet élément est donc d’exporter ma box une fois la configuration de ma machine virtuelle terminée. Dans mon cas, l’exporter est Vagrant, ce qui signifiera que packer créera un box vagrant depuis la machine virtuelle Virtualbox. Comme pour les providers et les provisionners, il existe de nombreux post-processors : https://www.packer.io/docs/post-processors

Le script d’installation

Comme nous l’avons vu plus haut, j’ai un petit script Shell qui est invoqué pour effectuer le provisionning de ma machine. Voici le contenu de ce dernier, qui est assez basique.

#!/bin/sh
# Deploy keys to allow all nodes to connect each others as root
mkdir /root/.ssh/
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

# Install updates and curl
apt update
apt install -y curl
# Apt cleanup.
apt autoremove -y
apt update

#  Blank netplan machine-id (DUID) so machines get unique ID generated on boot.
truncate -s 0 /etc/machine-id
rm /var/lib/dbus/machine-id
ln -s /etc/machine-id /var/lib/dbus/machine-id

# Download k3s
curl -L https://get.k3s.io -o /home/packer/k3s
chmod +x /home/packer/k3s

# Download Traefik
curl -L https://github.com/containous/traefik/releases/download/v2.2.11/traefik_v2.2.11_linux_amd64.tar.gz -o /home/packer/traefik.tar.gz
cd /home/packer
tar xvfz ./traefik.tar.gz
rm ./traefik.tar.gz
chmod +x /home/packer/traefik

Dans un premier temps, nous pouvons retrouver le déploiement de ma clé SSH. Quand nous exploitions que Vagrant, c’était lui qui faisait cette installation. Maintenant, Packer le fait pour nous. Cela permettra aussi de se connecter directement en utilisant ces clés.

Ensuite, je mets à jour mes paquets et j’installe curl, car l’image que j’utilise de base est une image "minimale" ne comportant pas ce paquet. Puis je nettoie les paquets inutiles. Outre l’aspect d’une installation propre, cela permet de réduire la taille de ma box.

Un petit nettoyage est ensuite fait pour que notre machine n’ait pas d’identifiant lié à elle, point très important pour Vagrant, sans quoi nous pouvons avoir des conflits entre nos machines virtuelles.

Enfin, je télécharge K3S et Traefik, que je mets à disposition dans /home/packer. Cela permettra à Packer de les utiliser directement.

Le fichier de preseed

Ce fichier est sans doute le plus "brute", mais très important, car c’est lui qui va effectuer l’installation du socle de notre machine.

choose-mirror-bin mirror/http/proxy string
d-i base-installer/kernel/override-image string linux-server
d-i clock-setup/utc boolean true
d-i clock-setup/utc-auto boolean true
d-i finish-install/reboot_in_progress note
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i partman-auto/disk string /dev/sda
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman-auto/method string lvm
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-lvm/device_remove_lvm boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_write_new_label boolean true
d-i pkgsel/include string openssh-server cryptsetup build-essential libssl-dev libreadline-dev zlib1g-dev linux-source dkms nfs-common
d-i pkgsel/install-language-support boolean false
d-i pkgsel/update-policy select none
d-i pkgsel/upgrade select full-upgrade
d-i time/zone string UTC
tasksel tasksel/first multiselect standard, ubuntu-server

d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/layoutcode string us
d-i keyboard-configuration/modelcode string pc105
d-i debian-installer/locale string en_US.UTF-8

# Create packer user account.
d-i passwd/user-fullname string packer
d-i passwd/username string packer
d-i passwd/user-password password packer
d-i passwd/user-password-again password packer
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false
d-i passwd/user-default-groups packer sudo
d-i passwd/user-uid string 900

De manière non exhaustive, on retrouve :

  • La configuration de la langue et de la disposition du clavier
  • Le partitionnement de mes disques
  • Les paquets de bases à installer
  • La création de mon utilisateur "packer"

Lançons le tout !

Une fois que nous avons tous nos fichiers, nous pouvons donc lancer la construction de ma box Vagrant avec packer avec un simple :

packer build packer.pkr.hcl

Comme vous pouvez le voir ci-dessous, ce dernier prend quelques minutes et va effectuer toutes les actions que je vous ai indiquées plus tôt.

Une fois ce dernier terminé, nous avons donc une box "packer_ubuntu_virtualbox.box" dans notre répertoire actuel. Durant l’exécution, nous pouvons voir la fenêtre Virtualbox, c’est un choix que j’ai fait. Il est tout à fait possible de désactiver son affichage en ajoutant

headless = true

dans ma source.

Utiliser cette box dans Vagrant

Maintenant que ma box est créée, je peux l’utiliser dans Vagrant. Les scripts de bases ressemblent beaucoup à ceux d’origine, sauf que :

  • Je ne pars plus d’une box publique Ubuntu, mais de ma box locale
  • Je n’ai plus besoin de déployer mes clés RSA
  • J’utilise la connexion via la clé RSA au lieu du login vagrant natif
  • Je n’ai plus besoin de télécharger K3S et Traefik
  • Je désactive la synchronisation du répertoire Vagrant que je n’utilise pas, vu qu’il nécessite l’utilisation des "guests additions" que je n’ai pas installé

Voici donc le fichier en question :

MASTER_COUNT = 3
NODE_COUNT = 3
IMAGE = "packer_ubuntu_virtualbox.box"

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 "shell", path: "scripts/master_install.sh"
      kubemasters.ssh.username = "root"
      kubemasters.ssh.private_key_path = ".ssh/id_rsa"
      kubemasters.vm.synced_folder '.', '/vagrant', disabled: true
    end
  end

  (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 "shell", path: "scripts/node_install.sh"
      kubenodes.ssh.username = "root"
      kubenodes.ssh.private_key_path = ".ssh/id_rsa"
      kubenodes.vm.synced_folder '.', '/vagrant', disabled: true
    end
  end

  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", path: "scripts/lb_install.sh"
      traefik.vm.network "forwarded_port", guest: 6443, host: 6443
      traefik.ssh.username = "root"
      traefik.ssh.private_key_path = ".ssh/id_rsa"
      traefik.vm.synced_folder '.', '/vagrant', disabled: true
  end
end

Comme vous pouvez le voir, pas d’énormes changements ici par rapport à mon script d’origine.

Les vrais changements sont dans les scripts de déploiement de Vagrant.

Installation de K3S

Installation du master

#!/bin/sh

# 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
    export INSTALL_K3S_EXEC="server --cluster-init --tls-san $(hostname) --bind-address=${current_ip} --advertise-address=${current_ip} --node-ip=${current_ip} --no-deploy=traefik"
    export INSTALL_K3S_VERSION="v1.16.15+k3s1"
    sh /home/packer/k3s
else
    echo "10.0.0.11  kubemaster1" >> /etc/hosts
    scp -o StrictHostKeyChecking=no root@kubemaster1:/var/lib/rancher/k3s/server/token /tmp/token
    export 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"
    export INSTALL_K3S_VERSION="v1.16.15+k3s1"
    sh /home/packer/k3s
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

Comme vous pouvez le voir, plus de déploiement de clés, plus de téléchargement de K3S. Je me contente de configurer mon host et de lancer directement K3S qui est déjà sur ma box.

Les arguments de ligne de commande (INSTALL_K3S_EXEC) sont rigoureusement les mêmes.

Note sur la version de K3S


Vous pourrez noter que j'ai fixé la version de K3S à l'aide de la variable d'environnement INSTALL_K3S_VERSION, cela vient du fait que les versions récentes de K3S utilisent une authentification par clé RSA par défaut que je n'ai pas réussi à faire fonctionner avec Traefik. Même avec le bon mot de passe, je n'ai pas réussi non plus à exploiter l'authentification basique. J'ai donc fait le choix de pointer sur la version que j'avais utilisé en septembre dernier pour ce billet.

Je ferais surement un billet sur ce point quand j'aurais trouvé une solution.

Installation des noeuds d'exécution

#!/bin/sh
# 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
export INSTALL_K3S_EXEC="agent --server https://kubemaster1:6443 --token-file /tmp/token --node-ip=${current_ip}"
export INSTALL_K3S_VERSION="v1.16.15+k3s1"
sh /home/packer/k3s

On retrouve ici la même logique : plus aucun téléchargement, configuration basique de l’hôte et lancement direct de K3S.

Installation de Traefik

La machine virtuelle Traefik en est quant à elle réduite à lancer le binaire directement en exploitant les configurations poussées par Vagrant.

#!/bin/bash
# Run Traefik as a front load balancer
/home/packer/traefik --configFile=/tmp/traefikconf/static_conf.toml &> /dev/null&

On fait tourner le tout

Nous avons donc le nécessaire pour lancer le déploiement avec Vagrant avec un simple

vagrant up

Comme pour mon billet précédent, au bout de quelques minutes mon cluster est en ligne. Il est possible de vérifier que tout fonctionne bien avec les commandes suivantes :

source ./scripts/configure_kubectl.sh
kubectl get nodes 

Vous devriez avoir un retour similaire à celui-ci :

Pour terminer

Cet article reste sur une utilisation très basique de Packer. L’outil est très puissant et vous permet d’avoir des approches de multiprovisionning de manière très simple. De plus, la force de Packer, comme la plupart des outils d’infra as code est la reproductibilité : il est très simple de recréer des images de base à l’identique sans avoir besoin de partager des fichiers de plusieurs centaines de Mo.

Comme beaucoup d’outils d’HashiCorp, le langage est d’assez haut niveau et très accessible. Pour ma part, il m’a fallu une heure pour faire ma première VM en partant de zéro il y a quelques années en utilisant Packer.

N’hésitez pas à réagir via les commentaires si vous avez des questions ou remarques !