Ansible KVM Libvirt — Create and Manage Virtual Machines

Introduction

KVM (Kernel-based Virtual Machine) with libvirt is the standard Linux virtualization stack. With the community.libvirt Ansible collection, you can automate VM creation, network configuration, storage pools, snapshots, and cloud-init provisioning — treating your hypervisors as cattle, not pets.

Prerequisites

# Install collection
ansible-galaxy collection install community.libvirt

# On the hypervisor host
sudo apt install qemu-kvm libvirt-daemon-system virtinst python3-libvirt
# or RHEL/CentOS:
sudo dnf install qemu-kvm libvirt virt-install python3-libvirt
# inventory.yml
all:
  children:
    hypervisors:
      hosts:
        kvm01:
          ansible_host: 192.168.1.50
          ansible_user: root

Create a Virtual Machine

---
- name: Create KVM virtual machines
  hosts: hypervisors
  become: true
  vars:
    vm_name: webserver01
    vm_vcpus: 2
    vm_memory_mb: 2048
    vm_disk_gb: 20
    vm_os_variant: ubuntu24.04
    vm_image: /var/lib/libvirt/images/ubuntu-24.04-server.qcow2
    vm_network: default

  tasks:
    - name: Ensure libvirtd is running
      ansible.builtin.systemd:
        name: libvirtd
        state: started
        enabled: true

    - name: Check if VM already exists
      community.libvirt.virt:
        command: list_vms
      register: existing_vms

    - name: Create VM disk from base image
      ansible.builtin.command:
        cmd: >
          qemu-img create -f qcow2 -F qcow2
          -b {{ vm_image }}
          /var/lib/libvirt/images/{{ vm_name }}.qcow2
          {{ vm_disk_gb }}G
        creates: "/var/lib/libvirt/images/{{ vm_name }}.qcow2"
      when: vm_name not in existing_vms.list_vms

    - name: Define VM
      community.libvirt.virt:
        command: define
        xml: "{{ lookup('template', 'vm.xml.j2') }}"
      when: vm_name not in existing_vms.list_vms

    - name: Start VM
      community.libvirt.virt:
        name: "{{ vm_name }}"
        state: running

    - name: Set VM to autostart
      community.libvirt.virt:
        name: "{{ vm_name }}"
        autostart: true

VM XML Template

<!-- templates/vm.xml.j2 -->
<domain type='kvm'>
  <name>{{ vm_name }}</name>
  <memory unit='MiB'>{{ vm_memory_mb }}</memory>
  <vcpu>{{ vm_vcpus }}</vcpu>
  <os>
    <type arch='x86_64'>hvm</type>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/><apic/>
  </features>
  <cpu mode='host-passthrough'/>
  <devices>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='/var/lib/libvirt/images/{{ vm_name }}.qcow2'/>
      <target dev='vda' bus='virtio'/>
    </disk>
    <interface type='network'>
      <source network='{{ vm_network }}'/>
      <model type='virtio'/>
    </interface>
    <channel type='unix'>
      <target type='virtio' name='org.qemu.guest_agent.0'/>
    </channel>
    <graphics type='vnc' port='-1' autoport='yes'/>
    <console type='pty'/>
  </devices>
</domain>

Cloud-Init Provisioning

    - name: Create cloud-init ISO
      ansible.builtin.command:
        cmd: >
          cloud-localds /var/lib/libvirt/images/{{ vm_name }}-cidata.iso
          /tmp/{{ vm_name }}-userdata.yml
          /tmp/{{ vm_name }}-metadata.yml
        creates: "/var/lib/libvirt/images/{{ vm_name }}-cidata.iso"

    - name: Generate user-data
      ansible.builtin.copy:
        content: |
          #cloud-config
          hostname: {{ vm_name }}
          users:
            - name: ansible
              sudo: ALL=(ALL) NOPASSWD:ALL
              ssh_authorized_keys:
                - {{ lookup('file', '~/.ssh/id_rsa.pub') }}
          package_update: true
          packages:
            - qemu-guest-agent
            - python3
        dest: "/tmp/{{ vm_name }}-userdata.yml"

Manage VM State

    - name: Start VM
      community.libvirt.virt:
        name: "{{ vm_name }}"
        state: running

    - name: Shutdown VM gracefully
      community.libvirt.virt:
        name: "{{ vm_name }}"
        state: shutdown

    - name: Force stop VM
      community.libvirt.virt:
        name: "{{ vm_name }}"
        state: destroyed

    - name: Pause VM
      community.libvirt.virt:
        name: "{{ vm_name }}"
        state: paused

    - name: Delete VM and disk
      block:
        - name: Destroy VM
          community.libvirt.virt:
            name: "{{ vm_name }}"
            state: destroyed
          ignore_errors: true

        - name: Undefine VM
          community.libvirt.virt:
            command: undefine
            name: "{{ vm_name }}"

        - name: Remove disk
          ansible.builtin.file:
            path: "/var/lib/libvirt/images/{{ vm_name }}.qcow2"
            state: absent

Storage Pools

    - name: Define storage pool
      community.libvirt.virt_pool:
        command: define
        name: vm-storage
        xml: |
          <pool type='dir'>
            <name>vm-storage</name>
            <target>
              <path>/var/lib/libvirt/images</path>
            </target>
          </pool>

    - name: Start and autostart pool
      community.libvirt.virt_pool:
        name: vm-storage
        state: active
        autostart: true

    - name: List pools
      community.libvirt.virt_pool:
        command: list_pools
      register: pools

Network Management

    - name: Define isolated network
      community.libvirt.virt_net:
        command: define
        name: app-network
        xml: |
          <network>
            <name>app-network</name>
            <bridge name='virbr1'/>
            <ip address='10.10.10.1' netmask='255.255.255.0'>
              <dhcp>
                <range start='10.10.10.100' end='10.10.10.200'/>
              </dhcp>
            </ip>
          </network>

    - name: Start network
      community.libvirt.virt_net:
        name: app-network
        state: active
        autostart: true

Batch VM Creation

---
- name: Create multiple VMs
  hosts: hypervisors
  become: true
  vars:
    vms:
      - name: web01
        vcpus: 2
        memory: 2048
        disk: 20
      - name: web02
        vcpus: 2
        memory: 2048
        disk: 20
      - name: db01
        vcpus: 4
        memory: 4096
        disk: 50

  tasks:
    - name: Get existing VMs
      community.libvirt.virt:
        command: list_vms
      register: existing

    - name: Create disks
      ansible.builtin.command:
        cmd: >
          qemu-img create -f qcow2
          /var/lib/libvirt/images/{{ item.name }}.qcow2
          {{ item.disk }}G
        creates: "/var/lib/libvirt/images/{{ item.name }}.qcow2"
      loop: "{{ vms }}"
      when: item.name not in existing.list_vms

    - name: Get VM info
      community.libvirt.virt:
        command: info
      register: vm_info

    - name: Show running VMs
      ansible.builtin.debug:
        msg: "{{ vm_info.keys() | list }}"

Troubleshooting

IssueSolution
Permission deniedAdd user to libvirt group or use become: true
VM won't startCheck virsh domblklist <vm> for missing disks
No networkVerify default network: virsh net-list --all
Python module missingInstall python3-libvirt on target host
QEMU/KVM not availableCheck `lsmod

Best Practices

  1. Use cloud-init for VM provisioning — no manual setup
  2. qcow2 backing files for thin provisioning — save disk space
  3. VirtIO drivers for best performance — disk and network
  4. Autostart critical VMs — survive host reboots
  5. Snapshot before changes — easy rollback
  6. QEMU guest agent — enables graceful shutdown and IP reporting

Conclusion

The community.libvirt collection gives you full control over KVM virtual machines, networks, and storage pools through Ansible. Combined with cloud-init and qcow2 backing files, you can spin up entire lab environments in minutes — all defined as code.