Pulumi + Proxmox VE + Cloud-Init = ❤️
Cet article sera très certainement le premier d'une longue série sur ce sujet. Il est là pour vous donner un premier exemple concret.
Bon, il y a de très fortes chances que vous soyez tombés dessus, mais il y a peu de temps, Hashicorp annoncé le passage à la licence BSL (Business Source License) pour leur projet Terraform. 💩
Je vous conseil d'ailleurs d'aller consulter le site du collègue et ami Denis aka Zwindler, qui a écrit un billet sur le sujet.
De mon côté, j'utilise Pulumi depuis un bon moment à titre personnel, j'ai d'ailleurs préparé un talk sur ce sujet pour expliquer son fonctionnement. Ce talk arrivera dès la rentrée. 😊
Dans cet article, je vais simplement vous partager un usage simple pour commencer.
Contexte 😬
J'utilise Proxmox VE à la maison pour réaliser des PoCs. Proxmox VE (ou pve) possède une API qui permet d'intéragir avec celui-ci. Pour des raisons de simplicité, j'ai passé un peu de temps à déporter la création de vms dans un fichier YAML dédié.
Création du premier projet 😎
Pour une question de simplicité, ici je vais créer un projet Python pour la compréhension d'un plus grand nombre. A noter que le gros avantage de Pulumi est de pouvoir utiliser son langage de prédilection (dans la limite de ceux supportés).
mkdir -p pulumi-proxmox
cd pulumi-proxmox/
pulumi new python --name pulumi-proxmox --stack dev --description "Pulumi with Proxmox VE" --force
pip install pulumi-proxmoxve
pulumi stack init dev
Maintenant que notre projet est intialisé, nous allons attaquer la configuration de l'utilisateur Pulumi sur Proxmox VE pour que l'outil du même nom puisse intérragir avec l'API de Proxmox VE. 😊
Il est important de souligner que par défaut, Pulumi stock le state sur sa plateforme. Il est possible de le garder localement ou de l'envoyer dans du stockage objet (type S3).
Configuration de l'utilisateur Pulumi sur Proxmox VE
Il faut commencer par créer le role Pulumi avec les bons droits :
pveum role add Pulumi -privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit"
Ensuite, ajouter l'utilisateur :
pveum user add pulumi@pve -password <password> -comment "Pulumi account"
Spécifier le rôle que doit utiliser l'utilisateur pulumi@pve
:
pveum aclmod / -user pulumi@pve -role Pulumi
Créer un token :
pveum user token add pulumi@pve pulumi -expire 0 -privsep 0 -comment "Pulumi token"
Ne reste plus qu'à tester que celui-ci fonctionne ! ☺️
curl -X GET 'https://mox.homelab.lan:8006/api2/json/nodes' -H 'Authorization: PVEAPIToken=pulumi@pve!pulumi=xxxxxxxxxxxxxxx'
Ok, maintenant que la configuration de l'utilisateur Pulumi est faite côté Proxmox VE, nous allons créer le template Ubuntu Server 22.04 via la`cloudimg` qui supporte l'utilisation de cloud-init.
D'ailleurs, si vous ne connaissez pas cloud-init
, je vous conseil de lire cette introduction (en Français) :
Création du template 🥸
Pour ce faire on va récupérer l'image sur le site de Canonical et installer les dépendances. 😁
cd /tmp
wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
apt update -y && apt install libguestfs-tools -y
On va ensuite installer le paquet qemu-guest-agent
, ajouter notre premier utilisateur (ici jbriault
), créer le path et injecter la clé SSH. On termine par définir les bons droits.
virt-customize -a jammy-server-cloudimg-amd64.img --install qemu-guest-agent
virt-customize -a jammy-server-cloudimg-amd64.img --run-command 'useradd jbriault'
virt-customize -a jammy-server-cloudimg-amd64.img --run-command 'mkdir -p /home/jbriault/.ssh'
virt-customize -a jammy-server-cloudimg-amd64.img --ssh-inject jbriault:file:/home/jbriault/.ssh/id_rsa.pub
virt-customize -a jammy-server-cloudimg-amd64.img --run-command 'chown -R jbriault: /home/jbriault'
Maintenant, on rentre dans le dur et on créé le template qui aura pour id 9000
. 🚀
qm create 9000 --name "ubuntu-2204-cloudinit-template" --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9000 jammy-server-cloudimg-amd64.img local-lvm
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --serial0 socket --vga serial0
qm set 9000 --agent enabled=1
qm template 9000
Vous devriez voir sur Proxmox VE:
Maintenant, pour une question de simplification (dans cette première version du projet), j'ai fait en sorte de pouvoir déclarer ses vms au format YAML. ☺️
Voici un exemple :
Et le coeur du projet en Python qui vient lire les informations du fichier vms.yaml
et les importer pour les créer avec Pulumi sur Proxmox VE.
import yaml
import pulumi
import pulumi_proxmoxve as proxmox
with open('vms.yaml', 'r') as file:
yaml_content = file.read()
parsed_data = yaml.safe_load(yaml_content)
for vm in parsed_data['vms']:
disks = []
nets = []
ip_configs = []
ssh_keys = []
for disk_entry in vm.get('disks', []):
disk = disk_entry.popitem()[1]
disks.append(
proxmox.vm.VirtualMachineDiskArgs(
interface=disk.get('interface', ''),
datastore_id=disk.get('datastore_id', ''),
size=disk.get('size', 0),
file_format=disk.get('file_format', ''),
cache=disk.get('cache', '')
)
)
for ip_config_entry in vm['cloud_init']['ip_configs']:
ipv4 = ip_config_entry.get('ipv4')
ipv6 = ip_config_entry.get('ipv6')
if ipv4:
ip_configs.append(
proxmox.vm.VirtualMachineInitializationIpConfigArgs(
ipv4=proxmox.vm.VirtualMachineInitializationIpConfigIpv4Args(
address=ipv4.get('address', ''),
gateway=ipv4.get('gateway', '')
)
)
)
if ipv6:
ip_configs.append(
proxmox.vm.VirtualMachineInitializationIpConfigArgs(
ipv6=proxmox.vm.VirtualMachineInitializationIpConfigIpv6Args(
address=ipv6.get('address', ''),
gateway=ipv6.get('gateway', '')
)
)
)
for ssk_keys_entry in vm['cloud_init']['user_account']['keys']:
ssh_keys.append(ssk_keys_entry)
for net_entry in vm.get('network_devices', []):
net = net_entry.popitem()[1]
nets.append(
proxmox.vm.VirtualMachineNetworkDeviceArgs(
bridge=net.get('bridge', ''),
model=net.get('model', '')
)
)
virtual_machine = proxmox.vm.VirtualMachine(
vm_id=vm['vm_id'],
resource_name=vm['resource_name'],
node_name=vm['node_name'],
agent=proxmox.vm.VirtualMachineAgentArgs(
enabled=vm['agent']['enabled'],
trim=vm['agent']['trim'],
type=vm['agent']['type']
),
bios=vm['bios'],
cpu=proxmox.vm.VirtualMachineCpuArgs(
cores=vm['cpu']['cores'],
sockets=vm['cpu']['sockets']
),
clone=proxmox.vm.VirtualMachineCloneArgs(
node_name=vm['clone']['node_name'],
vm_id=vm['clone']['vm_id'],
full=vm['clone']['full'],
),
disks=disks,
memory=proxmox.vm.VirtualMachineMemoryArgs(
dedicated=vm['memory']['dedicated']
),
name=vm['name'],
network_devices=nets,
initialization=proxmox.vm.VirtualMachineInitializationArgs(
type=vm['cloud_init']['type'],
datastore_id=vm['cloud_init']['datastore_id'],
dns=proxmox.vm.VirtualMachineInitializationDnsArgs(
domain=vm['cloud_init']['dns']['domain'],
server=vm['cloud_init']['dns']['server']
),
ip_configs=ip_configs,
user_account=proxmox.vm.VirtualMachineInitializationUserAccountArgs(
username=vm['cloud_init']['user_account']['username'],
password=vm['cloud_init']['user_account']['password'],
keys=ssh_keys
),
),
on_boot=vm['on_boot']
)
pulumi.export(vm['name'], virtual_machine.id)
Il ne reste plus qu'à vérifier les modifications qui vont être apportées (équivalent du plan
sur Terraform) :
pulumi preview
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/........
Type Name Plan
pulumi:pulumi:Stack pulumi-pro
+ ├─ proxmoxve:VM:VirtualMachine vm2 create
+ └─ proxmoxve:VM:VirtualMachine vm1 create
Il ne reste plus qu'à déployer nos modifications.
pulumi up
Vous pouvez retrouver tout le code ici :
Si vous souhaitez clean tout ça, vous pouvez lancer un destroy
. 😈
pulumi destroy