Introduction
Automatiser Proxmox avec le CI/CD signifie traiter les machines virtuelles comme du code : chaque VM est définie dans un dépôt versionné, chaque modification passe par une revue et des vérifications automatisées, et chaque déploiement laisse une trace d'audit. Ce guide s'adresse aux développeurs, ingénieurs DevOps et équipes techniques de startups qui souhaitent remplacer les clics manuels dans Proxmox par des pipelines reproductibles.
Vous apprendrez à :
- Inventorier votre environnement Proxmox et planifier l'automatisation en toute sécurité.
- Construire un pipeline CI/CD minimal mais prêt pour la production avec GitLab CI, Terraform et Ansible.
- Déployer et configurer une VM depuis zéro jusqu'à servir du trafic.
- Ajouter des contrôles de santé, des plans de retour arrière et des diagnostics de panne.
- Éviter les erreurs qui rendent l'automatisation Proxmox fragile et dangereuse.
Chaque exemple utilise des espaces réservés explicites (<placeholder>) et suppose Proxmox VE 8.x. Ne copiez jamais de véritables identifiants, jetons ou identifiants de production dans le contrôle de version.
L'objectif est la sécurité opérationnelle : observer avant de modifier, limiter le rayon d'impact, vérifier chaque étape et savoir comment récupérer avant d'en avoir besoin.
Inventaire des versions et de l'environnement
Avant d'écrire la moindre ligne de code de pipeline, collectez les faits concernant votre installation Proxmox. Cela évite les incompatibilités de versions et les hypothèses erronées sur l'API.
Ce qu'il faut capturer
Exécutez ces commandes en lecture seule sur un nœud Proxmox via SSH ou le shell web :
# Version de Proxmox
pveversion
# Sortie attendue : pve-manager/8.1.4/7d9c1b5e (running kernel: 6.5.11-7-pve)
# Statut du cluster (si en cluster)
pvecm status
# Sortie attendue : informations sur le quorum, liste des membres et chaque nœud marqué comme en ligne
# Stockage disponible pour les disques VM
pvesm status
# Sortie attendue : un tableau des identifiants de stockage, types et utilisation. Notez le type : dir, lvm, lvmthin, zfs, cephfs, nfs, etc.
# Ponts réseau
ip link show | grep -E '^[0-9]+: (vmbr|tap)'
# Sortie attendue : lignes montrant vmbr0, vmbr1, etc., avec état UP
Enregistrez ces informations dans un fichier environment.md dans votre dépôt d'automatisation :
# Instantané de l'environnement Proxmox
Date : 2025-02-20
Nœud : pve-node-1
Version PVE : 8.1.4
Cluster : oui (3 nœuds, quorum OK)
Stockage pour VM : local-lvm (lvmthin), ceph-pool (cephfs)
Ponts réseau : vmbr0 (gestion), vmbr1 (public VM)
Utilisateur d'automatisation : terraform@pve (jeton API uniquement)
Automatisation de l'inventaire
Vous pouvez automatiser cet inventaire avec un script planifié. Exemple avec Ansible :
# playbooks/facts.yml
- name: Collecter les faits Proxmox
hosts: pve_nodes
gather_facts: false
vars:
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
tasks:
- name: Exécuter pveversion
command: pveversion
register: pve_version
- name: Afficher la version
debug:
msg: "{{ pve_version.stdout }}"
Lancez-le avec ansible-playbook -i inventory/proxmox.yml playbooks/facts.yml. Planifiez-le chaque semaine ou dans la première étape de votre pipeline pour détecter les dérives.
Pourquoi c'est important
Si vous écrivez du code Terraform pour la version 2 de l'API Proxmox et que votre serveur est encore en version 1, tout échouera. Si vous supposez que local-lvm est disponible mais que le nœud utilise ZFS, vos définitions de disques seront incorrectes. Inventoriez d'abord, automatisez ensuite.
Chemin de configuration sécurisé
Les identifiants d'automatisation doivent suivre le principe du moindre privilège. Au lieu d'utiliser le mot de passe root, créez un utilisateur Proxmox dédié avec des autorisations de jeton API.
Créer un jeton API restreint
Dans l'interface web Proxmox :
- Allez dans Datacenter > Permissions > Utilisateurs et cliquez sur Ajouter.
- Nom d'utilisateur :
terraform@pve - Domaine : Authentification PVE
- Mot de passe : générez-en un fort, stockez-le dans un gestionnaire de secrets.
- Allez dans Datacenter > Permissions > Jetons API et cliquez sur Ajouter.
- Utilisateur :
terraform@pve - Identifiant du jeton :
ci-token - Décochez Séparation des privilèges (afin que le jeton ait les autorisations de l'utilisateur).
- Notez l'identifiant complet du jeton :
terraform@pve!ci-tokenet le secret (il n'est affiché qu'une seule fois).
- Allez dans Datacenter > Permissions > Ajouter > Permission de jeton API.
- Chemin :
/ - Jeton :
terraform@pve!ci-token - Rôle :
PVEVMAdmin(permet la gestion des VM mais pas les modifications au niveau de l'hôte).
Ce jeton peut créer, démarrer, arrêter et supprimer des VM, mais ne peut pas modifier les paramètres de stockage ou de réseau. Il convient au CI/CD.
Stocker les secrets en toute sécurité
Ne mettez jamais le secret du jeton dans le fichier de pipeline. Dans GitLab CI, utilisez des variables masquées :
PROXMOX_URL=https://pve-node-1.example.com:8006/api2/jsonPROXMOX_TOKEN_ID=terraform@pve!ci-tokenPROXMOX_TOKEN_SECRET= (la longue chaîne secrète, masquée)
Vous pouvez également utiliser HashiCorp Vault, AWS Secrets Manager ou les variables de GitLab CI. Le jeton doit être renouvelé tous les 90 jours, et le renouvellement doit faire partie de votre liste de contrôle des opérations.
Vérifier l'accès au jeton
Testez le jeton avec curl :
curl -k -H "Authorization: PVEAPIToken=terraform@pve!ci-token=<SECRET>" \
https://pve-node-1.example.com:8006/api2/json/nodes
# Attendu : tableau JSON des nœuds
Une erreur 401 ou 403 signifie que le jeton est mal configuré.
Construction du pipeline CI/CD avec Terraform et GitLab CI
Nous utiliserons Terraform pour définir une VM Proxmox, Ansible pour la configurer et GitLab CI pour tout orchestrer. Le même schéma fonctionne avec GitHub Actions, Jenkins ou tout exécuteur capable d'exécuter des conteneurs.
Structure du dépôt
proxmox-ci/
├── .gitlab-ci.yml
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── ansible/
│ ├── site.yml
│ └── inventory/
│ └── hosts.yml
└── scripts/
└── healthcheck.sh
Configuration du fournisseur Terraform
terraform/main.tf :
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "0.60.0"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_url
insecure = true # uniquement pour les certificats auto-signés ; mettre false avec une CA valide
api_token = "${var.proxmox_token_id}=${var.proxmox_token_secret}"
}
terraform/variables.tf :
variable "proxmox_url" {
type = string
sensitive = true
}
variable "proxmox_token_id" {
type = string
sensitive = true
}
variable "proxmox_token_secret" {
type = string
sensitive = true
}
variable "vm_name" {
type = string
default = "web-demo"
}
variable "vm_cpu" {
type = number
default = 2
}
variable "vm_memory" {
type = number
default = 2048
}
Définition de la VM
Ajoutez une ressource VM à main.tf en utilisant le modèle d'image cloud Ubuntu :
resource "proxmox_virtual_environment_vm" "web" {
name = var.vm_name
node_name = "pve-node-1"
vm_id = 9001
cpu {
cores = var.vm_cpu
}
memory {
dedicated = var.vm_memory
}
agent {
enabled = true
}
disk {
datastore_id = "local-lvm"
file_id = proxmox_virtual_environment_download_file.ubuntu_cloud_image.id
interface = "scsi0"
size = 20
}
network_device {
bridge = "vmbr1"
}
operating_system {
type = "l26"
}
initialization {
datastore_id = "local-lvm"
ip_config {
ipv4 {
address = "dhcp"
}
}
user_account {
username = "ubuntu"
keys = [file("~/.ssh/id_rsa.pub")]
}
}
}
Avant de l'utiliser, téléchargez un modèle d'image cloud. Vous pouvez le faire manuellement ou avec Terraform :
resource "proxmox_virtual_environment_download_file" "ubuntu_cloud_image" {
content_type = "iso"
datastore_id = "local"
node_name = "pve-node-1"
url = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
file_name = "noble-server-cloudimg-amd64.img"
}
Convertissez-le ensuite en modèle de VM. L'automatisation de la création du modèle dépasse le cadre de cet article, mais vous pouvez suivre le wiki Proxmox pour les images cloud-init.
Pipeline GitLab CI
.gitlab-ci.yml :
stages:
- validate
- plan
- deploy
- configure
- verify
variables:
TF_ROOT: terraform
ANSIBLE_HOST_KEY_CHECKING: "false"
before_script:
- apt-get update && apt-get install -y curl jq openssh-client
- cd $TF_ROOT
- terraform --version
validate:
stage: validate
script:
- terraform init -backend=false
- terraform validate
only:
- merge_requests
plan:
stage: plan
script:
- terraform init
- terraform plan -out=plan.tfplan -var-file=vars/prod.tfvars
artifacts:
paths:
- terraform/plan.tfplan
expire_in: 1 week
only:
- main
deploy:
stage: deploy
script:
- terraform init
- terraform apply -auto-approve plan.tfplan
- terraform output -json vm_ip > /tmp/vm_ip.json
- cat /tmp/vm_ip.json
artifacts:
paths:
- /tmp/vm_ip.json
expire_in: 1 day
when: manual
only:
- main
configure:
stage: configure
script:
- apt-get update && apt-get install -y ansible
- cd ansible
- echo "[web]" > inventory/hosts.ini
- IP=$(jq -r '.value' /tmp/vm_ip.json)
- echo "web ansible_host=$IP ansible_user=ubuntu ansible_ssh_private_key_file=$SSH_PRIVATE_KEY" >> inventory/hosts.ini
- ansible-playbook -i inventory/hosts.ini site.yml -e vm_ip=$IP
only:
- main
verify:
stage: verify
script:
- IP=$(jq -r '.value' /tmp/vm_ip.json)
- ./scripts/healthcheck.sh $IP
only:
- main
Configuration Ansible
ansible/site.yml :
- name: Configurer le serveur web
hosts: web
become: true
tasks:
- name: Mettre à jour le cache apt
apt:
update_cache: yes
- name: Installer nginx
apt:
name: nginx
state: present
- name: Démarrer nginx
service:
name: nginx
state: started
enabled: yes
- name: Créer index.html
copy:
content: "<html><body><h1>Déployé par CI/CD</h1></body></html>"
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: '0644'
Script de contrôle de santé
scripts/healthcheck.sh :
#!/bin/bash
IP=$1
for i in {1..5}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://$IP/)
if [ "$HTTP_CODE" == "200" ]; then
echo "Contrôle de santé réussi : HTTP 200"
exit 0
fi
echo "Tentative $i : obtenu $HTTP_CODE, nouvelle tentative dans 5s..."
sleep 5
done
echo "Contrôle de santé échoué"
exit 1
Résultats attendus et signaux d'échec
- terraform validate échoue si la syntaxe ou le bloc fournisseur est invalide.
- terraform plan montre les changements souhaités ; si aucun changement, la VM existe déjà.
- L'étape deploy applique le plan et affiche l'IP de la VM.
- L'étape configure installe nginx et le démarre.
- L'étape verify interroge l'IP et attend un code HTTP 200.
Si une étape échoue, le pipeline s'arrête. Corrigez l'erreur, validez et relancez. L'état Terraform est stocké dans le backend GitLab (vous devez configurer un backend, par exemple HTTP ou S3, ou utiliser l'état Terraform géré par GitLab).
Vérification et diagnostics
Après le déploiement, vérifiez toujours depuis l'extérieur de la VM, pas seulement depuis l'intérieur. Le script de contrôle de santé le fait déjà, mais voici des vérifications supplémentaires.
Vérifier le statut de la VM via l'API Proxmox
curl -k -H "Authorization: PVEAPIToken=terraform@pve!ci-token=<SECRET>" \
https://pve-node-1.example.com:8006/api2/json/nodes/pve-node-1/qemu/9001/status/current
# Attendu : JSON avec "status": "running"
Vérifier les journaux système dans la VM
Dans le playbook Ansible, ajoutez une tâche pour récupérer les journaux si nécessaire :
- name: Obtenir le journal d'erreurs nginx
command: tail -n 20 /var/log/nginx/error.log
register: nginx_log
ignore_errors: true
- name: Afficher le journal
debug:
msg: "{{ nginx_log.stdout }}"
Diagnostiquer les problèmes courants
| Symptôme | Cause probable | Commande de diagnostic |
|---|---|---|
| Terraform apply échoue avec 401 | Mauvais jeton ou secret | curl -I -H "Authorization: ..." https://pve/api2/json |
| VM créée mais n'obtient jamais d'IP | Cloud-init non configuré | Vérifiez la sortie console de la VM : qm terminal 9001 |
| SSH Ansible échoue | Pas de clé SSH injectée ou pare-feu | ssh -i key ubuntu@IP depuis l'exécuteur |
| Le contrôle de santé obtient une connexion refusée | nginx non démarré ou pare-feu | Dans la VM : systemctl status nginx, ufw status |
| API Proxmox lente ou délais d'attente | Mauvaise configuration réseau ou DNS | ping pve-node-1, curl -v l'API |
Capturez toujours la sortie exacte de la commande avant de faire des modifications. Le journal des tâches Proxmox (interface web > Nœud > Historique des tâches) montre les actions et erreurs de l'API.
Modes de défaillance et récupération
L'automatisation peut échouer de nombreuses façons. Voici les échecs les plus probables et comment les récupérer.
1. Corruption de l'état Terraform
Échec : Une exécution de pipeline est interrompue pendant que Terraform applique. Le fichier d'état sur le backend peut être verrouillé ou corrompu.
Récupération :
- Vérifiez si l'état est verrouillé :
terraform force-unlock <lock-id>(obtenez l'ID de verrouillage à partir de l'erreur). - Si l'état est corrompu, restaurez à partir d'une sauvegarde. L'état géré par GitLab a une gestion des versions ; vous pouvez revenir en arrière.
- Relancez
terraform planpuisapply.
2. VM déployée mais la configuration échoue
Échec : Terraform crée la VM, mais Ansible échoue à mi-chemin (par exemple, apt update échoue en raison du DNS). La VM reste dans un état partiellement configuré.
Récupération :
- Diagnostiquez via les journaux Ansible. Si corrigible, relancez uniquement la tâche en échec :
ansible-playbook -i inventory site.yml --start-at-task="Installer nginx" - Si la VM est inutilisable, détruisez-la avec Terraform :
terraform destroy -target=proxmox_virtual_environment_vm.webet redéployez. - Pour éviter l'état partiel, définissez
ignore_errors: falseet utilisezblock/rescuedans Ansible pour nettoyer.
3. Retour arrière vers une version précédente de la VM
Scénario : Un nouveau déploiement introduit une version cassée de l'application. Vous devez revenir à l'instantané précédent de la VM.
Récupération :
Si vous avez pris un instantané Proxmox avant le déploiement, le retour arrière est instantané :
qm rollback 9001 snap_before_update
# Attendu : la VM est ramenée à l'état de l'instantané, redémarrée
Vous devez intégrer la création d'instantanés dans votre pipeline avant d'exécuter des modifications de configuration :
qm snapshot 9001 snap_before_update --description "Avant la mise à jour de l'application"
Si vous utilisez une infrastructure immuable (traitez les VM comme jetables), vous déployez simplement la balise de version précédente depuis Git :
- Sur GitLab, balisez chaque build réussi avec
v1.2.3. - Dans votre pipeline, autorisez le déploiement manuel de n'importe quelle balise.
- Pour revenir en arrière, exécutez le job de déploiement pour la balise précédente.
4. Suppression accidentelle de la VM de production
Échec : Une modification Terraform supprime la ressource VM parce que quelqu'un a changé le count ou le nom.
Récupération :
- Le plan Terraform aurait montré la suppression. Exigez une approbation manuelle pour l'application.
- Planifiez des sauvegardes Proxmox (par exemple, nocturnes vers PBS ou NFS). Restaurez à partir de la sauvegarde.
- Utilisez le méta-argument de cycle de vie
prevent_destroysur les VM critiques.
lifecycle {
prevent_destroy = true
}
5. Le jeton API expire ou est révoqué
Échec : Le pipeline échoue avec 401 parce que le jeton a été renouvelé ou révoqué.
Récupération :
- Conservez le secret du jeton dans un gestionnaire de secrets avec renouvellement automatique.
- Dans GitLab, mettez à jour la variable CI et relancez le pipeline.
- Documentez l'expiration du jeton dans votre runbook.
Liste de contrôle des opérations
Utilisez cette liste avant et après chaque déploiement en production.
Liste de contrôle pré-déploiement
| Élément | Responsable | Fréquence |
|---|---|---|
| Versions Proxmox et stockage disponibles | Ingénieur DevOps | À chaque déploiement |
| Plan Terraform examiné et approuvé | Chef d'équipe | À chaque déploiement |
| Secrets renouvelés et valides | Ingénieur sécurité | Mensuel |
| Sauvegarde de la VM existante effectuée | Ingénieur DevOps | Avant les modifications de configuration |
| Script de contrôle de santé mis à jour pour le nouveau service | Développeur | À chaque déploiement |
Liste de contrôle post-déploiement
| Élément | Responsable | Fréquence |
|---|---|---|
| Contrôle de santé réussi | Pipeline CI | À chaque déploiement |
| VM en cours d'exécution et IP correcte | Ingénieur DevOps | À chaque déploiement |
| Les journaux ne montrent pas de boucles de crash | Développeur | Dans l'heure |
| Alertes de surveillance configurées | SRE | Hebdomadaire |
| Documentation mise à jour avec la nouvelle version | Rédacteur technique | Hebdomadaire |
Attribuez un responsable unique pour chaque élément de la liste de contrôle. Le responsable décide quand revoir l'élément et signe l'achèvement. Utilisez un système de tickets (Jira, GitLab issues) pour suivre ces tâches.
Pièges courants et comment les éviter
1. Utiliser le compte root pour l'automatisation
Pourquoi cela arrive : Commodité. Vous devez faire avancer les choses rapidement. Conséquence : Un jeton CI compromis donne un contrôle total de l'hôte. Évitement : Créez un utilisateur dédié avec un jeton API et un rôle minimal (PVEVMAdmin). Renouvelez régulièrement les jetons.
2. Coder en dur les secrets dans Git
Pourquoi cela arrive : Les développeurs veulent que le pipeline fonctionne sans configuration supplémentaire. Conséquence : Les secrets se retrouvent dans l'historique des commits, exposés à toute personne ayant accès au dépôt. Évitement : Utilisez les variables CI/CD, Vault ou des secrets externes. Analysez les dépôts à la recherche de secrets avec git-secrets ou la détection de secrets de GitLab.
3. Ne pas épingler les versions des fournisseurs
Pourquoi cela arrive : Utiliser latest ou omettre la version dans le fournisseur Terraform. Conséquence : Une nouvelle version du fournisseur peut casser votre configuration. Évitement : Épinglez des versions exactes dans required_providers et utilisez un fichier de verrouillage (terraform init génère .terraform.lock.hcl). Validez le fichier de verrouillage.
4. Sauter l'approbation du plan Terraform
Pourquoi cela arrive : Pour accélérer les déploiements, vous définissez toujours auto-approve. Conséquence : Un mauvais changement peut détruire la production sans examen humain. Évitement : Utilisez une étape d'approbation manuelle dans le pipeline (when: manual) avant l'application. Examinez la sortie du plan.
5. Pas de plan de retour arrière
Pourquoi cela arrive : Les équipes supposent que les nouvelles versions fonctionnent toujours. Conséquence : Lorsqu'une mauvaise version sort, la panique et les corrections manuelles provoquent des interruptions. Évitement : Ayez toujours un retour arrière par instantané ou un déploiement de la balise précédente prêt. Testez le retour arrière en staging avant la production.
6. Ignorer la configuration cloud-init
Pourquoi cela arrive : Vous créez une VM à partir d'une image disque sans cloud-init et vous vous attendez à ce que les clés SSH fonctionnent. Conséquence : La VM démarre mais vous ne pouvez pas vous connecter, Ansible échoue. Évitement : Utilisez des images cloud-init et un bloc initialization approprié dans Terraform. Testez l'accès SSH avant de continuer.
7. Ne pas surveiller après le déploiement
Pourquoi cela arrive : Le pipeline passe, tout le monde passe à autre chose. Conséquence : Le service échoue plus tard sans être remarqué. Évitement : Intégrez la surveillance (Prometheus, Grafana) et configurez des alertes. Incluez une étape de vérification qui s'exécute périodiquement, pas seulement une fois.
Conclusion
L'automatisation CI/CD avec Proxmox est un moyen puissant de gérer l'infrastructure comme du code, mais elle exige de la discipline. En suivant les étapes de ce guide — inventaire, configuration sécurisée, pipeline avec Terraform et Ansible, vérification et planification de la récupération — vous pouvez déployer des VM de manière fiable et revenir rapidement en arrière lorsque les choses tournent mal.
Commencez avec une VM à faible risque, mettez en œuvre le pipeline et entraînez-vous à un retour arrière. Ensuite, étendez-vous à plus de VM et à des applications complexes. Gardez votre automatisation versionnée, vos secrets en sécurité et vos contrôles de santé significatifs. Un flux de travail fiable rend les échecs visibles et la récupération une routine, pas une crise.