cloud-init et VMWare vSphere

Lorsqu'il s'agit d'initialiser une machine virtuelle dans une infrastructure VMWare vSphere, les systèmes Linux sont le parent pauvre. En effet, VMWare a bien intégré SysPrep de Microsoft. Pour Linux, il y a VMWare Guest Tools qui permet un peu d'initialisation. C'est limité au nom d'hôte et aux interfaces réseaux essentiellement. Notamment, VMWare Guest Tools ne permet pas d'injecter une clef publique SSH.

cloud-init

cloud-init est un projet de configuration de système. C'est un agent déployé dans une image de machine virtuelle. Au démarrage du système, cloud-init cherche une configuration et l'applique. Cela inclue la définition du nom d'hôte, la configuration réseau, la création d'utilisateurs, l'injection de clef publique SSH, l'installation de paquets, etc.

En général, cloud-init laisse la main à un outils comme Ansible pour le reste du provisionnement. cloud-init est adapté pour l'amorçage d'une machine à partir d'une image : nom d'hôte, configuration réseau, injection de clef publique SSH.

Le projet cloud-init est un des rares projets Canonical ayant dépassé le microcosme Ubuntu. Il faut dire que le créateur initial est Yahoo. Et que l'idée est en fait d'Amazon pour l'initialisation des machines virtuelles EC2 via un serveur de métadonnées. Ensuite, OpenStack a implémenté cette architecture en s'appuyant sur cloud-init comme agent. À partir de là, le projet cloud-init était bien installé.

Servir les méta-données

Un des enjeux de cloud-init est de récupérer ces fameuses métadonnées : le nom d'hôte, la configuration réseau et les clefs publiques SSH. Chaque technologie IaaS a sa propre manière de faire. Chez EC2 et OpenStack, le fameux serveur magique http://169.254.169.254/ retourne les méta-données ad-hoc de la machine qui lui demande. Mais le format est différent. cloud-init a un composant appelé DataSource responsable de récupérer ces méta-données et des les normaliser. Plusieurs implémentations sont disponibles. On a donc une source EC2 et une source OpenStack.

Pas de ça pour VMWare vSphere. Il faut concevoir soi-même le moyen de passer ces métadonnées à la machine, et les bonnes ! Jusqu'à cloud-init 21.2, il faut forger une image disque ISO avec deux fichiers dedans et nommer le disque CIDATA pour Cloud-Init DATA. Pas franchement le plus pratique.

Depuis cloud-init 21.3, une DataSource VMWare dédiée est arrivée. L'hébergeur injecte les méta-données dans les extraOptions de la machine virtuelle, encodées en base64. La source VMWare de cloud-init récupère les méta-données via le protocole RPC de VMWare Guest Tools. On peut facilement jouer avec ces extraOptions avec l'outil vmware-rpctool:

$ vmware-rpctool 'info-get guestinfo.vmtools.description'
open-vm-tools 11.2.5 build 17337674
$ vmware-rpctool 'info-get guestinfo.ip'
100.64.24.32
$

Mais voilà, cette version 21.3 est sortie le 10 août 2021. Autant dire qu'on ne va pas la voir de si tôt dans les chaumières. Alors que faire ?

Relier VMWare et cloud-init

Une autre source va nous dépanner : NoCloud. Cette source est assez agnostique comme son nom l'indique. Elle permet de récupérer les données depuis un dossier ou une URL HTTP. NoCloud cherche notamment les méta-données dans le dossier /var/lib/cloud/seed/cloud-net/. En clair, le simple script bash suivant va interroge l'API RPC de VMWare Guest Tools, décode les données et les stockes au bon endroit pour la source NoCloud de cloud-init :

#!/bin/bash -eux
mkdir -p /var/lib/cloud/seed/nocloud-net/
vmware-rpctool "info-get guestinfo.metadata" | base64 --decode > /var/lib/cloud/seed/nocloud-net/meta-data
vmware-rpctool "info-get guestinfo.userdata" | base64 --decode > /var/lib/cloud/seed/nocloud-net/user-data

Simple, non ? Les clefs guestinfo.metadata et guestinfo.userdata sont celles utilisées par la source VMWare de cloud-init 21.3.

Reste à exécuter ce script au bon moment. systemd vient à notre aide, en ajoutant une ligne ExecStartPre au service cloud-init-local.service. Pour cela, ajouter un fichier seed-nocloud-from-guest-infos.conf dans le dossier /etc/systemd/system/cloud-init-local.service.d/ avec les deux lignes suivantes :

[Service]
# Script récupérant les méta-données avec vmware-rpctool
ExecStartPre=/usr/local/sbin/seed-nocloud-from-guest-info

Pensez à recharger systemd avec systemctl daemon-reload pour prendre en compte ce fichier. Vérifier que c'est bien le cas avec la commande systemctl cat:

[root@host ~]# systemctl daemon-reload
[root@host ~]# systemctl cat cloud-init-local
# /usr/lib/systemd/system/cloud-init-local.service
[Unit]
Description=Initial cloud-init job (pre-networking)
...
[Service]
Type=oneshot
ExecStartPre=/bin/mkdir -p /run/cloud-init
ExecStartPre=/sbin/restorecon /run/cloud-init
ExecStartPre=/usr/bin/touch /run/cloud-init/enabled
ExecStart=/usr/bin/cloud-init init --local
...
# /etc/systemd/system/cloud-init-local.service.d/vmware-seeder.conf
[Service]
ExecStartPre=/usr/local/sbin/seed-nocloud-from-guest-info
[root@host ~]#

Avant de redémarrer, s'assurer que la source NoCloud est bien active ! Pour cela, déposer un fichier /etc/cloud/cloud.cfg.d/nocloud.cfg avec le contenu:

datasource_list: [NoCloud, None]

Générer les métadonnées

Les méta-données sont au format YAML, encodé en base64.

$ cat metadata.yml
instance-id: monhote
$ cat userdata.yml
#cloud-config
hostname: monhote
ssh_authorized_keys:
- "ssh-ed25519 ..."
runcmd:
# Relancer dhclient pour prendre en compte le nom d'hote.
- dhclient -r
- dhclient -H monhote
final_message: "Configuré par VMWare vSphere"

Encode ça en base64 et gardez ça sous le coude. Naviguer dans VMWare vSphere. Une fois éteinte, éditer la configuration de la machine virtuelle, dans l'onglet Options VM. Dans la section Avancé, cliquer sur le lien Modifier la configuration. Dans la nouvelle boite de dialogue, cliquer sur le bouton Ajouter des paramètres de configuration.. Renseigner guestinfo.metadata avec le contenu de metadata.yml encodé en base64, et de la meme manière le contenu de userdata.yml encodé en base64 dans un paramètre guestinfo.userdata.

Pour info, la source VMWare de cloud-init 21.3 demande également de spécifier l'encodage base64 dans les paramètres guestinfo.metadata.encoding et guestinfo.userdata.encoding. Le script bash n'en tiens pas compte. Voir la documentation de la source VMWare de cloud-init pour plus de détails.

Le grand test

Après avoir injecté les métadonnées dans les extraOptions de la machine, il nous faut maintenant redémarrer. On vérifie ensuite les traces:

[root@host ~]# journalctl -u cloud-init-local
-- Logs begin at jeu. 2021-09-16 09:44:09 CEST, end at ven. 2021-09-17 16:42:12 CEST. --
sept. 16 09:44:14 host systemd[1]: Starting Initial cloud-init job (pre-networking)...
sept. 16 09:44:15 host seed-nocloud-from-guest-info[785]: + mkdir -p /var/lib/cloud/seed/nocloud-net/
sept. 16 09:44:15 host seed-nocloud-from-guest-info[785]: + base64 --decode
sept. 16 09:44:15 host seed-nocloud-from-guest-info[785]: + vmware-rpctool 'info-get guestinfo.metadata'
sept. 16 09:44:15 host seed-nocloud-from-guest-info[785]: + base64 --decode
sept. 16 09:44:15 host seed-nocloud-from-guest-info[785]: + vmware-rpctool 'info-get guestinfo.userdata'
sept. 16 09:44:17 host cloud-init[795]: Cloud-init v. 19.4 running 'init-local' at Thu, 16 Sep 2021 07:44:16 +0000. Up 7.99 seconds.
[root@host ~]# cat /var/log/cloud-init.log
2021-09-16 07:44:17,001 - util.py[DEBUG]: Cloud-init v. 19.4 running 'init-local' at Thu, 16 Sep 2021 07:44:16 +0000. Up 7.99 seconds.
2021-09-16 07:44:17,001 - main.py[DEBUG]: No kernel command line url found.
...
2021-09-16 07:44:17,042 - __init__.py[DEBUG]: Looking for data source in: ['NoCloud', 'None'], via packages ['', u'cloudinit.sources'] that matches dependencies ['FILESYSTEM']
2021-09-16 07:44:17,058 - __init__.py[DEBUG]: Searching for local data source in: [u'DataSourceNoCloud']
...
2021-09-16 07:44:17,129 - util.py[DEBUG]: Reading from /var/lib/cloud/seed/nocloud-net/user-data (quiet=False)
2021-09-16 07:44:17,129 - util.py[DEBUG]: Read 1034 bytes from /var/lib/cloud/seed/nocloud-net/user-data
2021-09-16 07:44:17,129 - util.py[DEBUG]: Reading from /var/lib/cloud/seed/nocloud-net/meta-data (quiet=False)
2021-09-16 07:44:17,130 - util.py[DEBUG]: Read 877 bytes from /var/lib/cloud/seed/nocloud-net/meta-data
2021-09-16 07:44:17,130 - DataSourceNoCloud.py[DEBUG]: Using seeded data from /var/lib/cloud/seed/nocloud-net
...
2021-09-16 07:44:17,362 - stages.py[INFO]: Loaded datasource DataSourceNoCloud - DataSourceNoCloud [seed=/var/lib/cloud/seed/nocloud-net][dsmode=net]
...
[root@host ~]#

Et voilà, le script seed-nocloud-from-guest-info a bien été exécuté. Les fichiers sont bien pris en compte par cloud-init. Et d'ailleurs, la résolution DNS de la machine est correct ainsi que l'accès SSH.

Un playbook tout en un

J'ai mis en œuvre cette solution dans Cornac, l'implémentation libre de AWS RDS développée par Dalibo. Un playbook Ansible pour CentOS 7 configure tout ça de bout en bout : cloud-init.yml. Et vous ? Comment initialisez-vous les machines virtuelles Linux dans votre infrastructure VMWare ?