Procédure de déploiement — IaC Proxmox (exemple)
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, domaineexemple.local, nomspve01/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.
| Hyperviseur | Proxmox VE 9.x — nœud pve01 (10.77.30.1) |
| Stockage | local-lvm — LVM-thin, format raw imposé |
| Réseau | SDN, zone VLAN z01, cinq VNets |
| Domaine | exemple.local — DC / DNS / NTP : 10.77.20.2 |
| Adressage | 3ᵉ octet = n° de VLAN, passerelle toujours en .254 |
| OS d'infrastructure | Debian 13 « Trixie » |
| Windows | Server 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
| Type | Nom | Valeur | Nécessaire pour |
|---|---|---|---|
| A | gitlab01 | 10.77.40.1 | phase 4 |
| A | gitlab | 10.77.40.1 | phase 4 — c'est l'URL du service |
| A | runner01 | 10.77.50.1 | phase 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 :
| Type | Nom | Valeur |
|---|---|---|
| PTR | 1.40.77.10.in-addr.arpa | gitlab01.exemple.local |
| PTR | 1.50.77.10.in-addr.arpa | runner01.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 ?
| OS | Enregistrement |
|---|---|
| Windows | automatique — la machine s'enregistre après la jonction au domaine |
| Linux | manuel — à 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
runner01depuis 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.
| Destination | Port | Usage |
|---|---|---|
| hyperviseur | 8006/TCP | interface web Proxmox et console noVNC |
| hyperviseur | 22/TCP | SSH hyperviseur |
| GitLab | 443/TCP | interface web GitLab |
| GitLab | 22/TCP | git clone / git push |
| runner | 22/TCP | SSH VM d'administration IaC |
Runner → le reste — les flux critiques
| Destination | Port | Usage |
|---|---|---|
| GitLab | 443/TCP | runner ↔ GitLab, backend OpenTofu, artefacts |
| GitLab | 22/TCP | git push en SSH |
| hyperviseur | 8006/TCP | API Proxmox — clonage, cloud-init, Packer |
| DC/DNS | 53/TCP+UDP | DNS |
| DC/DNS | 123/UDP | NTP |
| VM Windows en construction | 5985/TCP | Packer → VM Windows en construction |
| VLAN cibles | 22/TCP | Ansible → VM Linux |
| VLAN cibles | 5986/TCP | Ansible → VM Windows |
| VLAN cibles | ICMP echo | contrô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
| Destination | Port | Usage |
|---|---|---|
| DC/DNS | 53/TCP+UDP | DNS |
| DC/DNS | 123/UDP | NTP |
| DC/DNS | 636/TCP | LDAPS, si tu actives l'authentification AD |
| relais SMTP | 25 ou 587/TCP | notifications |
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.
| Port | Protocole | Service |
|---|---|---|
| 53 | TCP+UDP | DNS |
| 88 | TCP+UDP | Kerberos |
| 123 | UDP | NTP — écart maximum toléré : 5 minutes |
| 135 | TCP | RPC endpoint mapper |
| 389 | TCP+UDP | LDAP |
| 445 | TCP | SMB / SYSVOL / netlogon |
| 464 | TCP+UDP | Kerberos password change |
| 636 | TCP | LDAPS |
| 3268 / 3269 | TCP | catalogue global |
| 49152-65535 | TCP | RPC 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
| Machine | Destinations | Port |
|---|---|---|
| Hyperviseur | download.proxmox.com, enterprise.proxmox.com, cloud.debian.org, fedorapeople.org, miroirs Debian | 80/443 |
| Runner | get.opentofu.org, github.com, registry.opentofu.org, releases.hashicorp.com, packages.gitlab.com, pypi.org, files.pythonhosted.org, galaxy.ansible.com, miroirs Debian | 80/443 |
| GitLab | packages.gitlab.com, miroirs Debian | 80/443 |
| VM Windows en construction | cloudbase.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
| Code | OS | Environnement |
|---|---|---|
LP / LR / LQ | Linux | Prod / Recette / Qualif |
WP / WR / WQ | Windows | Prod / 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.yamlrelu, 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
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.
| Zone | Usage | Attribution |
|---|---|---|
| 510-589 | VM d'infrastructure IaC (GitLab, runner) | manuelle |
| 590-599 | Clones temporaires (maintenance de template) | manuelle |
| 600-997 | VM déployées par le pipeline | automatique |
| 998-999 | Templates (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,scsi1puisqm config <vmid>pour vérifier qu'il ne reste rien. Retire ensuite le disque ajouté :qm set <vmid> --delete scsi1, puis le volumeunused0.
✅ 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.Useest indispensable : sans lui, l'attachement de la carte réseau à un VNet échoue avec un « permission denied » peu explicite.VM.Monitorn'existe plus depuis PVE 9. Il a été remplacé parSys.Auditpour l'accès au moniteur KVM, et par les privilègesVM.GuestAgent.*pour l'agent invité — c'est l'agent qui remonte l'IP des VM au provider. Si tu trouves une documentation qui mentionne encoreVM.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)
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 importdiskau lieu deqm 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 danstofu/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)
Prérequis de cette phase
- DNS :
gitlabetgitlab01→ 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
gitrefuseront 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)
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
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ôme | Cause |
|---|---|
event not found | guillemets doubles autour du token — utilise des simples |
| erreur TLS | certificat de Proxmox non approuvé sur cette VM |
| pas de réponse | flux 8006 fermé entre le VLAN Automation et le VLAN Management |
401 | token erroné ou révoqué |
PHASE 7 — Projet GitLab, runner et variables
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.
| Variable | Valeur | Masked |
|---|---|---|
PROXMOX_VE_ENDPOINT | https://10.77.30.1:8006/ | non |
PROXMOX_VE_API_TOKEN_ID | iac@pve!tofu | non — pas un secret |
PROXMOX_VE_API_TOKEN_SECRET | l'UUID seul, sans = | oui |
PROXMOX_VE_INSECURE | true si certificat auto-signé | non |
SSH_PUBLIC_KEY | contenu de ~/.ssh/id_ed25519_ansible.pub | non |
AD_DOMAIN | exemple.local | non |
AD_OU | (vide — conteneur par défaut de l'AD) | non |
AD_JOIN_USER | svc_join@exemple.local | oui |
AD_JOIN_PASSWORD | … | oui |
WIN_BOOTSTRAP_PASSWORD | mot de passe admin local du template | oui |
⚠️ 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!deiac@pve!tofuen 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 :
- nommage — calcule un nom conforme à la convention via l'API Proxmox
- validate — format du nom, IP libre au ping, pas de A record existant
- plan — affiche ce qui va être créé
- apply — ▶ manuel, c'est toi qui déclenches
- 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.200libre, 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 :
| Fichier | Variable |
|---|---|
packer/windows/files/setup-winrm.ps1 | $BuildIP |
packer/windows/variables.pkrvars.hcl | build_ip |
Si elles divergent, Packer cherche au mauvais endroit et reste bloqué deux heures sur « Waiting for WinRM ».
9.4 Construire
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 :
- les pilotes virtio sont installés — sans eux, ni disque ni réseau ;
- cloudbase-init est configuré sur
ConfigDriveService— c'est lui qui applique hostname, IP et DNS au premier démarrage du clone ; - 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
configdrive2sur Proxmox. Il est figé dans le template, Ansible s'y connecte puis le fait tourner. Les trois valeursadmin_password(Packer),<AdministratorPassword>(Autounattend) etWIN_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_joinavec 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 plan → apply. 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ôme | Cause probable |
|---|---|
Nom hors convention au plan | VM_SERVICE ou VM_SITE ne font pas 3 lettres |
no matching network for IP | l'IP n'est dans aucune plage de networks.yaml |
Attempted to load more than one element | deux plages se chevauchent dans networks.yaml |
| VM démarrée, aucun trafic | double marquage VLAN : un tag posé alors que le VNet en porte déjà un |
permission denied sur la carte réseau | privilège SDN.Use absent du rôle |
Runner « offline », tofu init en timeout | flux 443 entre le VLAN Automation et le VLAN GitLab |
x509: certificate signed by unknown authority | AC interne non installée sur le runner |
Résolution DNS erratique en .local | libnss-mdns encore présent |
| VM Linux sans IP | lecteur cloud-init absent du template |
| VM Windows sans IP | cloudbase-init absent, mal configuré, ou ostype non Windows |
| Packer figé sur « Waiting for WinRM » | $BuildIP ≠ build_ip, ou flux 5985 fermé |
| Ansible Windows : identifiants rejetés | les trois mots de passe bootstrap diffèrent |
| Jonction AD qui échoue sans message clair | plage RPC 49152-65535 fermée, ou écart d'horloge > 5 min |
401 Unauthorized sur l'API | token expiré, ou ! mal échappé dans la variable CI |
apt update refuse une signature | sqv de Debian 13 — voir le script de bootstrap |
GitLab 502 Whoops | Puma pas encore démarré, ou RAM insuffisante |
qm clone échoue mais la VM est reconfigurée | VMID 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_recordsupprime 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
iacsur 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.