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.

D open source à BullShit Licence (BSL)
Résumé des épisodes précédents Il y a à peine un mois, j’écrivais mon billet d’humeur annuel sur une boite, championne de l’open source (RHEL), qui utilise une faille de la licence GPL pour gratter un peu de thune en fermant ses sources aux “rebuilders” (je parle évidem…

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) :

Introduction à Cloud-init
Lors de précédents articles, nous avons vu comment installer et déployer des applications à l’aide de Docker, et de cloud providers. Faut-il que je réalise mon installation de Docker à chaque création d’instance ? Comment garantir une homogénéité dans mes installations ? Gagner du temps ?

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 :

---
vms:
  - name: "vm1"
    node_name: "mox"
    resource_name: "vm1"
    vm_id: 1000
    agent:
      enabled: false
      trim: true
      type: "virtio"
    bios: "seabios"
    cpu:
      cores: 1
      sockets: 1
    cloud_init:
      type: "nocloud"
      datastore_id: "local-lvm"
      dns:
        domain: "homelab.lan"
        server: "1.1.1.1 1.0.0.1"
      ip_configs:
        - ipv4:
            address: "192.168.0.110/24"
            gateway: "192.168.0.254"
        - ipv6:
            address: "fd91:0812:a17f:6194::10/64"
            gateway: "fd91:0812:a17f:6194::1"
      user_account:
        username: "ubuntu"
        password: "ubuntu"
        keys:
          - ssh-rsa AAAAB3Nza .........
    clone:
      node_name: "mox"
      vm_id: 9000
      full: true
    disks:
      - disk1:
          interface: "scsi0"
          datastore_id: "local-lvm"
          size: 32
          file_format: "raw"
          cache: "none"
      - disk2:
          interface: "scsi1"
          datastore_id: "local-lvm"
          size: 32
          file_format: "raw"
          cache: "none"
    memory:
      dedicated: 512
    network_devices:
      - net1:
          bridge: "vmbr0"
          model: "virtio"
      - net2:
          bridge: "vmbr0"
          model: "virtio"
    on_boot: true
  - name: "vm2"
    node_name: "mox"
    resource_name: "vm2"
    vm_id: 1001
    agent:
      enabled: false
      trim: true
      type: "virtio"
    bios: "seabios"
    cpu:
      cores: 1
      sockets: 1
    cloud_init:
      type: "nocloud"
      datastore_id: "local-lvm"
      dns:
        domain: "homelab.lan"
        server: "1.1.1.1 1.0.0.1"
      ip_configs:
        - ipv4:
            address: "192.168.0.111/24"
            gateway: "192.168.0.254"
        - ipv6:
            address: "fd91:0812:a17f:6194::10/64"
            gateway: "fd91:0812:a17f:6194::1"
      user_account:
        username: "ubuntu"
        password: "ubuntu"
        keys:
          - ssh-rsa AAAAB3Nza .........
    clone:
      node_name: "mox"
      vm_id: 9000
      full: true
    disks:
      - disk1:
          interface: "scsi0"
          datastore_id: "local-lvm"
          size: 32
          file_format: "raw"
          cache: "none"
      - disk2:
          interface: "scsi1"
          datastore_id: "local-lvm"
          size: 32
          file_format: "raw"
          cache: "none"
    memory:
      dedicated: 512
    network_devices:
      - net1:
          bridge: "vmbr0"
          model: "virtio"
      - net2:
          bridge: "vmbr0"
          model: "virtio"
    on_boot: true
vms.yaml

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 :

GitHub - juhnny5/pulumi-proxmox-cloud-init: Manage Proxmox VE vms with Pulumi
Manage Proxmox VE vms with Pulumi. Contribute to juhnny5/pulumi-proxmox-cloud-init development by creating an account on GitHub.

Si vous souhaitez clean tout ça, vous pouvez lancer un destroy. 😈

pulumi destroy