Building

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:

AspectSubstrate (Automation)Tenant (Terraform)
Change frequencyRare, deliberateFrequent, agile
State modelProcedural, idempotentDeclarative, stateful
Drift detectionManual verificationBuilt-in plan/apply
LifecycleConfigure existingCreate/destroy
Use caseInfrastructure configVM provisioning

Why Terraform for Tenants? #

  1. Declarative definitions β€” Define what should exist, not how to create it
  2. State tracking β€” Know exactly what’s deployed
  3. Plan before apply β€” Preview changes before execution
  4. Destroy support β€” Clean up tenant resources completely
  5. 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:

RequirementDescription
Cloud-initTemplate must support cloud-init for initial config
SSH key injectionAutomation user SSH key injected at boot
Network configAddress assigned from the tenant subnet via cloud-init
Base packagesPython3 for Ansible, basic utilities
OS diskSmall 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 #

AspectSubstrate BuilderTenant Building
TargetSubstrate infrastructureTenant workloads
ToolAutomationTerraform
HypervisorManagement planeTenant hypervisor
LifecycleLong-lived, stableFrequent create/destroy
AuthorityPlatform adminsMay 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:

PolicyWhen Used
Auto-generatedDefault for ephemeral/test VMs
DeterministicWhen 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):

  1. Tenant fabric present β€” the tenant hypervisor runs the overlay fabric the tenant attaches to
  2. Perimeter transit β€” the core router provides the transit boundary for tenant egress and shared-service access
  3. 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:

  1. Run terraform plan to identify drift
  2. Either:
    • Update Terraform config to match reality
    • Or run terraform apply to enforce desired state

Summary #

  1. Tenant workloads use Terraform (not Ansible) for provisioning
  2. Declarative definitions enable reproducibility and drift detection
  3. VMs clone from cloud-init enabled templates
  4. Tenant inventory is separate from substrate inventory
  5. MAC addresses may be auto-generated or deterministic (optional)
  6. Substrate builder provides the infrastructure that tenants run on
Page last modified: September 17, 2026