Tenant Building #
**This describes the flow before the Deevnet API.** [ADR-0015](/docs/architecture/decisions/0015-tenant-onboarding-through-api/) (proposed) moves every per-tenant object behind the API: a tenant is admitted, then declares itself, its workloads and its names through the `deevnet/deevnet` provider, holding no Proxmox credential and no index. This page is how it works until that is deployed. The factory repository is now [`deevnet-tenant-fabric`](https://github.com/deevnet/deevnet-tenant-fabric) and keeps only the fabric; the tenant module remains available by its tags.
Defines the provisioning model for tenant workloads.
Purpose #
Tenant building provides:
- Declarative infrastructure β Define tenant environments as code
- Rebuild from scratch β A tenant is fully reconstituted from its own code against the substrate: overlay network, VMs, and DNS
- Reproducibility β Recreate tenant environments reliably
- Drift detection β Identify manual changes
- Lifecycle automation β Create, update, destroy via automation
Terraform-First Approach #
Unlike substrate infrastructure (automation-first), tenant workloads use Terraform:
| Aspect | Substrate (Automation) | Tenant (Terraform) |
|---|---|---|
| Change frequency | Rare, deliberate | Frequent, agile |
| State model | Procedural, idempotent | Declarative, stateful |
| Drift detection | Manual verification | Built-in plan/apply |
| Lifecycle | Configure existing | Create/destroy |
| Use case | Infrastructure config | VM provisioning |
Why Terraform for Tenants? #
- Declarative definitions β Define what should exist, not how to create it
- State tracking β Know exactly what’s deployed
- Plan before apply β Preview changes before execution
- Destroy support β Clean up tenant resources completely
- Proxmox provider β Native Terraform support for VM lifecycle
Tenant Provisioning Workflow #
1. Define Tenant Infrastructure #
A tenant is a single module instantiation. Every identifier it uses β its VRF, its VNets, its subnet β derives from one allocated index, so there is very little to declare:
module "tenant" {
# Consumed by tag from the factory, never by path: a tenant lives in its own
# repository. The tag is never moved, so this pins the module as precisely as
# a lock file pins providers.
source = "git::ssh://git@github.com/deevnet/deevnet-tenant-factory.git//modules/tenant?ref=tenant-module-v1.1.0"
tenant_name = "grooveiq" # <= 8 chars: Proxmox caps SDN zone IDs, and the zone ID is the name
tenant_index = 2 # issued by the API; everything else follows from it
# Issued by the substrate at onboarding, alongside the tenant's DNS key and
# its egress. A tenant never invents these, and no longer reads them out of
# the fabric's state - it cannot, from its own repository.
controller_id = var.controller_id
node = var.node
vm_count = 2
template_vm_id = var.template_vm_id
ssh_keys = var.ssh_keys
}
The module creates the tenant’s EVPN zone (its VRF), its VNet, its addressed subnet, and its VMs. Addresses are assigned from the subnet by cloud-init, not leased β Proxmox implements SDN DHCP for Simple zones only, and a tenant zone is EVPN.
Step-by-step instructions, including allocating the index and verifying the result, are in Provisioning a Tenant.
2. Plan Changes #
terraform plan
Review what will be created, modified, or destroyed.
3. Apply Changes #
terraform apply
Terraform creates or updates VMs on the tenant hypervisor.
4. Post-Provisioning #
After VMs are created:
- Ansible applies application-level configuration
- DNS records are created as part of the Terraform configuration
- Monitoring is configured
Template Requirements #
Tenant VMs clone from Proxmox templates:
| Requirement | Description |
|---|---|
| Cloud-init | Template must support cloud-init for initial config |
| SSH key injection | Automation user SSH key injected at boot |
| Network config | Address assigned from the tenant subnet via cloud-init |
| Base packages | Python3 for Ansible, basic utilities |
| OS disk | Small and growable; capacity for a workload comes from a separate data disk the tenant declares. See Substrate Storage |
Templates are built by the Image Factory and stored on the tenant hypervisor.
Data Disks #
A tenant VM’s data disk is part of the tenant’s own Terraform, alongside the VM it attaches to. That keeps the split visible in the tenant’s code: the substrate supplies an image with a small OS disk, and the tenant declares whatever capacity its workload actually needs. Anything worth keeping belongs on the data disk, never the OS disk, which is replaced whenever the image is rebuilt ( Substrate Storage).
One repository per tenant #
A tenant’s code lives in its own repository, deevnet-tenant-<name>, and that repository is the
tenant (
ADR-0006):
deevnet-tenant-grooveiq/
βββ main.tf the module instantiation above
βββ variables.tf
βββ terraform.tfvars tenant_name, tenant_index, ssh keys
βββ fabric.auto.tfvars issued by the substrate - not authored here
The substrate keeps the other half: the fabric, the module every tenant consumes, the index registry, and a reference implementation that new tenants are copied from and which cannot itself be applied.
What this separation buys is precise: a tenant’s recurring lifecycle touches no substrate repository. Adding a record, rebuilding, destroying β all happen here. Onboarding still touches substrate, because allocating an index and issuing a key, an egress and a fabric attachment are substrate acts; that division is ADR-0004 Β§5.
Distinction from Substrate Builder #
| Aspect | Substrate Builder | Tenant Building |
|---|---|---|
| Target | Substrate infrastructure | Tenant workloads |
| Tool | Automation | Terraform |
| Hypervisor | Management plane | Tenant hypervisor |
| Lifecycle | Long-lived, stable | Frequent create/destroy |
| Authority | Platform admins | May delegate to tenant admins |
The substrate Builder provisions the infrastructure that tenant building runs on top of.
MAC Address Policy #
For tenant VMs, MAC addresses may be:
| Policy | When Used |
|---|---|
| Auto-generated | Default for ephemeral/test VMs |
| Deterministic | When stable identity is required |
If deterministic MACs are needed:
- Define MAC in Terraform configuration
- Store in tenant inventory
- Record the address in the tenant’s own code (assigned, not leased)
This is optional for tenants, unlike management-plane VMs where deterministic MACs are mandatory.
Network Prerequisites #
Under the tenant fabric model, a tenant owns its own network and creates it as part of its own code β a virtual overlay in the fabric with its own subnet and anycast gateway. Configuring a per-tenant VLAN on the core router is not a build step.
What the substrate must provide first (the substrate side of the tenant contract):
- Tenant fabric present β the tenant hypervisor runs the overlay fabric the tenant attaches to
- Perimeter transit β the core router provides the transit boundary for tenant egress and shared-service access
- DNS zone β the substrate zone the tenant publishes its records into
The tenant’s overlay network, addressing, and DNS records are all created by the tenant’s own Terraform β not as substrate prerequisites. See ADR-0001.
State Management #
Terraform State #
The substrate offers a state store; a tenant may use it or keep state local and carry its own custody. The decision, and why the opt-out is load-bearing rather than a courtesy, is ADR-0007.
Whichever a tenant picks, two rules hold:
- State is never edited by hand.
- State must not come to contain a secret. Prefer resources whose values can be re-derived over ones that generate a credential, because a generated credential lives in state permanently. State is not IaC source and is not protected the way source is β see Secure Identity Β§4.4.
Losing state costs a rebuild, not a loss: nothing in a conforming tenant’s state is irreplaceable, which is the same property that makes rebuilt from code, not from a backup true.
Drift Handling #
If manual changes are detected:
- Run
terraform planto identify drift - Either:
- Update Terraform config to match reality
- Or run
terraform applyto enforce desired state
Summary #
- Tenant workloads use Terraform (not Ansible) for provisioning
- Declarative definitions enable reproducibility and drift detection
- VMs clone from cloud-init enabled templates
- Tenant inventory is separate from substrate inventory
- MAC addresses may be auto-generated or deterministic (optional)
- Substrate builder provides the infrastructure that tenants run on