Incus, you say?
According to its purveyors, Incus is "next-generation system container, application container, and virtual machine manager." It is a fork of LXD which retains a more open-source friendly license.
Although Incus/LXD are typically associated with Linux containers, I was more interested in testing its virtual machine capabilities, so my resources are a bit more VM-centric.
This article assumes you already have a working Incus cluster.
Configure your Incus client
Before you can use the Terraform provider, you'll need to obtain and configure the Incus client. I'm using a Mac, so I followed the directions for "Other operating systems" found here.
After downloading the client, you can configure it via these instructions.
I'm using my own TLS certificates in my homelab, so there are a few extra steps.
Directory structure
First, let's see what the working directory for my incus client looks like:
$ tree ~/.config/incus
/Users/inflatador/.config/incus
├── client.ca
├── client.crt
├── client.key
├── config.yml
├── oidctokens
└── servercerts
└── example.crt
3 directories, 5 files
The files are follows:
-
servercerts/example.crt: Found in/var/lib/incus/server.crton the incus server. If this changes on the incus server and you try to login from your workstation without updating it, you'll get certificate errors. -
client.crt, client.key: Certificate/private key for mutual TLS authentication. The certificate must be added to the incus cluster's certificate trust, see this page for details.
config.yml
Here's my known-good incus client config file:
default-remote: example
remotes:
images:
addr: https://images.linuxcontainers.org
protocol: simplestreams
public: true
local:
addr: unix://
protocol: incus
public: false
example:
addr: https://incus.service.rl:443
auth_type: tls
project: default
protocol: incus
public: false
keepalive: 90
aliases: {}
defaults:
list_format: ""
console_type: ""
console_spice_command: ""
You can verify that this is working by running any incus command:
incus remote list
+------------------+------------------------------------+---------------+-------------+--------+--------+--------+
| NAME | URL | PROTOCOL | AUTH TYPE | PUBLIC | STATIC | GLOBAL |
+------------------+------------------------------------+---------------+-------------+--------+--------+--------+
| images | https://images.linuxcontainers.org | simplestreams | none | YES | NO | NO |
+------------------+------------------------------------+---------------+-------------+--------+--------+--------+
| local | unix:// | incus | file access | NO | YES | NO |
+------------------+------------------------------------+---------------+-------------+--------+--------+--------+
| example (current)| https://incus.service.rl:443 | incus | tls | NO | NO | NO |
+------------------+------------------------------------+---------------+-------------+--------+--------+--------+
Terraform config
Here's the relevant bits from provider.tf:
// You will need the Incus client on the host that runs terraform.
// Ref https://github.com/lxc/incus/blob/main/doc/installing.md
provider "incus" {
accept_remote_certificate = true
default_remote = "example"
remote {
name = "example"
address = "https://incus.service.rl:443"
authentication_type = "tls"
}
}
And terraform.tf:
terraform {
required_providers {
incus = {
source = "lxc/incus"
version = "1.0.1"
}
}
Creating virtual machine "flavors" via incus profiles
To get a cloud-like experience in Incus, I've defined an incus profile as follows:
resource "incus_profile" "example2-2" {
project = "default"
name = "example2-2"
description = "2 GB RAM, 2 vCPUs"
config = {
"boot.autostart" = true
"limits.cpu" = 2
"limits.memory" = "2GiB"
}
device {
name = "root"
type = "disk"
properties = {
path = "/"
pool = data.incus_storage_pool.lvm.name
size = "12GiB"
}
}
// all VMs created from this profile get a cloud-init config-drive
device {
name = "cidata"
type = "disk"
properties = {
source = "cloud-init:config"
}
}
// all VMs created from this profile are attached to the "lab98" VLAN
device {
name = var.nic_device
type = "nic"
properties = {
parent = "lab98"
nictype = "macvlan"
}
}
}
Then we create a VM based on this profile:
resource "incus_instance" "example100" {
project = "default"
profiles = ["example2-2"]
name = "example100"
image = "images:debian/13/cloud"
type = "virtual-machine"
config = {
"cloud-init.user-data" = templatefile("${path.module}/../shared/debian-user-data.yml.tpl",
{
hostname = "example100"
fqdn = "example100.host.rl"
})
"cloud-init.network-config" = templatefile("${path.module}/../shared/network-data.yml.tpl",
{ last_octet = "102"
nic_device = "enp5s0"
})
}
}
Note my choice of image: the "cloud" variant, which has cloud-init pre-installed. I'm also using terraform's templatefile function. to template the cloud-init
user-data and cloud-init network-config.
debian-user-data.yml.tpl:
#cloud-config
# Debian
hostname: ${hostname} # this will be replaced by the value "example100" above
manage_etc_hosts: True
...
network-data.yml.tpl:
version: 2
ethernets:
${nic_device}: # renders as "enp5s0"
addresses:
- 172.19.98.${last_octet}/24 # renders as "172.19.98.102"
A few minutes after a terraform apply, the new VM is available:
incus list
+------------+---------+------------------------+------+-----------------+-----------+--------------------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | LOCATION |
+------------+---------+------------------------+------+-----------------+-----------+-
| example100 | RUNNING | 172.19.98.102 (enp5s0) | | VIRTUAL-MACHINE | 0 | dreamcast.host.rl |
+------------+---------+------------------------+------+-----------------+-----------+--------------------+
Soon afterwards, cloud-init configures the network and enables my SSH access, so I can login and have a look at the new VM:
ssh 172.19.98.102
Linux example100 6.12.57+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1 (2025-11-05) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Dec 28 18:58:34 2025 from [...]
inflatador@example100:~$ hostnamectl
Static hostname: example100
Icon name: computer-vm
Chassis: vm 🖴
Machine ID: 38b857894fb644d9ae3d205ee66e6f63
Boot ID: 7f9406d7b9a64c5dad387226e86f7e32
AF_VSOCK CID: 3081916918
Virtualization: kvm
Operating System: Debian GNU/Linux 13 (trixie)
Kernel: Linux 6.12.57+deb13-amd64
Architecture: x86-64
Hardware Vendor: QEMU
Hardware Model: Standard PC _Q35 + ICH9, 2009_
Firmware Version: unknown
Firmware Date: Wed 2022-02-02
Firmware Age: 3y 10month 3w 4d
In Conclusion
Thanks to the Incus devs for a great product! The Terraform integrations make it easy to manage virtual machines in a cloud-like fashion.