Léo DavidDocumentation
Connexion

Procédure de déploiement — IaC Proxmox (exemple)

Auteur : Leo David·Modifié le 03 août 2026

Procédure de déploiement — IaC Proxmox / exemple.local

⚠️ Document d'exemple. Adresses IP, VLAN et noms de machines de cette page sont volontairement génériques (schéma 10.77.x.x, domaine exemple.local, noms pve01 / gitlab01 / runner01) — à adapter à ton propre plan d'adressage avant usage.

Document unique de mise en œuvre. Les phases s'enchaînent dans l'ordre, chacune se termine par un contrôle : ne passe à la suivante que s'il est vert.

Proxmox OpenTofu GitLab Ansible Packer Debian

HyperviseurProxmox VE 9.x — nœud pve01 (10.77.30.1)
Stockagelocal-lvm — LVM-thin, format raw imposé
RéseauSDN, zone VLAN z01, cinq VNets
Domaineexemple.local — DC / DNS / NTP : 10.77.20.2
Adressage3ᵉ octet = n° de VLAN, passerelle toujours en .254
OS d'infrastructureDebian 13 « Trixie »
WindowsServer 2025 Standard, ISO en anglais, clavier AZERTY, Paris

Durée : environ une journée pour les phases 0 à 8, une seconde pour les phases 9 et 10 (Windows).



PHASE 0 — À préparer avant de toucher à quoi que ce soit

Ces trois points regroupent tout ce qui dépend d'autres équipes ou d'autres outils. Fais-les traiter en une fois, avant de commencer : c'est ce qui évite de rester bloqué au milieu d'une phase.

0.1 Plan d'adressage

Dans cet exemple, cinq VLAN suffisent : Administration (10), DNS/AD (20), Management/hyperviseur (30), GitLab (40), Automation/runner (50). DNS partout : 10.77.20.2. Le détail complet d'un plan réel vit dans tofu/networks.yaml.

Avant de commencer, réserve une IP par VM d'infrastructure (hyperviseur, DC, poste d'admin, GitLab, runner), plus une IP temporaire sur le VLAN Automation pour la construction du template Windows (le temps du packer build — elle doit être libre et hors de toute plage DHCP), et une IP par VM de test.

0.2 Enregistrements DNS à créer

Sur le DC 10.77.20.2, zone exemple.local. À faire avant la phase 4 : GitLab fige son URL externe au moment de l'installation, la changer ensuite est pénible.

Zone directe

TypeNomValeurNécessaire pour
Agitlab0110.77.40.1phase 4
Agitlab10.77.40.1phase 4 — c'est l'URL du service
Arunner0110.77.50.1phase 5

Zones inverses

Crée les zones 40.77.10.in-addr.arpa et 50.77.10.in-addr.arpa si elles n'existent pas, puis :

TypeNomValeur
PTR1.40.77.10.in-addr.arpagitlab01.exemple.local
PTR1.50.77.10.in-addr.arparunner01.exemple.local

Le PTR n'est pas cosmétique : Kerberos et la jonction au domaine s'appuient dessus.

Et pour les VM déployées ensuite ?

OSEnregistrement
Windowsautomatique — la machine s'enregistre après la jonction au domaine
Linuxmanuel — à créer avant chaque déploiement, sinon la VM est joignable par IP seulement

Le job validate du pipeline refuse d'ailleurs de déployer si un A record existe déjà pour le nom calculé : c'est un garde-fou anti-doublon.

C'est le premier candidat à l'automatisation une fois la chaîne en place (voir « Évolutions » en fin de document).

0.3 Flux à ouvrir

Ton runner est sur le VLAN Automation et doit traverser plusieurs réseaux. Tant que ces flux ne sont pas ouverts, les symptômes sont trompeurs et ne pointent jamais vers le pare-feu : runner « offline », tofu init en timeout, Packer figé sur « Waiting for WinRM ».

Le runner ne reçoit jamais rien. Il sort vers GitLab en 443, il n'écoute pas. Aucun flux entrant vers runner01 depuis GitLab n'est nécessaire.

Poste d'administration → infrastructure

Ton poste d'administration est une VM Windows sur le VLAN Administration : ces flux partent donc de ce VLAN, pas du Management.

DestinationPortUsage
hyperviseur8006/TCPinterface web Proxmox et console noVNC
hyperviseur22/TCPSSH hyperviseur
GitLab443/TCPinterface web GitLab
GitLab22/TCPgit clone / git push
runner22/TCPSSH VM d'administration IaC

Runner → le reste — les flux critiques

DestinationPortUsage
GitLab443/TCPrunner ↔ GitLab, backend OpenTofu, artefacts
GitLab22/TCPgit push en SSH
hyperviseur8006/TCPAPI Proxmox — clonage, cloud-init, Packer
DC/DNS53/TCP+UDPDNS
DC/DNS123/UDPNTP
VM Windows en construction5985/TCPPacker → VM Windows en construction
VLAN cibles22/TCPAnsible → VM Linux
VLAN cibles5986/TCPAnsible → VM Windows
VLAN ciblesICMP echocontrôle « IP libre » du job validate

L'ICMP n'est pas facultatif : sans lui le garde-fou anti-doublon d'adresse devient inopérant.

GitLab → le reste

DestinationPortUsage
DC/DNS53/TCP+UDPDNS
DC/DNS123/UDPNTP
DC/DNS636/TCPLDAPS, si tu actives l'authentification AD
relais SMTP25 ou 587/TCPnotifications

VM Windows déployées → contrôleur de domaine

Jeu complet requis pour la jonction. En oublier un donne des erreurs très peu explicites.

PortProtocoleService
53TCP+UDPDNS
88TCP+UDPKerberos
123UDPNTP — écart maximum toléré : 5 minutes
135TCPRPC endpoint mapper
389TCP+UDPLDAP
445TCPSMB / SYSVOL / netlogon
464TCP+UDPKerberos password change
636TCPLDAPS
3268 / 3269TCPcatalogue global
49152-65535TCPRPC dynamique

La plage RPC dynamique est le point qui coince le plus souvent. Si ton pare-feu ne peut pas l'ouvrir, restreins-la côté DC par registre puis n'ouvre que la plage réduite.

Sorties Internet

MachineDestinationsPort
Hyperviseurdownload.proxmox.com, enterprise.proxmox.com, cloud.debian.org, fedorapeople.org, miroirs Debian80/443
Runnerget.opentofu.org, github.com, registry.opentofu.org, releases.hashicorp.com, packages.gitlab.com, pypi.org, files.pythonhosted.org, galaxy.ansible.com, miroirs Debian80/443
GitLabpackages.gitlab.com, miroirs Debian80/443
VM Windows en constructioncloudbase.it, powershellgallery.com, Windows Update (ou WSUS interne)443

📄 Matrice détaillée et commandes de test : docs/FLUX-RESEAU.md

0.4 Convention de nommage (exemple)

Le pipeline calcule lui-même le nom des VM qu'il déploie, à partir d'un gabarit générique :

XXX    LP     YYY    SVC    01
│      │      │      │      └── incrément, calculé automatiquement
│      │      │      └───────── trigramme du service
│      │      └──────────────── site
│      └─────────────────────── L/W (OS) + P/R/Q (environnement)
└────────────────────────────── organisation
CodeOSEnvironnement
LP / LR / LQLinuxProd / Recette / Qualif
WP / WR / WQWindowsProd / Recette / Qualif

13 caractères, sous la limite NetBIOS de 15. Tu ne saisis jamais le nom : le pipeline le construit et cherche le premier incrément libre en interrogeant l'API Proxmox. Il comble les trous — si 01 et 03 existent, il propose 02.

Pour la suite de ce document, on simplifie et on utilise directement des noms courts (test-lnx01, test-win01) plutôt que le nom calculé complet.

0.5 Récapitulatif de la phase 0

  • IP réservées dans ton plan d'adressage
  • 3 enregistrements A créés
  • 2 zones inverses et 2 PTR créés
  • Flux ouverts et validés côté pare-feu
  • tofu/networks.yaml relu, VLAN confirmés
  • ISO Windows Server 2025 Standard (en-US) récupérée
  • Poste d'admin : client SSH, Git, et AC interne approuvée dans Cert:\LocalMachine\Root
  • Paire de clés SSH générée sur le poste d'admin (ssh-keygen -t ed25519)


PHASE 1 — Vérifications sur l'hyperviseur

Proxmox

Où : depuis le poste d'admin, en SSH root sur pve01.

Valide d'abord tes propres accès :

Test-NetConnection 10.77.30.1 -Port 8006
Test-NetConnection 10.77.30.1 -Port 22
hostname                            # doit renvoyer pve01
pveversion                          # 9.x
pvesm status                        # local-lvm actif, espace suffisant
pvesh get /cluster/sdn/zones        # la zone z01
pvesh get /cluster/sdn/vnets        # tes VNets

Si tu as touché au SDN récemment : Datacenter → SDN → Apply. Tant que ce n'est pas fait, les VNets n'existent pas comme bridges sur le nœud.

1.1 Fichier de clés d'administration

Les VM d'infrastructure (GitLab, runner, clones de maintenance) reçoivent ta clé publique par cloud-init. Il faut donc qu'elle soit disponible sur le nœud.

Sur le poste d'admin, si tu n'as pas encore de paire :

ssh-keygen -t ed25519 -C "admin@exemple.local"
type $env:USERPROFILE\.ssh\id_ed25519.pub

Sur pve01, en root, regroupe les clés autorisées :

cat > /root/iac-keys.pub <<'EOF'
ssh-ed25519 AAAA...ta-cle... admin@exemple.local
EOF
chmod 600 /root/iac-keys.pub

Le champ sshkeys de Proxmox accepte plusieurs clés, une par ligne. Ajoute celle de root sur le nœud si tu veux aussi pouvoir rebondir depuis l'hyperviseur :

[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519
cat /root/.ssh/id_ed25519.pub >> /root/iac-keys.pub

⚠️ Ne confonds pas avec la clé du runner (id_ed25519_ansible, générée en phase 6) : celle-ci est destinée aux VM déployées par le pipeline, pas aux VM d'infrastructure. Les deux usages restent séparés.

1.2 Plan de VMID

Toute l'infrastructure gérée par l'IaC vit au-dessus de 500, en zones distinctes. Ça isole la chaîne de tes VM existantes et supprime tout risque de collision.

ZoneUsageAttribution
510-589VM d'infrastructure IaC (GitLab, runner)manuelle
590-599Clones temporaires (maintenance de template)manuelle
600-997VM déployées par le pipelineautomatique
998-999Templates (Linux, Windows)manuelle

Les templates restent tout en haut, à l'écart de la plage d'attribution automatique — c'est la convention Proxmox la plus répandue et ça les rend immédiatement identifiables dans la liste.

Réserver la plage automatique

Pour que Proxmox attribue les VMID des VM du pipeline à partir de 600, déclare la plage dans /etc/pve/datacenter.cfg :

grep -q '^next-id:' /etc/pve/datacenter.cfg \
  && sed -i 's/^next-id:.*/next-id: lower=600,upper=998/' /etc/pve/datacenter.cfg \
  || echo 'next-id: lower=600,upper=998' >> /etc/pve/datacenter.cfg

cat /etc/pve/datacenter.cfg
pvesh get /cluster/nextid        # doit renvoyer 600 (ou le premier libre au-dessus)

La borne haute est exclusive : upper=998 fait s'arrêter l'attribution automatique à 997, ce qui préserve les deux templates.

C'est cette valeur que consulte le provider quand OpenTofu crée une VM sans VMID imposé. Le réglage vaut aussi pour les créations manuelles via l'interface web, ce qui est cohérent.

Si tu veux forcer un VMID précis pour une VM donnée, le pipeline accepte la variable OpenTofu vm_id.

Vérifier ce qui est déjà occupé

qm list
pct list                     # les conteneurs partagent le même espace de VMID
pvesh get /cluster/resources --type vm --output-format yaml | grep -E 'vmid|name'

Garde-fou avant chaque clonage

⚠️ Si le VMID cible est déjà pris, qm clone échoue — mais les qm set qui suivent s'appliquent à la VM existante et la reconfigurent en silence. Vérifie systématiquement :

VMID=510
qm config $VMID >/dev/null 2>&1 && echo "❌ VMID $VMID DÉJÀ PRIS — arrête-toi" \
  || echo "✅ VMID $VMID libre"

Si c'est déjà arrivé : ne redémarre pas la VM. Si elle tourne, les changements sont en attente et annulables : qm set <vmid> --revert memory,cores,balloon,onboot,net0,ipconfig0,nameserver,searchdomain,ciuser,sshkeys,scsi1 puis qm config <vmid> pour vérifier qu'il ne reste rien. Retire ensuite le disque ajouté : qm set <vmid> --delete scsi1, puis le volume unused0.

Contrôle : tes VNets apparaissent dans la liste, cat /root/iac-keys.pub affiche au moins une clé, pvesh get /cluster/nextid renvoie une valeur entre 600 et 997, et tes VMID cibles sont libres.


PHASE 2 — Compte et token API Proxmox

Où : en SSH root sur pve01.

pveum role add IaC -privs "Datastore.Allocate Datastore.AllocateSpace \
Datastore.Audit Pool.Allocate SDN.Use Sys.Audit Sys.Console Sys.Modify \
VM.Allocate VM.Audit VM.Clone VM.Config.CDROM VM.Config.Cloudinit \
VM.Config.CPU VM.Config.Disk VM.Config.HWType VM.Config.Memory \
VM.Config.Network VM.Config.Options VM.GuestAgent.Audit \
VM.GuestAgent.Unrestricted VM.Migrate VM.PowerMgmt"

pveum user add iac@pve
pveum aclmod / -user iac@pve -role IaC
pveum user token add iac@pve tofu --privsep 0

⚠️ Le secret n'est affiché qu'une seule fois et ne doit sortir de ce terminal que pour aller dans les variables CI. En cas de doute, révoque et recrée : pveum user token remove iac@pve tofu.

iac@pve!tofu=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Deux points sur cette liste :

  • SDN.Use est indispensable : sans lui, l'attachement de la carte réseau à un VNet échoue avec un « permission denied » peu explicite.
  • VM.Monitor n'existe plus depuis PVE 9. Il a été remplacé par Sys.Audit pour l'accès au moniteur KVM, et par les privilèges VM.GuestAgent.* pour l'agent invité — c'est l'agent qui remonte l'IP des VM au provider. Si tu trouves une documentation qui mentionne encore VM.Monitor, elle est écrite pour PVE 8.

Pour lister les privilèges valides sur ton installation :

pveum role list --output-format json \
  | jq -r '.[] | select(.roleid=="Administrator") | .privs' | tr ',' '\n' | sort

Contrôle : pveum user token permissions iac@pve tofu liste les privilèges.


PHASE 3 — Template Debian 13 (VMID 999)

Debian

Où : en SSH root sur pve01.

apt install -y libguestfs-tools

cd /var/lib/vz/template/
wget https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
sha512sum debian-13-genericcloud-amd64.qcow2
# comparer avec .../trixie/latest/SHA512SUMS

Préparation de l'image. Le retrait de libnss-mdns est important : .local est un TLD réservé au mDNS, et sans ça la résolution de *.exemple.local partirait en multicast au lieu d'interroger le DC.

virt-customize -a debian-13-genericcloud-amd64.qcow2 \
  --install qemu-guest-agent,python3,sudo,curl \
  --uninstall libnss-mdns \
  --run-command 'systemctl enable qemu-guest-agent' \
  --truncate /etc/machine-id
qm create 999 --name tmpl-debian13 --ostype l26 \
  --memory 2048 --cores 2 --cpu x86-64-v2-AES \
  --net0 virtio,bridge=n0050 \
  --scsihw virtio-scsi-single --agent enabled=1 \
  --serial0 socket --vga serial0

qm disk import 999 debian-13-genericcloud-amd64.qcow2 local-lvm
qm set 999 --scsi0 local-lvm:vm-999-disk-0,discard=on,ssd=1
qm set 999 --ide2 local-lvm:cloudinit
qm set 999 --boot order=scsi0
qm template 999

Sur un PVE plus ancien : qm importdisk au lieu de qm disk import.

Ne redimensionne pas le disque ici, OpenTofu le fera VM par VM.

Pourquoi le VLAN Automation sur le template ?

Aucun besoin particulier : ce choix est arbitraire et n'a pas d'effet sur les VM déployées. Le template ne démarre jamais, les paquets sont pré-installés hors ligne par virt-customize, et OpenTofu écrase le bridge au clonage en le déduisant de l'IP via networks.yaml. Proxmox régénère par ailleurs l'adresse MAC à chaque clone.

Ce VLAN est retenu pour un seul cas de figure : la maintenance du template. Pour le mettre à jour, on le clone, on démarre le clone, on applique les mises à jour puis on re-template — et ce clone a besoin d'un accès au miroir de paquets. Le VLAN Automation l'a déjà, puisque c'est celui du runner. C'est aussi celui où Packer construit le template Windows : toute la fabrique d'images reste au même endroit.

Mettre le template à jour plus tard

Le template ne contient aucun compte utilisable : l'image cloud Debian a un utilisateur debian verrouillé, sans mot de passe ni clé. C'est le fonctionnement normal d'une image cloud — l'identité est injectée par cloud-init au clonage. Il faut donc la fournir ici comme aux phases 4 et 5.

qm clone 999 590 --name tmpl-maj --full

# Identité : sans ces deux lignes, la VM démarre mais reste injoignable
qm set 590 --ciuser admin
qm set 590 --sshkey /root/iac-keys.pub

qm set 590 --ipconfig0 ip=10.77.50.201/24,gw=10.77.50.254
qm set 590 --nameserver 10.77.20.2
qm set 590 --searchdomain exemple.local
qm start 590
ssh admin@10.77.50.201       # cloud-init donne le sudo sans mot de passe
sudo apt update && sudo apt full-upgrade -y

# Remise à zéro de l'identité avant de re-templater
sudo cloud-init clean --logs --seed
sudo truncate -s 0 /etc/machine-id
sudo rm -f /etc/ssh/ssh_host_*
sudo poweroff
qm destroy 999
qm set 590 --name tmpl-debian13
qm set 590 --delete ipconfig0,nameserver,searchdomain,ciuser,sshkeys
qm template 590

Le VMID passe de 999 à 590 : re-clone ensuite en 999 pour conserver le numéro, sinon il faut mettre template_id_linux à jour dans tofu/variables.tf.

Les trois commandes de remise à zéro comptent : machine-id vide sinon toutes les VM partagent le même identifiant (baux DHCP et supervision cassés), cloud-init clean pour que la configuration soit rejouée au prochain démarrage, et la suppression des clés d'hôte SSH pour qu'elles soient régénérées par VM plutôt que partagées.

Contrôle : qm config 999 | grep template renvoie template: 1.


PHASE 4 — Serveur GitLab gitlab01 (VMID 510)

GitLab

Prérequis de cette phase

  • DNS : gitlab et gitlab01 → 10.77.40.1, + PTR
  • Flux : poste d'admin → GitLab en 443 et 22 ; GitLab → DC/DNS en 53 et 123 ; sortie Internet 443
  • Sur le poste d'admin : AC interne approuvée dans le magasin Windows, sinon le navigateur et git refuseront le certificat de GitLab

4.1 Créer la VM

Où : en SSH root sur pve01.

Vérifie d'abord que le VMID est libre (voir 1.2) : qm config 510 >/dev/null 2>&1 && echo "DÉJÀ PRIS"

qm clone 999 510 --name gitlab01 --full --storage local-lvm
qm resize 510 scsi0 40G
qm set 510 --memory 16384 --cores 4 --balloon 8192 --onboot 1
qm set 510 --scsi1 local-lvm:100,discard=on,ssd=1      # disque de données
qm set 510 --net0 virtio,bridge=n0040                  # SDN : aucun tag
qm set 510 --ipconfig0 ip=10.77.40.1/24,gw=10.77.40.254
qm set 510 --nameserver 10.77.20.2
qm set 510 --searchdomain exemple.local
qm set 510 --ciuser admin
qm set 510 --sshkey /root/iac-keys.pub
qm start 510

8 Go de RAM est le minimum absolu, 16 Go recommandés : une installation fraîche consomme déjà 6 Go.

4.2 Installer GitLab

Où : en SSH sur 10.77.40.1. Détail complet dans docs/00-serveur-gitlab.md (TLS, SMTP, LDAP, réglages petite instance, sauvegardes). Version courte :

sudo -i
hostnamectl set-hostname gitlab01.exemple.local
timedatectl set-timezone Europe/Paris
apt purge -y libnss-mdns avahi-daemon
apt update && apt full-upgrade -y
apt install -y curl ca-certificates perl openssh-server tzdata postfix

mkfs.ext4 -L gitlab-data /dev/sdb
mkdir -p /var/opt/gitlab
echo 'LABEL=gitlab-data /var/opt/gitlab ext4 defaults,noatime 0 2' >> /etc/fstab
mount -a

fallocate -l 4G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

curl -fsSL https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | bash
EXTERNAL_URL="https://gitlab.exemple.local" apt install -y gitlab-ce
cat /etc/gitlab/initial_root_password    # supprimé automatiquement après 24 h

Pose ensuite ton certificat d'AC interne dans /etc/gitlab/ssl/ et applique les réglages « petite instance », puis gitlab-ctl reconfigure.

.local étant réservé au mDNS, Let's Encrypt est impossible : AC interne ou certificat auto-signé obligatoire.

Contrôle : https://gitlab.exemple.local répond, mot de passe root changé, gitlab-rake gitlab:check SANITIZE=true propre.


PHASE 5 — VM d'administration runner01 (VMID 511)

OpenTofu

Prérequis de cette phase

  • DNS : runner01 → 10.77.50.1, + PTR
  • Flux : les six lignes « runner → le reste » de la phase 0.3

Où : en SSH root sur pve01.

Vérifie d'abord que le VMID est libre : qm config 511 >/dev/null 2>&1 && echo "DÉJÀ PRIS"

qm clone 999 511 --name runner01 --full --storage local-lvm
qm resize 511 scsi0 40G
qm set 511 --memory 4096 --cores 2 --onboot 1
qm set 511 --net0 virtio,bridge=n0050
qm set 511 --ipconfig0 ip=10.77.50.1/24,gw=10.77.50.254
qm set 511 --nameserver 10.77.20.2
qm set 511 --searchdomain exemple.local
qm set 511 --ciuser admin
qm set 511 --sshkey /root/iac-keys.pub
qm start 511

Contrôle — c'est ici que tu valides le routage inter-VLAN, avant d'avoir quoi que ce soit à déboguer par-dessus :

ssh admin@10.77.50.1
sudo apt install -y netcat-openbsd dnsutils

nc -zv 10.77.30.1 8006          # API Proxmox
nc -zv 10.77.40.1 443           # GitLab
nc -zv 10.77.40.1 22            # git SSH
dig +short gitlab.exemple.local @10.77.20.2

Les quatre doivent passer. Sinon, reprends la phase 0.3.


PHASE 6 — Outillage sur runner01

OpenTofu Ansible Packer

Où : en SSH sur 10.77.50.1.

Récupère le dépôt (clé USB, scp, peu importe : GitLab existe mais le projet n'est pas encore créé), puis :

cd iac-proxmox
sudo ./scripts/bootstrap-runner.sh

Le script installe OpenTofu, Ansible (+ pywinrm et les collections Windows), Packer et GitLab Runner. Il gère les deux pièges de Debian 13 : le dépôt APT HashiCorp sans suite trixie, et le refus de certaines signatures tierces par sqv.

Fais approuver ton AC interne, sinon tofu init échouera sur le backend GitLab :

sudo cp ma-ca-interne.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo apt purge -y libnss-mdns avahi-daemon
getent hosts gitlab.exemple.local        # doit renvoyer 10.77.40.1

Contrôle :

tofu version && ansible --version && packer version && gitlab-runner --version
curl -I https://gitlab.exemple.local     # 200 ou 302, sans erreur TLS
# Guillemets SIMPLES : le « ! » du token déclencherait sinon l'expansion
# d'historique de bash (« event not found »).
curl -s -H 'Authorization: PVEAPIToken=iac@pve!tofu=SECRET' \
  https://10.77.30.1:8006/api2/json/version

La dernière commande valide d'un coup le routage, le certificat et le token. Elle est volontairement sans -k : avec, elle réussirait même avec un certificat non approuvé et ne prouverait donc rien. Réponse attendue : {"data":{"release":"9.x",...}}.

SymptômeCause
event not foundguillemets doubles autour du token — utilise des simples
erreur TLScertificat de Proxmox non approuvé sur cette VM
pas de réponseflux 8006 fermé entre le VLAN Automation et le VLAN Management
401token erroné ou révoqué

PHASE 7 — Projet GitLab, runner et variables

GitLab

7.1 Pousser le dépôt

Dans GitLab : crée le groupe infra et le projet iac-proxmox (vide).

Où : sur runner01 (ou depuis le poste d'admin, au choix).

cd iac-proxmox
git init && git add . && git commit -m "init"
git remote add origin git@gitlab.exemple.local:infra/iac-proxmox.git
git push -u origin main

7.2 Enregistrer le runner

Dans GitLab : Settings → CI/CD → Runners → New project runner, tag iac, décoche « Run untagged jobs ». Récupère le jeton glrt-….

sudo gitlab-runner register --non-interactive \
  --url https://gitlab.exemple.local/ \
  --token glrt-XXXXXXXX \
  --executor shell --shell bash \
  --description runner01

# le runner tourne sous l'utilisateur gitlab-runner : il lui faut la clé SSH
sudo install -d -o gitlab-runner -g gitlab-runner -m 700 /home/gitlab-runner/.ssh
sudo install -o gitlab-runner -g gitlab-runner -m 600 \
  ~/.ssh/id_ed25519_ansible /home/gitlab-runner/.ssh/id_ed25519_ansible

7.3 Variables CI

Settings → CI/CD → Variables. Coche Masked et Protected sur les secrets.

VariableValeurMasked
PROXMOX_VE_ENDPOINThttps://10.77.30.1:8006/non
PROXMOX_VE_API_TOKEN_IDiac@pve!tofunon — pas un secret
PROXMOX_VE_API_TOKEN_SECRETl'UUID seul, sans =oui
PROXMOX_VE_INSECUREtrue si certificat auto-signénon
SSH_PUBLIC_KEYcontenu de ~/.ssh/id_ed25519_ansible.pubnon
AD_DOMAINexemple.localnon
AD_OU(vide — conteneur par défaut de l'AD)non
AD_JOIN_USERsvc_join@exemple.localoui
AD_JOIN_PASSWORDoui
WIN_BOOTSTRAP_PASSWORDmot de passe admin local du templateoui

⚠️ Pourquoi le token est scindé en deux variables : GitLab n'accepte de masquer que des valeurs composées de caractères de l'alphabet Base64 plus @ : . ~. Le ! de iac@pve!tofu en est exclu, donc une variable contenant le token complet ne peut pas être masquée et apparaîtrait en clair dans les journaux de pipeline. L'identifiant seul n'est pas sensible, l'UUID l'est : le pipeline les recompose au moment de l'exécution.

Le state OpenTofu est stocké dans GitLab, un state par VM (…/terraform/state/gitlab01). Rien à installer, et détruire une VM ne touche pas aux autres.

Contrôle : le runner apparaît vert dans Settings → CI/CD → Runners.


PHASE 8 — Premier déploiement Linux

Prérequis de cette phase

  • DNS : créer manuellement l'enregistrement A de la VM de test (test-lnx01 → 10.77.10.51). Les VM Linux ne s'enregistrent pas seules.
  • Flux : runner → VLAN Administration en 22/TCP et ICMP

Dans GitLab : Build → Pipelines → Run pipeline.

VM_SERVICE = TST
VM_IP      = 10.77.10.51
VM_OS      = linux
VM_ENV     = prod

Déroulement :

  1. nommage — calcule un nom conforme à la convention via l'API Proxmox
  2. validate — format du nom, IP libre au ping, pas de A record existant
  3. plan — affiche ce qui va être créé
  4. apply — ▶ manuel, c'est toi qui déclenches
  5. configure — Ansible : hostname, paquets, mises à jour

Vérifications :

Le compte des VM Linux déployées est ansible, avec la clé publique de la variable CI SSH_PUBLIC_KEY — c'est OpenTofu qui l'injecte via cloud-init (variable linux_user dans tofu/variables.tf). Il n'y a pas de mot de passe : l'accès est uniquement par clé.

ssh ansible@10.77.10.51
ip a                       # bonne IP, bon masque
cat /etc/resolv.conf       # 10.77.20.2

Côté Proxmox, onglet Réseau de la VM : la carte doit être sur le VNet Administration sans aucun tag VLAN. Si un tag apparaît, tu as un double marquage et la VM n'aurait pas de réseau.

Pour détruire : ACTION=destroy + VM_NAME_OVERRIDE=test-lnx01.

Contrôle : la VM répond en SSH, bon nom, bonne IP.


PHASE 9 — Template Windows (VMID 998)

Prérequis de cette phase

  • IP : 10.77.50.200 libre, hors plage DHCP
  • Flux : runner → 10.77.50.200 en 5985 ; 10.77.50.200 → Internet en 443 et → DC/DNS en 53
  • Pas de DNS à créer : la VM de construction est temporaire

Phase la plus longue et la plus capricieuse.

9.1 Déposer les ISO

Où : en SSH root sur pve01.

cd /var/lib/vz/template/iso/
wget https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
# + copie ici ton ISO Windows Server 2025 Standard (en-US)

9.2 Confirmer l'édition

L'Autounattend.xml est réglé sur Windows Server 2025 Standard (Desktop Experience), pilotes virtio 2k25, interface en-US, clavier AZERTY 040c:0000040c, fuseau Romance Standard Time.

Confirme la chaîne exacte — c'est l'erreur n°1 qui fait échouer Packer :

dism /Get-WimInfo /WimFile:D:\sources\install.wim

Deux variantes existent : Windows Server 2025 Standard (Server Core) et … (Desktop Experience). Prends celle avec Desktop Experience.

9.3 Adresse de construction

La VM en cours d'installation n'a pas encore cloudbase-init : elle se pose sur une adresse statique temporaire, et Packer s'y connecte directement.

Aucun DHCP n'est nécessaire — et c'est heureux : le DHCP intégré au SDN Proxmox ne fonctionne qu'avec les zones Simple, pas avec ta zone VLAN z01.

⚠️ Cette adresse figure à deux endroits qui doivent concorder :

FichierVariable
packer/windows/files/setup-winrm.ps1$BuildIP
packer/windows/variables.pkrvars.hclbuild_ip

Si elles divergent, Packer cherche au mauvais endroit et reste bloqué deux heures sur « Waiting for WinRM ».

9.4 Construire

Packer

Où : sur runner01.

cd packer/windows
cp variables.pkrvars.hcl.example variables.pkrvars.hcl
$EDITOR variables.pkrvars.hcl     # token, nom exact de l'ISO, build_ip
packer init .
packer build -var-file=variables.pkrvars.hcl .

Compte 45 à 90 minutes. En cas de blocage, ouvre la console noVNC de la VM en construction : tu verras immédiatement si c'est l'Autounattend.xml ou les pilotes.

Trois choses doivent être vraies à la fin :

  1. les pilotes virtio sont installés — sans eux, ni disque ni réseau ;
  2. cloudbase-init est configuré sur ConfigDriveService — c'est lui qui applique hostname, IP et DNS au premier démarrage du clone ;
  3. WinRM est actif avec le mot de passe bootstrap, sinon Ansible ne peut pas joindre le domaine.

Le mot de passe n'est pas transmis par cloud-init : peu fiable en configdrive2 sur Proxmox. Il est figé dans le template, Ansible s'y connecte puis le fait tourner. Les trois valeurs admin_password (Packer), <AdministratorPassword> (Autounattend) et WIN_BOOTSTRAP_PASSWORD (GitLab) doivent être strictement identiques.

Packer remet la carte en DHCP avant le sysprep, pour que le template ne conserve pas l'adresse de construction.

Contrôle : qm config 998 | grep template renvoie template: 1.


PHASE 10 — Premier déploiement Windows

Prérequis de cette phase

  • AD : compte svc_join avec le droit Create Computer Objects sur le conteneur par défaut
  • DNS : rien à créer, la machine s'enregistre après la jonction
  • Flux : runner → VLAN Administration en 5986 ; VLAN Administration → DC/DNS, jeu complet AD de la phase 0.3, plage RPC dynamique comprise

Run pipeline :

VM_SERVICE  = TST
VM_IP       = 10.77.10.52
VM_OS       = windows
VM_ENV      = prod
DOMAIN_JOIN = true

Le nom calculé (test-win01 dans cet exemple) remplace le préfixe Linux par le préfixe Windows.

Le playbook attend le redémarrage provoqué par cloudbase-init, vérifie que l'IP est appliquée, réapplique clavier / culture / fuseau, renomme la machine, la joint au domaine, redémarre puis fait tourner le mot de passe administrateur local.

Vérifications sur la VM :

(Get-CimInstance Win32_ComputerSystem).Domain    # exemple.local
nltest /dsgetdc:exemple.local
w32tm /stripchart /computer:10.77.20.2 /samples:3   # écart < 5 min

Contrôle : la machine apparaît dans l'AD et l'enregistrement DNS s'est créé tout seul.



Exploitation quotidienne

Créer une VM : Run pipeline → trigramme du service, IP, OS, environnement → valider le planapply. Le nom est calculé tout seul. Pense à créer l'enregistrement DNS avant pour une VM Linux.

Ajouter un réseau : cinq lignes dans tofu/networks.yaml, une merge request. Rien d'autre à toucher.

Ajouter un site : ajouter le code à trois lettres dans les options de VM_SITE du .gitlab-ci.yml.

Détruire une VM : ACTION=destroy + VM_NAME_OVERRIDE.


Dépannage

SymptômeCause probable
Nom hors convention au planVM_SERVICE ou VM_SITE ne font pas 3 lettres
no matching network for IPl'IP n'est dans aucune plage de networks.yaml
Attempted to load more than one elementdeux plages se chevauchent dans networks.yaml
VM démarrée, aucun traficdouble marquage VLAN : un tag posé alors que le VNet en porte déjà un
permission denied sur la carte réseauprivilège SDN.Use absent du rôle
Runner « offline », tofu init en timeoutflux 443 entre le VLAN Automation et le VLAN GitLab
x509: certificate signed by unknown authorityAC interne non installée sur le runner
Résolution DNS erratique en .locallibnss-mdns encore présent
VM Linux sans IPlecteur cloud-init absent du template
VM Windows sans IPcloudbase-init absent, mal configuré, ou ostype non Windows
Packer figé sur « Waiting for WinRM »$BuildIPbuild_ip, ou flux 5985 fermé
Ansible Windows : identifiants rejetésles trois mots de passe bootstrap diffèrent
Jonction AD qui échoue sans message clairplage RPC 49152-65535 fermée, ou écart d'horloge > 5 min
401 Unauthorized sur l'APItoken expiré, ou ! mal échappé dans la variable CI
apt update refuse une signaturesqv de Debian 13 — voir le script de bootstrap
GitLab 502 WhoopsPuma pas encore démarré, ou RAM insuffisante
qm clone échoue mais la VM est reconfiguréeVMID déjà pris : le clone refuse, les qm set suivants s'appliquent à la VM existante — voir 1.2

Évolutions

  • DNS automatique — c'est le premier chantier utile : créer le A et le PTR depuis Ansible avec community.windows.win_dns_record supprime la seule étape manuelle qui reste pour les VM Linux.
  • IPAM — remplacer la saisie de l'IP par une réservation automatique dans NetBox ou phpIPAM.
  • Sauvegarde — le pipeline pose déjà le tag iac sur les VM, il suffit de déclencher les jobs de backup Proxmox sur ce tag.
  • Secrets — passer les identifiants AD dans Vault plutôt que dans les variables CI.