Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1319 minIntermédiaire

Créer des VM Proxmox avec Ansible

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • créer un jeton d'API Proxmox aux droits limités ;
  • cloner un modèle cloud-init avec proxmox_kvm ;
  • enchaîner création de la VM et configuration du système ;
  • lister les VM avec l'inventaire dynamique de Proxmox.

3 exercices corrigés

Ansible peut créer, configurer, démarrer et supprimer des machines virtuelles sur Proxmox VE en appelant son API HTTPS depuis le nœud de contrôle, avec les modules de la collection community.proxmox. Ce chapitre crée un jeton d'API aux droits limités, clone un modèle Debian 13 avec cloud-init pour obtenir le serveur de recette de Sorbier, enchaîne la création et la configuration du système dans un même playbook, et présente l'inventaire dynamique qui liste les VM du cluster.

Deux façons de travailler avec Proxmox

  • Configurer les hyperviseurs eux-mêmes. Un nœud Proxmox VE est un Debian 13 : on peut le gérer en SSH comme un serveur Linux (paquets, dépôts, NTP, comptes). Prudence : Proxmox gère lui-même une grande partie de sa configuration (réseau, cluster, stockage), qu'il ne faut pas réécrire derrière lui.
  • Piloter les VM par l'API. Les modules community.proxmox.* s'exécutent sur le nœud de contrôle et appellent l'API REST de Proxmox (port 8006) : créer, cloner, modifier, démarrer, arrêter, supprimer des VM et des conteneurs. C'est l'objet de ce chapitre.

Les modules Proxmox ont longtemps fait partie de community.general ; ils vivent désormais dans la collection community.proxmox (version 2.1 en novembre 2026). Les anciens noms (community.general.proxmox_kvm) ne sont plus que des redirections dépréciées, qui disparaîtront avec community.general 15 : écris community.proxmox.proxmox_kvm. Les modules ont besoin des bibliothèques Python proxmoxer et requests sur le nœud de contrôle (pipx inject ansible-core proxmoxer requests, chapitre 2).

Un jeton d'API aux droits limités

Ansible ne doit pas se connecter avec le compte root@pam de Proxmox. On crée un utilisateur dédié et un jeton d'API qui n'a que les droits nécessaires : gérer les VM, utiliser le stockage partagé et le pont réseau. Sur pve1, en root :

bash
# Un utilisateur du royaume Proxmox (pve), sans mot de passe : il n'utilisera que son jeton
pveum user add ansible@pve --comment "Automatisation Ansible"

# Un jeton à privilèges séparés : il n'a que les droits qu'on lui donne
pveum user token add ansible@pve automatisation --privsep 1

# Les droits du jeton : les VM, le stockage partagé, les ponts réseau
pveum acl modify /vms --tokens 'ansible@pve!automatisation' --roles PVEVMAdmin
pveum acl modify /storage/truenas-nfs --tokens 'ansible@pve!automatisation' --roles PVEDatastoreUser
pveum acl modify /sdn/zones/localnetwork --tokens 'ansible@pve!automatisation' --roles PVESDNUser

pveum user token add affiche le secret du jeton une seule fois : range-le immédiatement dans le coffre d'Ansible (vault_token_proxmox). L'identifiant complet du jeton est ansible@pve!automatisation.

CheminRôleCe qu'il permet
/vmsPVEVMAdminCréer, cloner, configurer, démarrer, supprimer toutes les VM, modèles compris
/storage/truenas-nfsPVEDatastoreUserAllouer de l'espace pour les disques sur le stockage partagé du cluster
/sdn/zones/localnetworkPVESDNUserBrancher une VM sur les ponts réseau locaux (vmbr0)

Le stockage truenas-nfs, le modèle 9000 modele-debian13 et sa configuration cloud-init sont ceux du cours Virtualisation, qui détaille aussi les utilisateurs, les rôles et les jetons de Proxmox. Si une tâche échoue sur un droit, le message de l'API nomme le privilège manquant (par exemple Permission check failed (/storage/local-lvm, Datastore.AllocateSpace)) : ajoute l'ACL correspondante, sans élargir les droits au-delà du besoin.

Le playbook de création d'une VM

Sorbier a besoin d'un serveur de recette, srv-recette, pour tester les playbooks avant la production : VM 186 (la convention de Sorbier donne aux VM le numéro de leur dernier octet), 4 cœurs, 8 Gio de mémoire, 40 Gio de disque, adresse 192.168.21.186 dans le VLAN 21 APPLIS (192.168.21.0/24, passerelle 192.168.21.254), que Sorbier a ouvert pour les applications internes et la recette quand le VLAN 20 des serveurs a été plein. Le playbook vm_recette.yml la crée à partir du modèle, puis lui applique le rôle socle :

yaml
---
- name: Créer la VM de recette sur Proxmox VE
  hosts: localhost
  gather_facts: false

  vars:
    vm_id: 186
    vm_nom: srv-recette
    vm_ip: 192.168.21.186
    vm_noeud: pve1

  module_defaults:
    group/community.proxmox.proxmox:
      api_host: 192.168.10.213
      api_user: ansible@pve
      api_token_id: automatisation
      api_token_secret: "{{ vault_token_proxmox }}"
      validate_certs: false

  tasks:
    - name: Cloner le modèle Debian 13
      community.proxmox.proxmox_kvm:
        node: "{{ vm_noeud }}"
        clone: modele-debian13
        vmid: 9000
        newid: "{{ vm_id }}"
        name: "{{ vm_nom }}"
        full: true
        storage: truenas-nfs
        timeout: 300

    - name: Régler le processeur, la mémoire et cloud-init
      community.proxmox.proxmox_kvm:
        node: "{{ vm_noeud }}"
        vmid: "{{ vm_id }}"
        cores: 4
        memory: 8192
        ciuser: ansible
        sshkeys: "{{ lookup('ansible.builtin.file', '~/.ssh/ansible_ed25519.pub') }}"
        net:
          net0: virtio,bridge=vmbr0,tag=21,firewall=1
        ipconfig:
          ipconfig0: ip={{ vm_ip }}/24,gw=192.168.21.254
        nameservers:
          - 192.168.10.177
          - 192.168.10.181
        searchdomains:
          - ad.sorbier.example
        update: true

    - name: Agrandir le disque à 40 Gio
      community.proxmox.proxmox_disk:
        vmid: "{{ vm_id }}"
        disk: scsi0
        size: 40G
        state: resized

    - name: Démarrer la VM
      community.proxmox.proxmox_kvm:
        node: "{{ vm_noeud }}"
        vmid: "{{ vm_id }}"
        state: started

    - name: Attendre que SSH réponde
      ansible.builtin.wait_for:
        host: "{{ vm_ip }}"
        port: 22
        delay: 10
        timeout: 300

    - name: Ajouter la VM à l'inventaire de cette exécution
      ansible.builtin.add_host:
        name: "{{ vm_nom }}"
        ansible_host: "{{ vm_ip }}"
        groups:
          - linux

- name: Appliquer le socle à la nouvelle VM
  hosts: srv-recette
  become: true
  roles:
    - socle

Ce qu'il faut comprendre :

  • hosts: localhost : les modules Proxmox parlent à l'API depuis le nœud de contrôle ; aucune connexion SSH n'est faite vers l'hyperviseur.
  • module_defaults donne les paramètres de connexion à tous les modules du groupe community.proxmox.proxmox, au lieu de les répéter dans chaque tâche. Le secret vient du coffre.
  • validate_certs: false accepte le certificat auto-signé que Proxmox génère à l'installation. En production, installe sur les nœuds un certificat de l'AC interne (ou Let's Encrypt) et retire cette ligne.
  • Le clonage puis la configuration sont deux tâches : lors d'un clonage, le module ne prend en compte que les paramètres du clone. update: true modifie ensuite la VM. Les paramètres ci*, ipconfig, nameservers, searchdomains et sshkeys remplissent le lecteur cloud-init du modèle : au premier démarrage, cloud-init crée le compte ansible avec la clé du bastion et le droit sudo sans mot de passe, et pose l'adresse.
  • wait_for attend que le port 22 réponde : la VM démarre, cloud-init s'exécute, puis SSH est prêt.
  • add_host ajoute la VM à l'inventaire en mémoire, pour la suite de cette exécution seulement, dans le groupe linux (elle reçoit donc group_vars/linux.yml). Le second play lui applique le rôle socle. Pour la gérer ensuite, ajoute-la dans hosts.yml (et son nom dans le DNS, chapitre 11) ou utilise l'inventaire dynamique ci-dessous.

Relancé, le playbook est idempotent : le clone voit que la VM 186 existe et ne fait rien, la configuration n'est modifiée que si une valeur diffère, le disque n'est pas agrandi s'il fait déjà 40 Gio, la VM est déjà démarrée.

Supprimer une VM

Pour supprimer la VM de recette à la fin d'un projet, il faut d'abord l'arrêter :

yaml
    - name: Arrêter la VM de recette
      community.proxmox.proxmox_kvm:
        vmid: 186
        state: stopped
        force: true
        timeout: 120

    - name: Supprimer la VM et ses disques
      community.proxmox.proxmox_kvm:
        vmid: 186
        state: absent
        purge: true

purge: true retire aussi la VM des tâches de sauvegarde, de réplication et de haute disponibilité où elle figurait. Pour les états autres que present, le nœud est retrouvé automatiquement ; node n'est pas nécessaire.

L'inventaire dynamique de Proxmox

Plutôt que d'ajouter chaque VM à la main dans hosts.yml, le plugin d'inventaire community.proxmox.proxmox interroge l'API et construit la liste des VM à chaque exécution. Le fichier doit se terminer par .proxmox.yml, par exemple inventaire/cluster.proxmox.yml :

yaml
---
plugin: community.proxmox.proxmox
url: https://192.168.10.213:8006
user: ansible@pve
token_id: automatisation
token_secret: "{{ lookup('ansible.builtin.env', 'PROXMOX_TOKEN_SECRET') }}"
validate_certs: false
want_facts: true
want_proxmox_nodes_ansible_host: false
keyed_groups:
  - key: proxmox_tags_parsed
    separator: ""
    prefix: etiquette_
compose:
  ansible_host: (proxmox_ipconfig0.ip | default('')).split('/') | first
  • Le plugin crée des groupes automatiquement, préfixés par proxmox_ : proxmox_all_qemu (toutes les VM), proxmox_all_running (celles qui tournent), proxmox_pve1_qemu (les VM du nœud pve1)…
  • keyed_groups ajoute un groupe par étiquette Proxmox : une VM étiquetée web dans l'interface (Tags) tombe dans le groupe etiquette_web. Les étiquettes deviennent ainsi un moyen de ranger les VM pour Ansible.
  • compose calcule des variables : ici, ansible_host est l'adresse posée par cloud-init (192.168.21.186/24 devient 192.168.21.186). Pour une VM sans configuration cloud-init, il faut une autre source (le DNS, ou l'agent invité QEMU).
  • Le secret est lu dans une variable d'environnement, PROXMOX_TOKEN_SECRET, pour ne pas l'écrire dans le fichier ; une valeur chiffrée par ansible-vault encrypt_string est aussi acceptée.

Si ansible.cfg désigne le dossier inventaire/ comme inventaire, Ansible fusionne hosts.yml et l'inventaire dynamique :

bash
ansible-inventory -i inventaire/ --graph proxmox_all_running

Exercices

Exercice 1 sur 3 Deux VM d'un coup Réussi À revoir

Sorbier veut deux serveurs web de test, srv-test1 (VM 187, 192.168.21.187) et srv-test2 (VM 188, 192.168.21.188), dans le VLAN 21 APPLIS, clonés du modèle 9000, 2 cœurs et 2 Gio chacun. Écris les tâches de clonage et de configuration avec une boucle sur une liste de dictionnaires. Vérifie que les adresses sont dans le réseau du VLAN 21 et qu'elles sont libres.

Voir la réponseMasquer la réponse de l'exercice 1
yaml
  vars:
    vms_test:
      - id: 187
        nom: srv-test1
        ip: 192.168.21.187
      - id: 188
        nom: srv-test2
        ip: 192.168.21.188

  tasks:
    - name: Cloner le modèle pour chaque VM de test
      community.proxmox.proxmox_kvm:
        node: pve1
        clone: modele-debian13
        vmid: 9000
        newid: "{{ item.id }}"
        name: "{{ item.nom }}"
        full: true
        storage: truenas-nfs
        timeout: 300
      loop: "{{ vms_test }}"
      loop_control:
        label: "{{ item.nom }}"

    - name: Configurer chaque VM de test
      community.proxmox.proxmox_kvm:
        node: pve1
        vmid: "{{ item.id }}"
        cores: 2
        memory: 2048
        ciuser: ansible
        sshkeys: "{{ lookup('ansible.builtin.file', '~/.ssh/ansible_ed25519.pub') }}"
        net:
          net0: virtio,bridge=vmbr0,tag=21,firewall=1
        ipconfig:
          ipconfig0: ip={{ item.ip }}/24,gw=192.168.21.254
        update: true
      loop: "{{ vms_test }}"
      loop_control:
        label: "{{ item.nom }}"

Le VLAN 21 est 192.168.21.0/24 : les hôtes vont de .1 à .254, et la passerelle est .254, sur le pare-feu. .187 et .188 sont libres (.182 est le conteneur ct-horaires, .186 la VM srv-recette). Dans le VLAN 20 des serveurs, ces deux adresses auraient été un piège : .187 est la dernière adresse libre, et .188 et .189 sont celles des deux pare-feu pfSense en cluster. Le modèle est branché sur le VLAN 20 : sans net0 avec l'étiquette 21, les clones seraient dans le mauvais réseau et ne joindraient pas leur passerelle.

Avec la boucle, ajouter une troisième VM revient à ajouter trois lignes à vms_test, sans toucher aux tâches.

Tu avais trouvé ?

Exercice 2 sur 3 Les bons droits Réussi À revoir

Le playbook de création échoue sur la tâche de clonage avec : Permission check failed (/storage/local-lvm, Datastore.AllocateSpace). Le jeton a les trois ACL de ce chapitre. Explique et propose deux corrections.

Voir la réponseMasquer la réponse de l'exercice 2

Le clone est créé sur le stockage local-lvm, pour lequel le jeton n'a aucun droit : le paramètre storage est absent ou mal écrit, et Proxmox utilise un autre stockage que truenas-nfs. Corrections possibles : préciser storage: truenas-nfs dans la tâche de clonage (la bonne solution, puisque les VM du cluster doivent être sur le stockage partagé pour pouvoir migrer), ou, si local-lvm est vraiment voulu, ajouter pveum acl modify /storage/local-lvm --tokens 'ansible@pve!automatisation' --roles PVEDatastoreUser. Ne pas donner un rôle d'administrateur sur / pour « faire marcher » : le jeton aurait alors tous les droits sur le cluster.

Tu avais trouvé ?

Exercice 3 sur 3 Ranger les VM par étiquette Réussi À revoir

Avec l'inventaire dynamique de ce chapitre, Moussa étiquette recette les VM 186, 187 et 188 dans l'interface de Proxmox. Écris la commande qui applique le playbook site.yml uniquement à ces VM, et dis à quoi sert --list-hosts ici.

Voir la réponseMasquer la réponse de l'exercice 3
bash
ansible-playbook -i inventaire/ site.yml --limit etiquette_recette --list-hosts
ansible-playbook -i inventaire/ site.yml --limit etiquette_recette

Le plugin crée le groupe etiquette_recette (préfixe etiquette_, séparateur vide, étiquette recette). --list-hosts vérifie d'abord quelles machines seront touchées : avec un inventaire dynamique, la liste dépend de l'état du cluster au moment de l'exécution, et une étiquette posée par erreur sur une VM de production l'ajouterait à la cible.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Terraform et OpenTofu : les notionsRevoir le sommaire du cours