m4nifest0-tech

Cerca nel sito

Automazione

Provisioning di VM Proxmox con template cloud-init e Ansible

Come creare un template cloud-init su Proxmox VE, clonarlo con qm e automatizzare lo stesso flusso con Ansible, tenendo i segreti fuori da Git.
Pubblicato il Aggiornato il 8 min di letturaLivello: Avanzato

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.

Sul nodo Proxmox
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 SHA512SUMS
Sul nodo Proxmox
qm 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 9000

Il 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

Sul nodo Proxmox
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 201

L'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.

Sulla workstation di amministrazione
python3 -m venv ~/venv-ansible
~/venv-ansible/bin/pip install ansible-core proxmoxer requests
~/venv-ansible/bin/ansible-galaxy collection install community.proxmox ansible.posix

Autenticazione 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

provisioning.yml
- 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: 300

Idempotenza

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.

inventory.ini
[vm_nuove]
vm-esempio ansible_host=192.0.2.50 ansible_user=debian
post-install.yml
- 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: present

L'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.

Sulla workstation di amministrazione
ansible-vault create group_vars/all/vault.yml
ansible-playbook provisioning.yml --ask-vault-pass

La 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.

.gitignore
.vault-pass
.env

Altre regole pratiche:

  • Niente valori sulla riga di comando. Un segreto passato con -e finisce nella cronologia della shell e nell'elenco dei processi.
  • no_log: true sui 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.

Articoli correlati