Skip to content
Français
Conteneurs & Orchestration medium

Terraform: deploy an Incus three-tier stack (VPC, ACLs, load balancer)

15 min de lecture

Read this page in French

incus logo

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/incus provider 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:

  1. The operator (admin) creates the tenant (a project), sets the quotas and issues the user's credential. See Multi-tenant private cloud.
  2. 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

Fenêtre de terminal
tofu init
tofu plan
tofu apply
Plan: 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_dir plus default_remote); there is no incus_project on the tenant side.
  • Key resources: incus_network (type ovn), incus_network_acl (ingress lists), incus_instance (device blocks), incus_network_lb (backend and port blocks).
  • 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.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn