Installare ogni VM a mano produce macchine leggermente diverse e porta via tempo. Un template cloud-init sposta la personalizzazione (utente, chiavi SSH, rete) al primo avvio, e Ansible rende ripetibile l'intero processo: clonazione, configurazione e post-installazione stanno in un repository. La guida riguarda Proxmox VE 8 o 9 e VM Debian 12 o 13. Storage, VMID e indirizzi sono esempi.
Perché un template
Un template è una VM di riferimento da cui si clonano le nuove. Con cloud-init l'immagine resta generica: utente, chiavi SSH, hostname e indirizzo vengono iniettati al primo avvio da un'unità dedicata, il disco cloud-init. Le macchine risultano uniformi, nessun segreto finisce nell'immagine e la procedura è descritta da comandi invece che da passaggi manuali.
Creare il template con qm
Le immagini cloud ufficiali di Debian sono pubblicate su cloud.debian.org; la variante genericcloud è pensata per gli ambienti virtuali. Prima dell'uso conviene verificare il checksum con il file SHA512SUMS presente nella stessa directory.
wget https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
wget https://cloud.debian.org/images/cloud/trixie/latest/SHA512SUMS
sha512sum --ignore-missing -c SHA512SUMSqm create 9000 --name debian13-cloudinit --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci
qm set 9000 --scsi0 local-lvm:0,import-from=/root/debian-13-genericcloud-amd64.qcow2
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot order=scsi0
qm set 9000 --serial0 socket --vga serial0
qm template 9000Il secondo comando importa l'immagine direttamente come disco SCSI; in alternativa qm disk import (alias qm importdisk) la importa come disco non usato, da collegare poi con qm set. La console seriale è l'impostazione raccomandata da Proxmox per le immagini cloud.
Attenzione
Nomi degli storage (local-lvm), VMID e percorso dell'immagine sono esempi: vanno verificati con pvesm status e qm list prima di eseguire i comandi. Un nome di storage errato fa fallire l'importazione o, peggio, usa lo storage sbagliato.
Clonare e configurare una VM
qm clone 9000 201 --name vm-esempio --full 1
qm set 201 --ciuser debian --sshkeys /root/chiavi/amministratore.pub --ipconfig0 ip=192.0.2.50/24,gw=192.0.2.1
qm resize 201 scsi0 +8G
qm start 201L'opzione --sshkeys richiede il percorso di un file che deve esistere sul nodo, con una chiave pubblica per riga. Senza --ipconfig0 cloud-init usa DHCP. Al primo avvio, inoltre, estende il filesystem radice al disco ingrandito. Un clone completo (--full 1) è indipendente dal template; per un template Proxmox prova altrimenti un clone collegato, che ne dipende. In un cluster, come quello descritto in cluster Proxmox a tre nodi, un template su storage locale si clona solo sul nodo che lo ospita: per scegliere il nodo di destinazione serve uno storage condiviso.
Lo stesso flusso con Ansible
Requisiti
Nota
I moduli Proxmox risiedono nella collezione community.proxmox. In passato facevano parte di community.general (community.general.proxmox_kvm): i vecchi nomi restano come reindirizzamenti deprecati, destinati a essere rimossi, quindi i playbook nuovi usano il nuovo nome.
Il modulo richiede le librerie Python proxmoxer e requests nell'ambiente in cui viene eseguito: con hosts: localhost è lo stesso di Ansible. Un ambiente virtuale le tiene separate dal sistema.
python3 -m venv ~/venv-ansible
~/venv-ansible/bin/pip install ansible-core proxmoxer requests
~/venv-ansible/bin/ansible-galaxy collection install community.proxmox ansible.posixAutenticazione con token API
Il modulo si autentica con un token API di un utente dedicato, non con root@pam e non con una password. I privilegi si limitano a ciò che serve, per esempio il ruolo PVEVMAdmin su un pool di VM e PVEDatastoreUser sugli storage di destinazione, assegnati a utente e token con i comandi pveum mostrati nell'articolo sul monitoraggio con Prometheus e Grafana. Il template di origine deve rientrare nello stesso pool, oppure il token deve avere almeno il privilegio VM.Clone su di esso. Host e utente API stanno in group_vars/all/vars.yml. ID e valore del token (pve_api_token_id e vault_pve_api_token_secret) stanno in un file cifrato con Ansible Vault. Con certificati autofirmati è preferibile installare la CA sulla workstation invece di disattivare validate_certs.
Il playbook di provisioning
- name: Provisioning di una VM da template cloud-init
hosts: localhost
gather_facts: false
module_defaults:
community.proxmox.proxmox_kvm:
api_host: "{{ pve_api_host }}"
api_user: "{{ pve_api_user }}"
api_token_id: "{{ pve_api_token_id }}"
api_token_secret: "{{ vault_pve_api_token_secret }}"
vars:
nodo: nodo1
vm_nome: vm-esempio
vm_id: 201
tasks:
- name: Clona il template
community.proxmox.proxmox_kvm:
node: "{{ nodo }}"
clone: debian13-cloudinit
name: "{{ vm_nome }}"
newid: "{{ vm_id }}"
full: true
timeout: 300
register: clonazione
- name: Imposta utente, chiave SSH e rete
community.proxmox.proxmox_kvm:
node: "{{ nodo }}"
name: "{{ vm_nome }}"
update: true
ciuser: debian
sshkeys: "{{ lookup('ansible.builtin.file', '~/.ssh/id_ed25519.pub') }}"
ipconfig:
ipconfig0: "ip=192.0.2.50/24,gw=192.0.2.1"
when: clonazione is changed
- name: Avvia la VM
community.proxmox.proxmox_kvm:
node: "{{ nodo }}"
name: "{{ vm_nome }}"
state: started
- name: Attende la porta SSH
ansible.builtin.wait_for:
host: 192.0.2.50
port: 22
timeout: 300Idempotenza
Il clone è idempotente solo se si fissa newid: se esiste già una VM con quel VMID il task riporta changed=false e non clona di nuovo. Senza newid ogni esecuzione cercherebbe un ID libero e creerebbe un'altra VM. Il task con update: true non confronta la configurazione attuale e riporta sempre una modifica: per questo è vincolato all'esito del clone con when: clonazione is changed. state: started non fa nulla se la VM è già in esecuzione. La verifica è semplice: una seconda esecuzione deve chiudersi con changed=0. Il vincolo ha però un limite: se la prima esecuzione si interrompe dopo il clone, la seconda non riapplica la configurazione cloud-init, e la VM incompleta va eliminata o configurata a mano.
Configurazione post-installazione
Il secondo playbook lavora sulla VM appena avviata. Si collega con l'utente predefinito dell'immagine, che dispone di sudo, usando la chiave iniettata da cloud-init.
[vm_nuove]
vm-esempio ansible_host=192.0.2.50 ansible_user=debian- name: Configurazione post-installazione
hosts: vm_nuove
become: true
vars:
utente_operativo: operatore
tasks:
- name: Applica gli aggiornamenti
ansible.builtin.apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
- name: Crea l'utente operativo
ansible.builtin.user:
name: "{{ utente_operativo }}"
shell: /bin/bash
- name: Installa la chiave pubblica SSH
ansible.posix.authorized_key:
user: "{{ utente_operativo }}"
key: "{{ lookup('ansible.builtin.file', '~/.ssh/id_ed25519.pub') }}"
- name: Installa il guest agent
ansible.builtin.apt:
name: qemu-guest-agent
state: presentL'utente operatore è un account senza privilegi per le attività quotidiane: l'amministrazione passa dall'utente predefinito e da sudo. Il guest agent va abilitato anche nella configurazione della VM con qm set 201 --agent enabled=1. Lo stesso playbook è il posto naturale per distribuire la configurazione descritta in hardening di SSH su Debian.
Tenere i segreti fuori da Git
Il repository con i playbook è pensato per essere versionato e condiviso, quindi non deve contenere nulla che dia accesso all'infrastruttura. Ansible Vault cifra un intero file o una singola variabile: in Git finisce solo il testo cifrato.
ansible-vault create group_vars/all/vault.yml
ansible-playbook provisioning.yml --ask-vault-passLa prima riga apre l'editor e salva il file già cifrato. Al posto di --ask-vault-pass si può usare --vault-password-file con un file fuori dal repository e permessi 0600, oppure uno script che legge la credenziale da un gestore di segreti.
.vault-pass
.envAltre regole pratiche:
- Niente valori sulla riga di comando. Un segreto passato con
-efinisce nella cronologia della shell e nell'elenco dei processi. no_log: truesui task che maneggiano segreti, per non scriverli nell'output.- Uno scanner di segreti come gitleaks, eseguito prima di ogni commit, intercetta gli errori prima della pubblicazione.
Pericolo
Un token finito in un repository va considerato compromesso: va revocato e rigenerato. Un nuovo commit che lo rimuove non basta, perché il valore resta nella cronologia e in ogni copia già clonata.