Modern libvirt-driven Terraform examples
This repository contains Terraform recipes to deploy various modern virtual machines using QEMU and libvirt.
By modern, it is meant virtual machines that leverage the use of modern desktop-oriented technologies, like UEFI firmware and recent virtual motherboard chipset (i.e. Phyllome OS itself), by staying as close as possible as domain definitions maintained here.
Organization
The folder multiple contains two subfolders, one with shared modules and the other with the various target deployment environments.
The idea is to reuse modules across multiple virtual machines and operating systems.
./multiple:
environments shared_modules
./multiple/environments:
cloud_init.yaml ubuntu-cloud-server-2404-bios
./multiple/environments/ubuntu-cloud-server-2404-bios:
ubuntu-cloud-server-2404-bios.tf
./multiple/shared_modules:
cloud-init.tf domain.tf network.tf outputs.tf pool.tf provider.tf variables.tf volume.tf
Requirements
Assumptions
Your Linux x86_64-based machine has at least 4 GB of available memory and 2 CPUs.
How to use it
- Clone this repository
- Go to folder example
- Execute the following commands, which will download and install the required Terraform provider if not already present
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of dmacvicar/libvirt from the dependency lock file
- Using previously-installed dmacvicar/libvirt v0.8.3
Terraform has been successfully initialized!
[...]
- The following command will plan the deployment, describing actions that will be taken when applied
$ terraform plan
[...]
Terraform will perform the following actions
# A cloud-init ISO disk is created, which provides pre-configured settings and scripts that are applied to a cloud-native disk image during its initial boot. Without it, no user would be created and it would not be possible to log into the virtual machine
+ resource "libvirt_cloudinit_disk" "commoninit" {
+ name = "commoninit.iso"
+ pool = "ubuntu-bios"
[...]
}
# The libvirt domain or virtual machine will be created
+ resource "libvirt_domain" "domain" {
+ cloudinit = (known after apply)
[...]
}
# Here, a libvirt pool to store the virtual machine disk image will be created. It should be possible to use the default one
+ resource "libvirt_pool" "ubuntu-bios" {
[...]
+ name = "ubuntu-bios"
+ type = "dir"
+ target {
+ path = "/tmp/ubuntu-bios"
}
}
# A qcow2 disk volume will be created and stored in the previously created pool, based on a Ubuntu noble (24.04) hosted cloud image
+ resource "libvirt_volume" "ubuntu-qcow2" {
+ format = "qcow2"
[...]
# The plan summaries the action to be taken, which in this case is about creating resources
Plan: 4 to add, 0 to change, 0 to destroy.
- The last command will carry out the plan
$ terraform apply
[...]
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
# The actions are carried out
libvirt_pool.ubuntu-bios: Creating...
libvirt_pool.ubuntu-bios: Creation complete after 0s
[...]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
- Identify the created machine
$ sudo virsh list --all
Id Name State
---------------------------------------------
10 ubuntu-cloud-server-2404-0 running
- Determine its IP address
$ sudo virsh domifaddr ubuntu-cloud-server-2404-0
Name MAC address Protocol Address
----------------------------------------------------------------
vnet3 52:54:00:e2:51:c0 ipv4 192.168.122.24/24
- Connect to the machine
$ ssh root@192.168.122.24
[...]
# Use the password defined in the cloud-init.cfg file
root@192.168.122.24's password:
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-71-generic x86_64)
[...]
System information as of Tue Aug 26 10:40:49 UTC 2025
System load: 0.0 Processes: 113
Usage of /: 67.4% of 2.35GB Users logged in: 0
Memory usage: 5% IPv4 address for ens3: 192.168.122.24
Swap usage: 0%
- Exit the virtual machine
root@ubuntu$ exit
- To destroy the virtual machine, execute the following command
$ terraform destroy
[...]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# libvirt_cloudinit_disk.commoninit will be destroyed
- resource "libvirt_cloudinit_disk" "commoninit" {
[...]
# libvirt_domain.domain[0] will be destroyed
- resource "libvirt_domain" "domain" {
- arch = "x86_64" -> null
[...]
}
}
# libvirt_pool.ubuntu2 will be destroyed
- resource "libvirt_pool" "ubuntu2" {
- allocation = 798310400 -> null
- available = 16019255296 -> null
[...]
}
}
# libvirt_volume.ubuntu-qcow2 will be destroyed
- resource "libvirt_volume" "ubuntu-qcow2" {
[...]
}
Plan: 0 to add, 0 to change, 4 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
libvirt_domain.domain[0]: Destroying... [id=611d5ede-e4b4-4ca5-ad83-83030942a6b5]
libvirt_domain.domain[0]: Destruction complete after 0s
libvirt_cloudinit_disk.commoninit: Destroying... [id=/tmp/cluster_storage2/commoninit.iso;5f4e08ef-ad51-484f-a9f2-c926f582974a]
libvirt_volume.ubuntu-qcow2: Destroying... [id=/tmp/cluster_storage2/ubuntu-qcow2]
libvirt_cloudinit_disk.commoninit: Destruction complete after 0s
libvirt_volume.ubuntu-qcow2: Destruction complete after 0s
libvirt_pool.ubuntu2: Destroying... [id=dbd62f8b-5d09-4e96-87e2-88e95c582896]
libvirt_pool.ubuntu2: Destruction complete after 0s
Destroy complete! Resources: 4 destroyed.
Explanations
Let's take a look inside the ubuntu-cloud-server-2404-bios folder, which contains two files, ubuntu-cloud-server-2404-bios.tf and cloud_init.cfg
The first file ubuntu-cloud-server-2404-bios.tf contains the main configuration for the Terraform deployment.
- It starts by defining the required Terraform version and provider
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
- The specific provider is defined here
provider "libvirt" {
uri = "qemu:///system"
}
The connection URI of the libvirt instance can be defined. One could for instance specific a libvirt instance that is hosted remotely
- A libvirt pool, to store the virtual machine image, is created:
resource "libvirt_pool" "ubuntu-bios" {
name = "ubuntu-bios"
type = "dir"
target {
path = "/tmp/ubuntu-bios"
}
}
- The cloud-init user data will be fetched from a specific file whose path has to be declared:
data "template_file" "user_data" {
template = file("${path.module}/cloud_init.cfg")
}
- The ISO cloud-init disk will be created:
resource "libvirt_cloudinit_disk" "commoninit" {
name = "commoninit.iso"
user_data = data.template_file.user_data.rendered
pool = libvirt_pool.ubuntu-bios.name
}
- Perhaps the most important, the domain will be created:
Values can be adjusted, such as memory or vCPU counts. In the examples, multiple virtio-based device hardware are created, such as a virtio-gpu.
resource "libvirt_domain" "domain" {
count = 1
name = "ubuntu-cloud-server-2404-${count.index}"
memory = "4092"
vcpu = 2
cloudinit = libvirt_cloudinit_disk.commoninit.id
cpu {
mode = "host-model"
}
disk {
volume_id = libvirt_volume.ubuntu-qcow2.id
}
console {
type = "pty"
target_port = "0"
target_type = "virtio"
}
video {
type = "virtio"
}
tpm {
backend_type = "emulator"
backend_version = "2.0"
}
network_interface {
network_name = "default"
}
}