
Provisioning instances with the lxc/incus provider is the starting point. This guide goes further: describing an entire three-tier private cloud as Infrastructure as Code. An OVN VPC, Security Groups, instances and a load balancer, in a single declarative file. Above all, it is deployed with the tenant's account, not the administrator's, the way a real cloud works. Tested with OpenTofu. Advanced audience.
What you will learn
- The two-tier model: the operator provisions, the tenant deploys.
- Configure the provider for the tenant's account.
- Describe an OVN VPC, ACLs, instances and a load balancer in HCL.
- Apply and verify.
Prerequisites
- Familiarity with the
lxc/incusprovider basics. - An OVN network and an uplink: see OVN in Incus.
- OpenTofu or Terraform installed.
The two-tier model
A cloud has two distinct roles, and therefore two Terraform configurations:
- The operator (admin) creates the tenant (a project), sets the quotas and issues the user's credential. See Multi-tenant private cloud.
- The tenant deploys their own infrastructure (VPC, instances, load balancer) with their own credential. They cannot create their project: that is an admin operation.
This guide covers the second tier: the plan the tenant applies.
The provider, on the tenant side
You point the provider at the tenant's config (their scoped certificate) and their remote:
terraform { required_providers { incus = { source = "lxc/incus" } }}
provider "incus" { config_dir = "/home/alice/.config/incus" # tenant config (scoped cert) default_remote = "mycloud"}
locals { project = "tenant-a"}Describing the OVN VPC
You pin the subnet to get deterministic IP addresses, which the load balancer needs:
resource "incus_network" "vpc" { name = "tf-vpc" type = "ovn" project = local.project config = { "network" = "UPLINK" "ipv4.address" = "10.100.0.1/24" "ipv4.nat" = "true" }}Describing the Security Groups
ACLs are declared with ingress and egress lists. A rule references another ACL by name, and you add an explicit drop that writes the intent into the configuration instead of leaving it in an implicit default (see Network ACLs):
resource "incus_network_acl" "db" { name = "tf-db" project = local.project ingress = [ { action = "allow" protocol = "tcp" source = "tf-app" destination_port = "5432" state = "enabled" }, { action = "drop" # explicit: the web tier never reaches the db source = "tf-web" state = "enabled" }, ]}Describing the instances
Each instance carries its network device (VPC, IP and ACL) and its disk (a Ceph pool). The count meta-argument produces the two web instances:
resource "incus_instance" "web" { count = 2 name = "tf-web${count.index + 1}" project = local.project image = "images:debian/13/cloud"
device { name = "eth0" type = "nic" properties = { network = incus_network.vpc.name "ipv4.address" = "10.100.0.1${count.index + 1}" "security.acls" = "tf-web" "security.acls.default.ingress.action" = "drop" "security.acls.default.egress.action" = "allow" } } device { name = "root" type = "disk" properties = { path = "/", pool = "ceph-rbd" } }}Describing the load balancer
The incus_network_lb resource carries backends and ports as blocks:
resource "incus_network_lb" "web" { network = incus_network.vpc.name project = local.project listen_address = "192.168.10.221"
config = { "healthcheck" = "true" "healthcheck.interval" = "10" }
backend { name = "web1" target_address = "10.100.0.11" target_port = "80" } backend { name = "web2" target_address = "10.100.0.12" target_port = "80" } port { protocol = "tcp" listen_port = "80" target_backend = ["web1", "web2"] }}Applying and verifying
tofu inittofu plantofu applyPlan: 9 to add, 0 to change, 0 to destroy....Apply complete! Resources: 9 added.The tenant has deployed their VPC, their ACLs, their instances and their load balancer, with their own account, confined to their project. A curl on the load balancer address answers, spread across the two web instances. The whole stack is now reproducible: tofu destroy tears it down, tofu apply rebuilds it identically.
Key points
- A cloud is described in two tiers: the operator provisions the tenant, the tenant deploys their infrastructure.
- The provider points at the tenant's config (
config_dirplusdefault_remote); there is noincus_projecton the tenant side. - Key resources:
incus_network(typeovn),incus_network_acl(ingresslists),incus_instance(deviceblocks),incus_network_lb(backendandportblocks). - Prefer an explicit drop in your ACLs: it reads back during an audit and survives a change to
default.ingress.action. - The result is reproducible: the three-tier stack can be torn down and rebuilt at will.