Based on this document and others.

Add your user to the libvirt group

sudo usermod -aG libvirt [ username ]

Create project

mkdir -p ~/terraform/kvm/ubuntu/{config,images,templates}
cd ~/terraform/kvm/ubuntu

Create terraform main.tf

cat <<EOF > main.tf
terraform {
  required_providers {
    libvirt = {
      source = "dmacvicar/libvirt"
    }
  }
}

terraform {
required_version = ">= 0.12"
}
EOF

Create terraform outputs.tf

cat <<EOF > outputs.tf
output "ips" {
  # show IP, run 'terraform refresh' if not populated
  value = libvirt_domain.domain.*.network_interface.0.addresses
}
EOF

Create terraform vars.tf

cat <<EOF > vars.tf
variable "hostname" {
  type    = list(string)
  default = [ "hostname0", "hostname1", "hostname2", "hostname3", "hostname4", "hostname5", "hostname6", "hostname7" ]
}
variable "bridge" {
  type = string
  default = name_of_your_bridge
}
variable "interface" {
  type = string
  default = "ens3"
}
variable "domain" {
  default = "local"
}
variable "memoryMB" {
  default = 1024*2
}
variable "cpu" {
  default = 2
}
variable "ips" {
  type = list
  default = ["192.168.0.60", "192.168.0.61", "192.168.0.62", "192.168.0.63", "192.168.0.64", "192.168.0.65", "192.168.0.66", "192.168.0.67"]
}
EOF

Create terraform create_vms.tf

cat <<EOF > create_vms.tf
# instance the provider
provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_volume" "os_image" {
  count = length(var.hostname)
  name = "${var.hostname[count.index]}.qcow2"
  pool = "default"
  #source = "https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img"
  #source = "https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64-disk-kvm.img"
  source = "images/ubuntu-20.04-server-cloudimg-amd64.img"
  format = "qcow2"
}

# Use CloudInit ISO to add ssh-key to the instance
resource "libvirt_cloudinit_disk" "commoninit" {
  count = length(var.hostname)
  name = "${var.hostname[count.index]}-commoninit.iso"
  user_data = data.template_file.user_data[count.index].rendered
  network_config = templatefile("${path.module}/templates/network_config.tpl", {
     interface = var.interface
     ip_addr   = var.ips[count.index]
  })
}

data "template_file" "user_data" {
  count = length(var.hostname)
  template = file("${path.module}/config/cloud_init.yml")
  vars = {
    hostname = element(var.hostname, count.index)
    fqdn = "${var.hostname[count.index]}.${var.domain}"
  }
}

# Create the machine
resource "libvirt_domain" "domain" {
  count = length(var.hostname)
  name = "${var.hostname[count.index]}"
  memory = var.memoryMB
  vcpu = var.cpu
  disk {
    volume_id = element(libvirt_volume.os_image.*.id, count.index)
  }

  network_interface {
    addresses = [var.ips[count.index]]
    bridge = var.bridge
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[count.index].id

  # IMPORTANT
  # Ubuntu can hang is a isa-serial is not present at boot time.
  # If you find your CPU 100% and never is available this is why
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  graphics {
    type = "spice"
    listen_type = "address"
    autoport = "true"
  }
EOF

Create the cloud_init config

  • https://cloudinit.readthedocs.io/en/latest/topics/examples.html
cat <<EOF > config/cloud_init.yml
ssh_pwauth: true
disable_root: false
chpasswd:
  list: |
     root:[ root_password ]
  expire: false
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    home: /home/ubuntu
    shell: /bin/bash
    lock_passwd: false
    ssh-authorized-keys:
      - [ ssh_pub_key ]
EOF

Create the network template

cat <<EOF > templates/network_config.tpl
ethernets:
    ${interface}:
        addresses:
        - ${ip_addr}/24
        dhcp4: false
        gateway4: [ gateway_ip ]
        nameservers:
            addresses:
            - [ dns_ip_0 ]
            - [ dns_ip_1 ]
version: 2
EOF

Download the image file unless downloading from internet

curl -O --output-dir images/ https://cloud-images.ubuntu.com/releases/focal/release-20231011/ubuntu-20.04-server-cloudimg-amd64.img

Terraform init, validate, plan, apply

terraform init
terraform validate
terraform plan
terraform apply

or auto approve

terraform apply -auto-approve

Troubleshooting

Error: failed to connect: authentication unavailable: no polkit agent available to authenticate action 'org.libvirt.unix.manage'

  • Add your user to the libvirt group
sudo usermod -aG libvirt [ username ]