Déploiement d'un lot de VM individualisées sur un serveur Proxmox avec Terraform

Terraform

Un aperçu final:

Présentation

L'objectif est de créer une VM pour chaque membre d'un groupe de 37 personnes:

  • chaque utilisateur disposera de ses identifiants pour se connecter à sa VM (il pourra, dans un premier temps s'y connecter via l'interface web de Proxmox puis, dans un second, en ssh);
  • les informations de "personnalisation" de chaque VM (nom d'utilisateur, mot de passe, ip, numéro attribué à la VM dans Proxmox) seront tirées d'un fichier csv composé en amont ;
  • l'administrateur pourra gérer les VM à distance via Ansible (étape 2).

Etape 1

1. Création d'une image personnalisée de debian 12 à partir d'une image cloud de cette distribution (dépôt):

cd /var/lib/vz/template/iso
wget https://cdimage.debian.org/cdimage/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
cp -pr debian-12-genericcloud-amd64.qcow2 debian-12-genericcloud-amd64-v2.qcow2 # on crée une copie pour disposer d'une image "propre" en cas de problème.
virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --install qemu-guest-agent
virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --root-password password:******

Dès le départ, on peut profiter de la création de cette image de base pour implémenter certaines fonctionnalités que l'on souhaite retrouver sur toutes les VM (un dossier, des utilitaires...)

virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --mkdir /home/toto
virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --install vim,bash-completion,wget,curl,telnet,unzip,net-tools

Important :
Les déploiements via Ansible impliquent de pouvoir se connecter en ssh sur toutes les VM sans utiliser le compte root : il faut donc créer un utilisateur commun à toutes les VM qui soit autorisé à se connecter en SSH. Je n'ai pas trouvé d'autre moyen de le faire qu'au moment de la création de l'image. Cela implique également une modification de la configuration par défaut de SSH qui crée une "faille" de sécurité (on autorise la connexion par mot de passe pour un utilisateur.) Si quelqu'un a une alternative, je suis preneur.

  • a- Création d'un utilisateur "ansible"
  • b- Ajout de l'utilisateur "ansible" au groupe des sudoers
  • c- Modification du fichier /etc/ssh/sshd_config pour permettre l'authentification par mot de passe et autoriser l'utilisateur "ansible". Soit:
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command 'useradd ansible --password ****** -s /bin/bash'  ## NB: il faut obligatoirement crypter le mot de passe (avec openssl)
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command "sudo usermod -aG sudo ansible"
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command 'sed -i "/PasswordAuthentication no/d" /etc/ssh/sshd_config'
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command 'echo PasswordAuthentication yes >> /etc/ssh/sshd_config'
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command 'sed -i "/PasswordAuthentication no/d" /etc/ssh/sshd_config'
    virt-customize -a debian-12-genericcloud-amd64-v2.qcow2 --run-command 'echo AllowUsers ansible >> /etc/ssh/sshd_config'

2. Création de la VM template

En veillant à rester ou revenir dans le répertoire où se trouve l'image debian modifiée (/var/lib/vz/template/iso) on procède à la création et au paramétrage de la VM qui va servir de modèle/template:

qm create 1008 --memory 2048 --core 2 --name debian12-custom --net0 virtio,bridge=vmbr1 --description "Debian 12 personnalisée"
qm importdisk 1008 debian-12-genericcloud-amd64-v2.qcow2 <storage>
qm set 1008 --scsihw virtio-scsi-pci --scsi0 <storage>:1008/vm-1008-disk-0.raw
qm set 1008 --boot c --bootdisk scsi0
qm set 1008 --ide2 <storage>:cloudinit
qm set 1008 --serial0 socket --vga qxl #ou serial0
qm set 1008 --agent enabled=1
qm template 1008

Cette VM est la base qui sera ensuite clonée autant de fois que nécessaire (ici, 37) puis modifiée par la suite avec Ansible.

3. Utilisation de Terraform****

Pour l'installation, se reporter à la documentation officielle.
Dans cet exemple, c'est le provider bpg qui a été utilisé (dans sa version 0.46.1, au moment de la rédaction).

a- Création sur le serveur proxmox d'un utilisateur spécifique qui pourra se connecter à l'API et génération du jeton de connexion
  1. Création de l'utilisateur terraform

    pveum user add terraform@pve --password <password>

    Il faut que cet utilisateur ait des droits de création, de configuration et d'attribution de ressources (à affiner et compléter en fonction de ce qui est déployé sur le serveur Proxmox)

  2. Création d'un rôle "Terraform" spécifique sur le serveur Proxmox.

    pveum role add Terraform -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"
  3. Attribution du rôle "Terraform" à l'utilisateur terraform

    pveum aclmod / -user terraform@pve -role Terraform
  4. Génération du token/jeton de connexion (value générée à noter précieusement)

    pveum user token add terraform@pve terraform -expire 0 -privsep 0 -comment "jeton pour terraform"
b- Création des fichier de configuration pour le déploiement avec Terraform et le provider bpg****

Préalable : Depuis le machine utilisée pour le déploiement, on doit pouvoir se connecter avec une clé ssh au serveur Proxmox avec ssh-agent.

Trois fichiers sont à créer dans le même répertoire (voir dépôt):

  • main.tf (le fichier de configuration principal du déploiement)
  • variables.tf (un fichier de variables à passer au fichier main.tf)
  • utilisateurs.csv (la liste des utilisateurs)

Le fichier main.tf est assez simple dans sa définition. C'est grâce à csvdecode qu'il pourra générer autant de VM que d'utilisateurs présents dans le csv en implémentant dans chaque VM des données uniques (nom, ip, username, password, VMid).
main.tf

terraform {
  required_providers {
    proxmox = {
      source = "bpg/proxmox"
      version = "0.46.1"
    }
  }
}

provider "proxmox" {
  endpoint = "https://server_proxmox:8006/"
  api_token = var.api_token # c'est une variable saisie dans le fichier variables.tf
  insecure = true # dans le cas d'un certificat auto-signé
  ssh {
    agent    = true
    username = "root"
  }
}
locals {
  users = csvdecode(file("utilisateurs.csv")) # csvdecode -propre à terraform- va permettre d'extraire du fichier csv des utilisateurs les données username, password, IP et VMID
  users_map = { for u in local.users : u.username => u }
}
resource "proxmox_virtual_environment_vm" "vm" {
  for_each = local.users_map 
  name = each.value.username # le nom de chaque VM est tiré du csv.
  # Nom de machine = nom d'utilisateur (utile pour la suite)
  vm_id  = each.value.vmid # idem pour le numéro à lui attribuer dans Prowmox
  node_name = var.node # le nom du noeud proxmox
  on_boot = var.boot_mode # démarrer les VM au boot
  started = var.started

  agent {
    enabled = true
  }
  startup {
    order      = "3" # ordre de démarrage VM (à ajuster selon les situations)
    up_delay   = "10" # délai avant démarrage VM suivante (à ajuster selon les situations)
    down_delay = "10" # délai avant extinction VM suivante (à ajuster selon les situations)
  }
  clone {
    vm_id = 1008 # Le numéro du template de VM à cloner
    retries = 2
  }

## La partie initialization "remplace" une partie des fichiers cloud-init
initialization {
    datastore_id = "storage"
    ip_config {
      ipv4 {
        address = "${each.value.ip}/24" # On attribue une IP à chaque VM
        gateway = "192.168.1.1"
      }
    }
user_account {
        password = each.value.password # On crée utilisateur et mot de passe.
        username = each.value.username
        keys = [
           "ssh-rsa ..." # On ajoute sa clé ssh publique pour pouvoir ultérieurement se connecter sans mot de passe à chaque VM
        ]
        }
  }
keyboard_layout = "fr"

  cpu {
    type    = "kvm64"
    cores   = var.cores
    sockets = var.sockets
    flags   = []
  }

  memory {
    dedicated = var.memory
  }

  network_device {
    bridge  = "vmbr0"
    model   = "virtio"
  }
  boot_order    = ["scsi0"]
  scsi_hardware = "virtio-scsi-single"
  disk {
    interface    = "scsi0"
    iothread     = true
    datastore_id = "<storage>"
    discard      = "ignore"
    size         = 10 # taille du disque de chaque VM
  }
}
c- Validation et lancement du déploiement
terraform init -upgrade ## initialiser et télécharger les providers et ressources mobilisés par la configuration
terraform validate # Tester la validité de la configuration
terraform plan # Obtenir l'aperçu de ce qui va être créé et déployé

Il est peut-être possible de réussir le déploiement en lançant simplement un terraform apply --auto-approve mais le déploiement d'un lot aussi important de VM a de grandes chances de créer des erreurs (dans mon cas, Proxmox ne parvenait pas à gérer autant de clonages, redimensionnements et démarrages simultanés). Par défaut, les clones sont lancés par lots de 10.
Pour éviter ce problème, il est possible de rajouter à la commande terraform apply --auto-approve le paramètre parallelism= en y adjoignant le nombre d'opérations simultanées à réaliser. Même en limitant ce paramètre à 2, le temps global de création restait raisonnable (12'30) et le déploiement s'effectue sans problème.

terraform apply --auto-approve -parallelism=2 # Pour lancer le déploiement et la création des VM (--auto-approve = créer sans prompt de confirmation)

En cas de problème :

terraform destroy # Après un déploiement, pour supprimer toutes les VM sur l'hôte Proxmox
  • Si l'on souhaite ajouter un ou des disque(s) supplémentaire(s) à chaque VM, il suffit d'ajouter ce bloc dans le main.tf :
  dynamic "disk" {
 for_each = local.instances

  content {
    interface    = "scsi1" # veiller à numéroter en cohérence avec le ou les autre(s) disques
    iothread     = true
    datastore_id = "<storage>"
    size         = 30
    discard      = "ignore"
    file_format  = "raw"
  }
}

NB : Terraform ne semble pas fait pour gérer le contenu des VM après leur création.
.

Fin de l'étape

Les 37 VM sont déployées et il est possible de se logger dessus depuis l'interface web de Proxmox avec le nom d'utilisateur affecté à la VM ou avec les utilisateurs ansible ou root.

En revanche, on ne peut se logger en ssh qu'avec l'utilisateur ansible.

L'étape suivante, avec Ansible, va donc avoir pour but de permettre d'ouvrir l'accès en ssh à chacun des utilisateurs (en modifiant la configuration de ssh), de déployer des programmes sur les VM ou encore de créer des répertoires, fichiers, etc.