La cinquième fois que j’ai créé un LXC Debian à la main dans l’interface Proxmox, j’ai réalisé que je reproduisais exactement les mêmes étapes dans le même ordre, en faisant exactement les mêmes fautes de frappe sur l’adresse IP. Le clic dans l’interface Proxmox c’est pratique pour une création, pas pour dix.
Ansible avec la collection community.general peut piloter l’API Proxmox directement : créer des LXC, des VMs KVM, injecter des clés SSH, configurer le réseau – sans jamais toucher l’UI. Le résultat n’est pas un remplacement de l’interface Proxmox, c’est un outil de provisioning répétable qui te laisse l’interface pour l’inspection et le diagnostic.
Prérequis et contexte
La machine de contrôle Ansible est mon poste de travail (Debian 12.7, Ansible 2.17). La cible est heighliner (Proxmox VE 8.3, 10.10.1.10). Je n’installe pas Ansible sur le nœud Proxmox lui-même – l’API REST de Proxmox est accessible depuis l’extérieur, c’est suffisant.
Versions exactes :
ansible [core 2.17.6]
community.general 9.5.0
proxmoxer 2.0.1 (Python, sur la machine de contrôle)
Installer les dépendances sur la machine de contrôle :
pip3 install proxmoxer requests
ansible-galaxy collection install community.general
proxmoxer est une librairie Python qui wrape l’API REST Proxmox. Les modules Ansible community.general.proxmox et community.general.proxmox_kvm l’utilisent en interne. Elle doit être installée sur la machine qui exécute les modules Ansible (le controller), pas sur le nœud Proxmox.
Créer un token API Proxmox
Je ne veux pas utiliser les credentials root dans Ansible. Proxmox supporte les tokens API liés à un utilisateur avec des permissions granulaires – c’est ce qu’on utilise.
Dans l’interface Proxmox (Datacenter > Permissions > API Tokens) :
- Créer l’utilisateur
ansible@pves’il n’existe pas (Datacenter > Permissions > Users) - Créer un token : User
ansible@pve, Token IDhomelab-token, décocher « Privilege Separation » pour que le token hérite des permissions de l’utilisateur
Donner les permissions nécessaires à ansible@pve :
Datacenter > Permissions > Add > User Permission
Path: /
User: ansible@pve
Role: PVEVMAdmin
PVEVMAdmin couvre la création, la modification et la suppression de VMs et LXC. Si tu veux aussi gérer les stockages, ajoute PVEDatastoreAdmin sur le chemin /storage.
L’interface affiche le token une seule fois à la création. Le noter immédiatement dans Ansible Vault.
Structure du projet Ansible
ansible-proxmox/
├── ansible.cfg
├── inventory/
│ └── hosts.yml
├── group_vars/
│ └── proxmox/
│ ├── vars.yml
│ └── vault.yml # chiffré avec ansible-vault
├── playbooks/
│ ├── create-lxc.yml
│ └── create-vm.yml
└── roles/
└── proxmox_lxc/
└── tasks/
└── main.yml
Fichier ansible.cfg :
[defaults]
inventory = inventory/hosts.yml
vault_password_file = ~/.ansible_vault_pass
host_key_checking = False
inventory/hosts.yml :
all:
children:
proxmox:
hosts:
heighliner:
ansible_host: 10.10.1.10
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
Ansible se connecte à heighliner en SSH pour exécuter les modules locaux et collecter les facts. Mais les modules proxmox et proxmoxkvm contactent l’API REST Proxmox via HTTP depuis la machine de contrôle – ils n’exécutent rien sur le nœud via SSH. La connexion SSH sert pour d’autres tâches (copier des fichiers, configurer le nœud lui-même).
groupvars/proxmox/vars.yml :
proxmox_api_host: "10.10.1.10"
proxmox_api_port: 8006
proxmox_api_user: "ansible@pve"
proxmox_node: "heighliner"
proxmox_storage: "local-zfs"
proxmox_template_storage: "local"
group_vars/proxmox/vault.yml (fichier chiffré) :
# Créer et chiffrer le vault
ansible-vault create group_vars/proxmox/vault.yml
Contenu :
vault_proxmox_api_token_id: "homelab-token"
vault_proxmox_api_token_secret: ""
Playbook 1 : créer un LXC Debian 12 minimal
Le module community.general.proxmox gère les conteneurs LXC. Il faut que le template soit déjà téléchargé sur le nœud Proxmox – on peut l’automatiser avec pveam, mais je le fais manuellement une fois :
# Sur heighliner, télécharger le template Debian 12
pveam update
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
Playbook playbooks/create-lxc.yml :
---
name: Créer un LXC Debian 12 sur Proxmox
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../group_vars/proxmox/vars.yml
- ../group_vars/proxmox/vault.yml
vars:
lxc_id: 120
lxc_hostname: "test-lxc"
lxc_ip: "10.10.30.120"
lxc_gw: "10.10.30.1"
lxc_memory: 512
lxc_cores: 1
lxc_disk_size: "8"
lxc_ssh_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
tasks:
- name: Créer le LXC
community.general.proxmox:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ lxc_id }}"
hostname: "{{ lxc_hostname }}"
ostemplate: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
storage: "{{ proxmox_storage }}"
disk: "{{ lxc_disk_size }}"
memory: "{{ lxc_memory }}"
cores: "{{ lxc_cores }}"
netif:
net0: "name=eth0,bridge=vmbr30,ip={{ lxc_ip }}/24,gw={{ lxc_gw }}"
features:
- "nesting=1"
pubkey: "{{ lxc_ssh_key }}"
unprivileged: true
onboot: true
state: present
register: lxc_result
- name: Démarrer le LXC
community.general.proxmox:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ lxc_id }}"
state: started
when: lxc_result.changed
- name: Afficher le résultat
debug:
msg: "LXC {{ lxc_id }} ({{ lxc_hostname }}) créé et démarré sur {{ lxc_ip }}"
when: lxc_result.changed
Exécuter :
ansible-playbook playbooks/create-lxc.yml
-e "lxc_id=120 lxc_hostname=mon-service lxc_ip=10.10.30.120"
Le state: present est idempotent : relancer le playbook sur un VMID existant ne recrée pas le LXC. Le module vérifie d’abord si un conteneur avec ce VMID existe, et si oui, ne fait rien. J’ai vérifié ce comportement avec un LXC actif – aucune interruption de service.
Utilise des plages de VMID cohérentes pour faciliter la lecture de pve status : 100-149 pour les LXC de services, 150-199 pour les LXC temporaires/tests, 200-249 pour les VMs KVM de services, 250-299 pour les VMs d’infrastructure (PBS, etc.).
Playbook 2 : créer une VM Ubuntu 24.04 via Cloud-init
Les VMs KVM avec cloud-init nécessitent une étape de préparation du template. Proxmox ne peut pas télécharger directement une image cloud-init depuis une URL – il faut d’abord importer le disque.
Préparer le template cloud-init (une fois)
# Sur heighliner - télécharger l'image cloud Ubuntu 24.04 LTS
wget -O /tmp/ubuntu-24.04-server-cloudimg-amd64.img
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Créer une VM template (VMID 9000 par convention)
qm create 9000 --name "ubuntu-2404-cloud" --memory 2048 --cores 2
--net0 virtio,bridge=vmbr30
# Importer le disque dans le stockage ZFS
qm importdisk 9000 /tmp/ubuntu-24.04-server-cloudimg-amd64.img local-zfs
# Configurer le disque importé comme disk principal
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-zfs:vm-9000-disk-0
# Ajouter le drive cloud-init
qm set 9000 --ide2 local-zfs:cloudinit
# Configurer le boot
qm set 9000 --boot c --bootdisk scsi0
# Activer l'agent QEMU
qm set 9000 --agent enabled=1
# Convertir en template
qm template 9000
# Nettoyer
rm /tmp/ubuntu-24.04-server-cloudimg-amd64.img
La commande qm template convertit la VM en template de façon irréversible. Un template ne peut pas être démarré directement – il ne peut qu’être cloné. Si tu veux tester l’image avant, démarrer la VM normalement d’abord, puis la convertir.
Playbook de création VM
playbooks/create-vm.yml :
---
name: Créer une VM Ubuntu 24.04 depuis le template cloud-init
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../group_vars/proxmox/vars.yml
- ../group_vars/proxmox/vault.yml
vars:
vm_id: 210
vm_name: "ubuntu-test"
vm_ip: "10.10.30.210"
vm_gw: "10.10.30.1"
vm_memory: 2048
vm_cores: 2
vm_disk_size: "20G"
vm_template_id: 9000
vm_ssh_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
vm_user: "pascal"
tasks:
- name: Cloner le template
community.general.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
newid: "{{ vm_id }}"
name: "{{ vm_name }}"
clone: "{{ vm_template_id }}"
full: true
storage: "{{ proxmox_storage }}"
state: present
register: vm_clone_result
- name: Configurer les ressources VM
community.general.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ vm_id }}"
memory: "{{ vm_memory }}"
cores: "{{ vm_cores }}"
balloon: 512
onboot: true
state: present
update: true
- name: Configurer cloud-init via API (options non couvertes par le module)
uri:
url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ proxmox_node }}/qemu/{{ vm_id }}/config"
method: POST
headers:
Authorization: "PVEAPIToken={{ proxmox_api_user }}!{{ vault_proxmox_api_token_id }}={{ vault_proxmox_api_token_secret }}"
body_format: json
body:
ciuser: "{{ vm_user }}"
sshkeys: "{{ vm_ssh_key | urlencode }}"
ipconfig0: "ip={{ vm_ip }}/24,gw={{ vm_gw }}"
nameserver: "10.10.10.101"
searchdomain: "arewel.local"
validate_certs: false
register: cloudinit_result
- name: Agrandir le disque scsi0
uri:
url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ proxmox_node }}/qemu/{{ vm_id }}/resize"
method: PUT
headers:
Authorization: "PVEAPIToken={{ proxmox_api_user }}!{{ vault_proxmox_api_token_id }}={{ vault_proxmox_api_token_secret }}"
body_format: json
body:
disk: "scsi0"
size: "{{ vm_disk_size }}"
validate_certs: false
- name: Démarrer la VM
community.general.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ vm_id }}"
state: started
when: vm_clone_result.changed
- name: Attendre la disponibilité SSH
wait_for:
host: "{{ vm_ip }}"
port: 22
timeout: 120
delay: 10
when: vm_clone_result.changed
- name: Afficher le résultat
debug:
msg: "VM {{ vm_id }} ({{ vm_name }}) disponible sur {{ vm_ip }}"
when: vm_clone_result.changed
Exécuter :
ansible-playbook playbooks/create-vm.yml
-e "vm_id=210 vm_name=ubuntu-test vm_ip=10.10.30.210"
Ce qui a coincé avec proxmoxkvm
Le module community.general.proxmoxkvm est complet pour l’essentiel, mais il ne couvre pas tous les paramètres de l’API Proxmox. En particulier, la configuration cloud-init (ciuser, sshkeys, ipconfig0, nameserver) n’est pas exposée directement dans le module – ou l’est partiellement selon les versions de la collection.
Ma solution : le task uri qui appelle l’API REST Proxmox directement avec le token d’authentification. Ce n’est pas élégant, mais c’est testable et reproductible. L’en-tête d’autorisation suit le format PVEAPIToken=USER!TOKENID=SECRET.
Le resize du disque a le même problème : proxmoxkvm accepte un paramètre disksize mais son comportement varie selon la version de la collection. Le task uri vers l’endpoint /resize est plus fiable.
La collection community.general pour Proxmox est activement maintenue mais suit l’API Proxmox avec un certain retard. Avant d’utiliser une option avancée, consulter le code source du module sur GitHub (ansible-collections/community.general/plugins/modules/proxmoxkvm.py) pour vérifier qu’elle est bien implémentée dans ta version de la collection.
Idempotence : est-ce qu’on peut relancer sans danger ?
J’ai testé l’idempotence en relançant chaque playbook sur des ressources existantes.
Pour le LXC : le module community.general.proxmox est idempotent sur state: present. Si le VMID existe, il ne fait rien et renvoie changed: false. Si le LXC est déjà démarré et qu’on demande state: started, pareil – pas de redémarrage.
Pour la VM KVM : le clone (state: present avec clone:) est idempotent si le VMID cible n’existe pas encore. Si la VM existe déjà, le module retourne une erreur par défaut. Il faut gérer ça avec ignoreerrors ou une condition when sur l’existence préalable du VMID. Je gère ça dans un rôle plus complet avec un task de vérification en amont :
- name: Vérifier si la VM existe déjà
community.general.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ vault_proxmox_api_token_id }}"
api_token_secret: "{{ vault_proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ vm_id }}"
state: current
register: vm_current
ignore_errors: true
name: Cloner uniquement si la VM n'existe pas
community.general.proxmox_kvm:
# ... options de clone ...
state: present
when: vm_current.failed
Résultat mesuré
Créer un LXC Debian 12 minimal à la main dans l’interface Proxmox : environ 8-10 minutes avec la saisie du réseau, le choix du template, l’attente du démarrage, la configuration SSH initiale.
Avec le playbook Ansible : 2 minutes 45 secondes en moyenne sur mes trois dernières créations. Le playbook inclut l’attente active du démarrage du LXC – les 2 min 45 incluent le temps de provisioning réel. Le gain est réel mais pas miraculeux : l’essentiel du gain vient de ne plus saisir les mêmes paramètres à la main et d’avoir la clé SSH injectée automatiquement.
Le vrai gain est l’erreur humaine évitée. Sur dix créations manuelles de LXC, j’avais fait trois fois une faute de frappe sur l’adresse IP, une fois un bridge incorrect, et une fois oublié d’activer « onboot ». Avec Ansible, ces paramètres sont dans les variables du playbook, vérifiés avant l’exécution, reproductibles.
Ce que je ferai ensuite
Terraform avec le provider bpg/proxmox est une alternative plus déclarative – tu décris l’état voulu, Terraform calcule le diff et applique. L’approche est différente d’Ansible : Terraform gère le cycle de vie complet (création, modification, destruction) de façon déclarative, là où Ansible est plus impératif. Pour un homelab, Ansible suffit. Pour une infrastructure partagée avec plusieurs personnes, Terraform avec son state file centralisé est plus adapté. C’est l’objet d’un prochain article.
Ce que j’intègre maintenant dans les playbooks : la configuration post-provisioning. Après la création du LXC, un second play se connecte en SSH et installe les packages de base (curl, wget, gnupg, ca-certificates) et configure le fuseau horaire. Ce n’est plus vraiment du provisioning Proxmox – c’est de la configuration système normale, et Ansible fait ça très bien.
slug: ansible-proxmox-automatisation
meta: Automatiser la création de LXC et VMs sur Proxmox VE 8.3 avec Ansible et community.general : token API, cloud-init, Vault, idempotence testée et métriques réelles.
tags: ansible, proxmox, automatisation, lxc, vm, cloud-init, homelab, infra-as-code