kvm icon
Commands icon
Process icon
CreateVm icon
StartVm icon
SlackIncomingWebhook icon

Provision a KVM virtual machine on demand

Clone a golden image, define and boot a libvirt KVM domain, wait for RUNNING, and alert Slack, with retries and idempotency in Kestra.

Categories
Infrastructure

Provision a KVM virtual machine on demand with libvirt, without hand-running virsh over SSH or maintaining brittle shell scripts. This flow clones a read-only golden qcow2 image into a fresh per-VM boot disk, renders a libvirt domain XML template from your inputs, defines the domain on the target hypervisor, boots it, blocks until libvirt reports RUNNING, and posts the VM details to Slack. It solves the repeatable KVM provisioning problem: turning ad hoc, manual hypervisor steps into a versioned, retry-safe, idempotent workflow you can trigger by hand, on a schedule, or from an API.

How it works

  1. clone_boot_disk (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.core.runner.Process runner) runs virsh vol-clone to create the per-VM boot disk from the golden base_volume, then virsh vol-resize to grow it to disk_size_gb. It is idempotent: if the target volume already exists it is reused.
  2. create_vm (io.kestra.plugin.kvm.CreateVm with startAfterCreate: true) defines the domain from the rendered xmlDefinition and boots it. It is idempotent by name, so re-running keeps the definition in sync and the boot is a no-op if already running.
  3. wait_for_running (io.kestra.plugin.kvm.StartVm with waitForRunning: true and timeToWait: PT2M) blocks until the domain reaches RUNNING.
  4. notify_provisioned (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts a Slack Block Kit message with the VM name, UUID, state, and resources.

A flow-level errors block (alert_failure) posts a Slack alert on failure, every remote task carries a retry, the libvirt uri is shared via pluginDefaults, and concurrency.limit: 1 stops two runs from racing on the same domain name.

What you get

  • Repeatable, idempotent KVM provisioning from a single declarative YAML file.
  • Automatic boot-disk cloning and resizing from a golden image.
  • A blocking wait until the domain is RUNNING before downstream steps proceed.
  • Slack notifications on both success and failure, with retries on every call.

Who it's for

  • Platform and infrastructure engineers running on-prem or self-hosted KVM hosts.
  • Homelab and bare-metal operators who provision VMs with libvirt and qcow2 images.
  • Teams who want self-service VM provisioning without exposing raw hypervisor access.

Why orchestrate this with Kestra

libvirt has no native scheduler, no retry logic, and no notification layer: it just executes virsh commands when you run them. Kestra fills that gap. You get event and schedule triggers (run on a cron, or from an API/webhook call), automatic retry on transient SSH or hypervisor hiccups, full execution lineage and logs for every provision, and a declarative YAML definition you can version and review. Cross-tool steps like the Slack notification and the failure errors handler live in the same flow instead of in a separate cron job.

Prerequisites

  • A reachable libvirt/KVM host. The URI looks like qemu+ssh://root@your-host/system for remote SSH or qemu:///system for a local hypervisor.
  • A golden-image qcow2 named in base_volume present in the libvirt default pool.
  • A default libvirt virtual network for the VM NIC.
  • A Kestra worker able to run virsh against the host (the Process runner uses the worker's virsh and LIBVIRT_DEFAULT_URI).

Secrets

  • LIBVIRT_URI: the full libvirt connection URI used by every KVM task and the clone step (for example qemu+ssh://root@10.0.0.10/system).
  • SLACK_WEBHOOK: the Slack Incoming Webhook URL used by the success and failure alerts.

Quick start

  1. Add the LIBVIRT_URI and SLACK_WEBHOOK secrets to your Kestra instance.
  2. Ensure the golden image named in base_volume exists in the default pool.
  3. Execute the flow, adjusting vm_name, memory_mib, vcpus, os_volume, and disk_size_gb as needed.

How to extend

  • Add a post-boot SSH or ping check before notifying, since RUNNING means libvirt started the domain, not that the guest OS or cloud-init has finished.
  • Attach a cloud-init seed disk to the domain XML for first-boot configuration.
  • Add a Schedule trigger to provision burst capacity, or a webhook trigger for self-service requests.
  • Customize the domain XML for extra NICs, virtio devices, or larger CPU topologies.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.