Aller au contenu
English
English
Virtualisation medium

Cloud-init avec KVM : VMs prêtes en 2 minutes

60 min de lecture

Logo KVM

Vous voulez créer des VMs KVM déjà configurées, sans installation manuelle ? Cloud-init + images cloud = VMs opérationnelles en moins de 2 minutes. Ce guide montre comment utiliser cloud-init spécifiquement avec KVM/libvirt.

  • Comprendre le principe image cloud + seed ISO
  • Créer un fichier user-data pour configurer vos VMs
  • Générer le seed ISO avec cloud-localds
  • Déployer une VM pré-configurée avec virt-install --import
  • Vérifier que cloud-init a bien appliqué votre configuration

Le gain n'est pas un gain de confort, c'est un changement de nature : la configuration d'une machine passe d'une suite de gestes à un fichier versionnable. Les deux sections ci-dessous fabriquent la même VM, la première à la main depuis une ISO, la seconde à partir d'une image déjà installée.

Sans cloud-init, créer une VM demande :

  1. Télécharger une ISO (~2 Go)
  2. Lancer l'installateur (~15-30 min)
  3. Répondre aux questions (langue, partitions, utilisateur...)
  4. Installer les packages manuellement
  5. Configurer SSH, sudo, etc.

Temps total : 30-60 minutes par VM. Et si vous devez en créer 10 ? Le processus manuel ne passe pas à l'échelle.

La logique s'inverse : au lieu d'installer puis de configurer, vous partez d'une image déjà installée et vous ne fournissez que la configuration. Les trois étapes ci-dessous se répètent à l'identique pour chaque VM, ce qui rend le provisionnement reproductible et scriptable.

  1. Télécharger une image cloud (~600 Mo, une seule fois)
  2. Écrire votre config dans un fichier YAML (1 minute)
  3. Lancer la VM, prête en 2 minutes

Le mécanisme repose sur la combinaison de deux disques au démarrage : l'image cloud qui porte le système, et un petit seed ISO qui porte votre configuration. Cloud-init, présent dans l'image, lit le seed au premier boot et applique ce qu'il y trouve. Retenez cette séparation, c'est elle qui permet de dériver des dizaines de machines distinctes d'une seule image de base.

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Image cloud │ + │ Seed ISO │ = │ VM configurée │
│ (Ubuntu, ...) │ │ (user-data) │ │ prête à SSH │
└─────────────────┘ └─────────────────┘ └─────────────────┘

Trois éléments suffisent à produire une VM configurée. L'image cloud est fournie par l'éditeur de la distribution et ne change pas ; les deux autres, user-data et meta-data, sont vos fichiers à vous. C'est la distinction à garder en tête : user-data décrit la configuration voulue, meta-data porte l'identité de l'instance, et cette dernière conditionne la ré-exécution de cloud-init.

ÉlémentCe que c'est
Image cloudDisque pré-installé (OS minimal)
user-dataVotre configuration (users, SSH, packages)
meta-dataIdentité de la VM (hostname, instance-id)

La séquence ci-dessous se déroule sans intervention, en quelques dizaines de secondes après le démarrage. Le point à comprendre est l'ordre : le réseau et les utilisateurs sont en place avant l'exécution de vos commandes finales, ce qui explique pourquoi un runcmd peut déjà télécharger des paquets ou appeler un service distant.

  1. La VM démarre sur l'image cloud
  2. Cloud-init détecte le seed ISO (monté comme CD-ROM)
  3. Il lit user-data et meta-data
  4. Il applique la configuration : crée les users, installe les packages, exécute vos scripts
  5. Résultat : VM prête à l'emploi, accessible en SSH
Fenêtre de terminal
sudo apt install cloud-image-utils

Ce paquet fournit cloud-localds, l'outil pour créer le seed ISO.

  1. Télécharger une image cloud

    Une image cloud est un disque pré-installé, optimisé pour être configuré par cloud-init. Contrairement à une ISO d'installation, elle contient déjà l'OS : il n'y a rien à installer.

    Fenêtre de terminal
    cd /var/lib/libvirt/images
    sudo curl -fLO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
    sudo curl -fLO https://cloud-images.ubuntu.com/noble/current/SHA256SUMS
    sha256sum --check --ignore-missing SHA256SUMS

    Ne sautez pas la vérification d'empreinte. Cette image devient le système de fichiers racine de toutes les VM que vous en dériverez : une image altérée se propage à chaque machine créée ensuite. La sortie attendue est noble-server-cloudimg-amd64.img: OK, ou : Réussi en français. Canonical publie aussi SHA256SUMS.gpg au même endroit, pour vérifier la signature du fichier de sommes lui-même.

    Pourquoi dans /var/lib/libvirt/images/ ? C'est le pool de stockage par défaut de libvirt. QEMU a les permissions pour y accéder.

  2. Créer le fichier user-data

    Le user-data est le cœur de cloud-init. C'est un fichier YAML qui décrit ce que vous voulez dans votre VM :

    Fenêtre de terminal
    cat > /tmp/user-data << 'EOF'
    #cloud-config
    hostname: ubuntu-cloud
    users:
    - name: devops
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
    - ssh-ed25519 AAAAC3Nza... votre-cle-publique
    package_update: true
    packages:
    - vim
    - htop
    - curl
    runcmd:
    - echo "Cloud-init terminé à $(date)" > /var/log/cloud-init-done.log
    EOF

    Décryptage ligne par ligne :

    DirectiveCe qu'elle fait
    #cloud-configIndique que c'est un fichier cloud-init (obligatoire en 1ère ligne)
    hostname:Définit le nom de la machine
    users:Crée des utilisateurs avec leurs permissions
    sudo: ALL=(ALL) NOPASSWD:ALLAutorise sudo sans mot de passe
    ssh_authorized_keys:Ajoute votre clé publique pour SSH
    package_update: trueLance apt update avant d'installer
    packages:Liste des paquets à installer
    runcmd:Commandes à exécuter à la fin
  3. Créer le fichier meta-data

    Le meta-data contient l'identité de la VM. C'est minimaliste mais important :

    Fenêtre de terminal
    cat > /tmp/meta-data << 'EOF'
    instance-id: ubuntu-cloud-001
    local-hostname: ubuntu-cloud
    EOF

    Pourquoi l'instance-id est crucial ? Cloud-init s'en sert pour décider s'il est sur une machine neuve. Il ne compare pas cet identifiant dans l'absolu : il le confronte à celui qu'il a mémorisé dans /var/lib/cloud/ sur le disque. Deux situations, souvent confondues, en découlent.

    Si vous repartez d'une image cloud vierge, ce cache n'existe pas : c'est un premier démarrage, et tout rejoue, même en réutilisant le même instance-id. Si vous redémarrez un disque déjà provisionné en gardant le même identifiant, cloud-init reconnaît la même instance et ne rejoue pas les modules marqués once-per-instance. C'est ce second cas, et lui seul, que l'on débloque en changeant l'identifiant.

  4. Générer le seed ISO

    Le seed ISO est une mini-image ISO qui contient vos fichiers user-data et meta-data. Cloud-init le détecte automatiquement au boot.

    Fenêtre de terminal
    cloud-localds /tmp/seed.iso /tmp/user-data /tmp/meta-data
    sudo cp /tmp/seed.iso /var/lib/libvirt/images/

    Sortie réelle :

    $ ls -la /tmp/seed.iso
    -rw-rw-r-- 1 bob libvirt 374784 janv. 31 18:35 /tmp/seed.iso

    ~370 Ko, c'est léger ! L'ISO contient juste vos fichiers de config.

  5. Créer le disque VM (backing file)

    Au lieu de copier l'image cloud (600 Mo), on crée un disque qui pointe vers elle. C'est le principe du backing file :

    Fenêtre de terminal
    sudo qemu-img create -f qcow2 -F qcow2 \
    -b /var/lib/libvirt/images/noble-server-cloudimg-amd64.img \
    /var/lib/libvirt/images/ubuntu-cloud.qcow2 20G

    Comment ça marche ?

    • L'image cloud reste intacte (lecture seule)
    • Le nouveau disque ubuntu-cloud.qcow2 ne stocke que les différences
    • Résultat : création instantanée, économie d'espace disque

    L'image cloud reste intacte et réutilisable : la personnalisation vit dans un fichier séparé, lu au premier démarrage, ce qui permet de dériver des dizaines de machines différentes d'une seule image de base.

  6. Créer la VM avec virt-install

    On assemble tout : le disque VM et le seed ISO. L'option --import dit à virt-install de démarrer directement, sans chercher à installer quoi que ce soit.

    Fenêtre de terminal
    virt-install \
    --name ubuntu-cloud \
    --memory 2048 \
    --vcpus 2 \
    --disk /var/lib/libvirt/images/ubuntu-cloud.qcow2 \
    --disk /var/lib/libvirt/images/seed.iso,device=cdrom \
    --network network=default \
    --os-variant ubuntu24.04 \
    --import \
    --graphics vnc \
    --noautoconsole

    Décryptage des options clés :

    OptionCe qu'elle faitPourquoi c'est important
    --disk ...seed.iso,device=cdromMonte le seed ISO comme CD-ROM virtuelCloud-init le détecte automatiquement
    --importDémarre sans installationL'image cloud est déjà prête
    --noautoconsoleRetourne au prompt immédiatementLa VM continue en arrière-plan
  7. Attendre et se connecter

    Cloud-init s'exécute au premier boot. Comptez 30-60 secondes pour qu'il :

    • Configure le réseau (DHCP)
    • Crée l'utilisateur
    • Installe les packages
    • Exécute vos commandes

    Récupérez l'IP avec :

    Fenêtre de terminal
    virsh net-dhcp-leases default

    Sortie réelle :

    Expiry Time MAC address Protocol IP address Hostname
    ------------------------------------------------------------------------------------
    2026-01-31 19:40:02 52:54:00:49:47:a3 ipv4 192.168.122.91/24 ubuntu-cloud

    Lecture : la VM ubuntu-cloud a obtenu l'IP 192.168.122.91 via DHCP.

    Connectez-vous :

    Fenêtre de terminal
    ssh devops@192.168.122.91

Une VM qui répond au ping ne prouve rien : cloud-init peut avoir démarré, buté sur un user-data invalide et s'être arrêté en chemin sans que personne ne le remarque. Les quatre tests ci-dessous se lancent depuis votre poste en moins d'une minute et contrôlent chacun une directive précise du fichier. Le dernier, cloud-init status, est celui qui tranche : il donne le verdict global que les trois autres ne font que confirmer.

Ce premier test valide d'un coup trois directives du user-data. En enchaînant hostname, whoami et sudo whoami sur une seule ligne SSH, vous vérifiez que le nom de machine, la création d'utilisateur et le sudo sans mot de passe ont tous été appliqués. Si le SSH lui-même aboutit, c'est aussi que votre clé publique a bien été déployée.

Fenêtre de terminal
ssh devops@192.168.122.91 "hostname && whoami && sudo whoami"

Sortie réelle :

ubuntu-cloud
devops
root

Verdict :

  • hostname vaut ubuntu-cloud, la directive hostname: est appliquée
  • whoami vaut devops, l'utilisateur est créé
  • sudo whoami vaut root sans demande de mot de passe, le sudo NOPASSWD est configuré

Ce test contrôle la directive packages:, celle qui échoue le plus discrètement. which ne répond que pour les binaires présents dans le PATH : une ligne absente de la sortie signale un paquet non installé, pas un simple alias manquant. Quand il en manque un, la cause est presque toujours un échec réseau au premier boot, et cloud-init status --long le confirme.

Fenêtre de terminal
ssh devops@192.168.122.91 "which vim htop curl"

Sortie réelle :

/usr/bin/vim
/usr/bin/htop
/usr/bin/curl

Les 3 paquets de la directive packages: sont bien présents dans le PATH.

La directive runcmd s'exécute en toute fin de séquence, une fois les utilisateurs créés et les paquets installés. Retrouver le fichier qu'elle écrit prouve donc que cloud-init est allé jusqu'au bout de votre configuration, et pas seulement qu'il a démarré. La date inscrite dans ce fichier est celle du premier boot : elle ne bougera plus aux démarrages suivants, puisque runcmd ne rejoue pas.

Fenêtre de terminal
ssh devops@192.168.122.91 "cat /var/log/cloud-init-done.log"

Sortie réelle :

Cloud-init terminé à Sat Jan 31 17:40:13 UTC 2026

La commande runcmd: a bien créé le fichier avec la date du premier boot.

Les trois tests précédents observent des effets ; celui-ci interroge cloud-init lui-même. Tant que la séquence n'est pas terminée, le statut n'est pas encore done, ce qui explique les faux négatifs quand on interroge la VM trop tôt après son démarrage. C'est la commande à lancer en premier lors d'un diagnostic, avant de soupçonner votre YAML.

Fenêtre de terminal
ssh devops@192.168.122.91 "sudo cloud-init status"

Sortie réelle :

status: done

done = tout s'est bien passé. Si vous voyez error, consultez la section Déboguer plus bas.

Maintenant que vous avez vu cloud-init fonctionner, détaillons les directives les plus utiles :

Le bloc users est celui que vous ajusterez le plus souvent. Deux options méritent votre vigilance : lock_passwd: false réautorise l'authentification par mot de passe, à ne faire que si vous en avez besoin en console VNC, et groups place l'utilisateur dans des groupes sensibles comme docker, qui équivaut à un accès root sur l'hôte du conteneur. Privilégiez toujours ssh_authorized_keys comme moyen d'accès principal.

users:
- name: devops # Nom de l'utilisateur
sudo: ALL=(ALL) NOPASSWD:ALL # Droits sudo sans mot de passe
groups: sudo, docker # Groupes supplémentaires
shell: /bin/bash # Shell par défaut
lock_passwd: false # Autoriser l'auth par mot de passe
ssh_authorized_keys: # Clés SSH autorisées
- ssh-ed25519 AAAAC3...
- ssh-rsa AAAAB3... # Plusieurs clés possibles

La directive passwd: attend un hash, jamais un mot de passe en clair : le user-data est recopié dans le seed ISO et dans /var/lib/cloud/ sur la VM, deux emplacements lisibles après coup. L'outil mkpasswd, fourni par le paquet whois sur Debian et Ubuntu, produit le hash SHA-512 attendu par /etc/shadow. Le mot de passe reste un complément : la clé SSH demeure le moyen d'accès de référence.

Fenêtre de terminal
# Installer mkpasswd
sudo apt install whois
# Générer un hash SHA-512
mkpasswd --method=SHA-512 --rounds=4096 "votremotdepasse"

Sortie :

$6$rounds=4096$2v2qIMF2mpxo0e2J$cHMrDN7oMGUBwQ...

Utilisez ce hash dans user-data :

users:
- name: devops
lock_passwd: false
passwd: $6$rounds=4096$2v2qIMF2mpxo0e2J$cHMrDN7oMGUBwQ...
ssh_authorized_keys:
- ssh-ed25519 AAAAC3...

La directive write_files dépose un fichier complet sur le système, contenu, permissions et propriétaire compris, sans passer par un script. Elle s'applique avant runcmd, ce qui permet d'écrire une configuration puis de démarrer le service qui la lit dans la foulée. Quotez toujours le mode ('0644') : sans les guillemets, YAML n'y voit plus une chaîne et les droits obtenus ne sont pas ceux que vous avez écrits.

write_files:
- path: /etc/motd
content: |
====================================
Serveur configuré par cloud-init
Ne pas modifier manuellement !
====================================
permissions: '0644'
owner: root:root

Deux directives lancent des commandes, et les confondre coûte cher. bootcmd intervient très tôt dans la séquence de démarrage et rejoue à chaque boot : tout ce qu'il contient doit donc supporter d'être exécuté des dizaines de fois. runcmd intervient à la fin, réseau disponible, et une seule fois pour l'instance. Installer un paquet ou démarrer un service relève de runcmd ; préparer un disque ou une interface avant le reste du système relève de bootcmd.

DirectiveQuandFréquenceUsage
bootcmdTrès tôt (avant réseau)Chaque bootConfig système bas niveau
runcmdÀ la finPremier boot seulementInstallation, scripts
# S'exécute une seule fois, à la fin
runcmd:
- systemctl enable nginx
- systemctl start nginx
- echo "Setup terminé" | logger

Ce fichier rassemble les directives vues plus haut dans l'ordre où cloud-init les traite, et il tient en une page. Trois choix méritent d'être relevés : disable_root: true interdit la connexion SSH directe en root, le durcissement SSH passe par un fichier déposé dans sshd_config.d/ plutôt que par une retouche du fichier principal, et le pare-feu n'est activé qu'après l'ouverture du port 22, sans quoi la VM se couperait d'elle-même. Remplacez les clés publiques avant de le rejouer : sans elles, la machine démarre inaccessible.

#cloud-config
# Hostname
hostname: web-prod-01
fqdn: web-prod-01.example.com
# Utilisateurs
users:
- name: deploy
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo, docker
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... admin@laptop
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... ci-runner
# Désactiver l'utilisateur par défaut (ubuntu, debian...)
disable_root: true
# Packages
package_update: true
package_upgrade: true
packages:
- nginx
- certbot
- python3-certbot-nginx
- fail2ban
- ufw
# Fichiers de configuration
write_files:
- path: /etc/ssh/sshd_config.d/hardening.conf
content: |
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
permissions: '0600'
# Commandes finales
runcmd:
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
- ufw --force enable
- systemctl enable nginx fail2ban
- systemctl start nginx fail2ban
- echo "Provisioning complete" | logger -t cloud-init-custom

Le seed ISO porte votre user-data en clair, clés publiques et éventuel hash de mot de passe compris, et il reste attaché à la VM tant que vous ne le retirez pas. L'enlever une fois la configuration appliquée réduit cette exposition. Les deux commandes ne font pas la même chose : virsh change-media --eject sort le média du lecteur, virsh detach-disk --config supprime le lecteur de la définition XML, donc de façon permanente.

Fenêtre de terminal
virsh change-media ubuntu-cloud sda --eject

Ou modifier la VM pour supprimer le disque seed :

Fenêtre de terminal
virsh detach-disk ubuntu-cloud sda --config

La quasi-totalité des échecs de provisionnement cloud-init se ramènent aux cinq cas ci-dessous. Deux reviennent sans cesse : la ligne #cloud-config oubliée, qui fait silencieusement ignorer tout le fichier, et le même instance-id réutilisé sur un disque déjà provisionné, qui empêche cloud-init de rejouer. Commencez toujours votre diagnostic par cloud-init status, avant de suspecter votre YAML.

ProblèmeCauseSolution
SSH refusedCloud-init pas terminéAttendre 60s, vérifier cloud-init status
Permission deniedClé SSH incorrecteVérifier la clé publique dans user-data
Hostname pas changé#cloud-config manquantPremière ligne obligatoire
Packages pas installésErreur réseauVérifier cloud-init status --long
Cloud-init ne se relance pasDisque déjà provisionné, même instance-idChanger l'instance-id, ou cloud-init clean

Deux sources d'information suffisent à traiter presque tous les cas, et elles ne disent pas la même chose. Le journal /var/log/cloud-init-output.log contient ce que vos commandes ont affiché ; le statut rendu par cloud-init status --long dit où cloud-init en est et quel module a échoué. Commencez par le statut, il oriente vers la bonne partie du journal.

Ce fichier capture la sortie standard et les erreurs de tout ce que cloud-init a lancé, y compris vos runcmd. C'est le premier endroit où regarder quand un paquet ne s'installe pas ou qu'un script échoue : le message d'erreur exact du programme fautif s'y trouve, ligne par ligne.

Fenêtre de terminal
ssh devops@IP "sudo cat /var/log/cloud-init-output.log"

Là où cloud-init status répond simplement done ou error, la variante --long précise quel module a échoué et quel datasource a été utilisé. La ligne DataSourceNoCloud confirme que votre seed ISO a bien été détecté, un point à vérifier en priorité si la configuration ne s'applique pas.

Fenêtre de terminal
ssh devops@IP "sudo cloud-init status --long"

Sortie si OK :

status: done
extended_status: done
boot_status_code: enabled-by-generator
detail:
DataSourceNoCloud [seed=/dev/sr0][dsmode=net]

cloud-init clean efface l'état conservé dans /var/lib/cloud/, y compris l'instance-id déjà traité. Au redémarrage suivant, cloud-init se croit sur une machine neuve et rejoue l'intégralité du user-data : c'est le moyen de tester une modification sans recréer la VM. L'option --logs supprime en plus les journaux, pour repartir d'une trace lisible. Les utilisateurs et les paquets déjà créés, eux, ne sont pas retirés.

Fenêtre de terminal
# Sur la VM
sudo cloud-init clean --logs
sudo reboot

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  1. Image cloud + seed ISO = VM configurée sans installation manuelle

  2. user-data contient votre config (users, SSH, packages, scripts)

  3. meta-data contient l'instance-id (doit être unique)

  4. cloud-localds génère le seed ISO à partir de user-data + meta-data

  5. virt-install --import démarre la VM directement depuis l'image cloud

  6. Cloud-init s'exécute une seule fois par instance : un disque neuf rejoue toujours, un disque déjà provisionné non

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn