Files
homelab/docs/vm-provisioning-flow.md

183 lines
11 KiB
Markdown

# ADR: VM Provisioning Pipeline Flow
**Status:** Accepted
**Date:** February 2026
**Supersedes:** PRD v1.0 Section 3.2 (adds DHCP step, adjusts tool boundaries)
**Last Updated:** February 28, 2026 — revised flow for DHCP-before-boot, HA affinity via Proxmox API, secrets via n8n globals
## Context
The mk-labs homelab needs an automated VM provisioning pipeline triggered by a single operator action. The original PRD defined the flow; this ADR documents the adjusted architecture where n8n owns all orchestration including DHCP reservation (previously Ansible-rendered Terraform templates).
## Decision
### System Context
| Tool | Host | IP Address | Role in Pipeline |
|------|------|-----------|-----------------|
| NetBox | fire-station | 10.1.71.102 | Source of truth — VM records, IP allocation, VLAN data, webhook emitter |
| n8n | tiki-room | 10.1.71.23 | Event orchestration, payload validation, pipeline sequencing |
| Terraform | city-hall | 10.1.71.35 | Proxmox VM lifecycle + Unifi DHCP reservations (repo: /opt/git/homelab) |
| Ansible / Semaphore | imagineering | 10.1.71.22 | OS configuration, DNS record creation, NetBox status updates |
| Proxmox | fantasyland | 10.1.71.13 | Target hypervisor for all new VM deployments |
### Status State Machine
```
PLANNED →(operator)→ STAGED →(pipeline)→ ACTIVE
|
on error
FAILED
```
- **Planned**: All fields populated, operator not yet ready. No trigger.
- **Staged**: Operator validated and ready. Triggers pipeline.
- **Active**: Set by Ansible on successful OS config + DNS registration.
- **Failed**: Set by n8n or Ansible on any error. Manual investigation.
### Provisioning Flow (Revised)
| # | System | Action | Detail |
|---|--------|--------|--------|
| **1** | NetBox | Operator sets VM status to Staged | Triggers webhook to n8n on tiki-room |
| **2** | n8n | Receive & validate webhook payload | Check: hostname, IP, template, proxmox_node, proxmox_datastore all present. Status must be exactly Staged. On failure → set NetBox status to Failed, stop. |
| **3** | n8n → city-hall | SSH to city-hall, run `terraform apply` (proxmox/vm) | Creates VM clone on fantasyland in **stopped** state. Uses `-state=states/{hostname}.tfstate` for per-VM state isolation. Passes secrets from n8n Global Constants via `-var` flags. |
| **4** | n8n | Parse Terraform output for MAC address | Reads `mac_address` from `terraform output -json`. |
| **5** | n8n → NetBox | Write MAC address to VM interface | PATCH to NetBox API to update VM interface record with MAC. |
| **6** | n8n → city-hall | SSH to city-hall, run `terraform apply` (unifi/dhcp) | Creates static DHCP reservation on UDM Pro. Uses `-state=states/{hostname}.tfstate`. MAC and IP passed as variables. |
| **7** | n8n → Proxmox API | Add VM to HA resource + node affinity rule | POST to `/cluster/ha/resources` to register VM as HA resource. PUT to `/cluster/ha/rules/{prefer_node}` to add VM to the target node's affinity rule. |
| **8** | n8n → Proxmox API | Start VM | POST to `/nodes/{node}/qemu/{vmid}/status/start`. VM boots and receives IP via DHCP (reservation already in place). |
| **9** | n8n → imagineering | Call Semaphore API to trigger Ansible | Launches vm-provision playbook with VM details as extra vars. |
| **10** | Ansible | OS configuration & DNS record creation | Applies vm-baseline role: packages, SSH keys, Technitium DNS A record creation. |
| **11** | Ansible → NetBox | Set VM status to Active | Final step of playbook updates NetBox VM status via API. |
| **ERR** | n8n / Ansible | Any step fails | NetBox VM status set to Failed. Pipeline halts. No cleanup — operator investigates manually. |
### Key Design Decisions
**VM created in stopped state (Step 3)**
Cloud-init uses `ip=dhcp`. The DHCP reservation must exist on the UDM Pro before the VM's first boot, otherwise the VM gets a random DHCP lease instead of its assigned IP. The VM stays stopped until after the Unifi DHCP reservation is confirmed (Step 6).
**HA affinity via Proxmox API, not Terraform (Step 7)**
Proxmox 9.x replaced HA groups with HA node affinity rules. The bpg/proxmox Terraform provider does not yet support these (tracked in bpg/terraform-provider-proxmox#2097). n8n calls the Proxmox API directly to add the VM to the appropriate `prefer_{node}` affinity rule. This also handles VM migration from fantasyland (clone source) to the target node.
**Per-VM Terraform state files (Steps 3, 6)**
Each VM gets its own state file via `-state=states/{hostname}.tfstate` to avoid state conflicts when managing multiple VMs. The `states/` directory is in `.gitignore`.
**Secrets via n8n Global Constants**
All secrets (Proxmox API token, Unifi credentials, NetBox API token) are stored in n8n's encrypted credential store using the `n8n-nodes-globals` community node. Secrets are passed to Terraform via `-var` flags in SSH commands. No secrets are stored on city-hall's filesystem. See `docs/n8n-setup.md` for details.
### Tool Responsibility Boundary (Adjusted)
| Tool | Owns | Does Not Own |
|------|------|-------------|
| **Terraform** | Proxmox VM lifecycle (bpg/proxmox), UDM Pro DHCP reservations (unifi provider) | OS config, DNS records, application deployment, NetBox data, HA placement |
| **Ansible** | OS configuration, DNS A records (Technitium API), NetBox status updates | Infrastructure provisioning, DHCP reservations, event orchestration |
| **n8n** | Event orchestration, API glue, pipeline sequencing, HA affinity rule management, triggering all Terraform and Ansible steps, secrets distribution | Configuration management, infrastructure state |
| **NetBox** | Source of truth for all assets | Execution of any automation — it only holds data |
### Dependencies
| Component | Dependency | Purpose |
|-----------|-----------|---------|
| n8n | n8n-nodes-globals (community node) | Encrypted secrets storage for pipeline credentials |
| Terraform (proxmox/vm) | bpg/proxmox provider >= 0.74.0 | Proxmox VM lifecycle management |
| Terraform (unifi/dhcp) | paultyng/unifi provider >= 0.41.0 | UDM Pro DHCP static reservations |
| Proxmox | API token: terraform@pve!terraform-token | Terraform + n8n API access (requires VM.GuestAgent.Audit for Proxmox 9) |
| NetBox | Event Rule + Webhook (VM Staged Trigger) | Pipeline trigger on status change to Staged |
| NetBox | REST API token | n8n reads/writes VM data |
### Change from PRD v1.0
**What changed (original):** DHCP reservation moved from "existing Terraform/UDM Pro integration" (out of scope) to an explicit pipeline step owned by n8n.
**What changed (this revision):**
1. VM is created in **stopped** state. DHCP reservation is created before VM boots.
2. HA node affinity rule assignment added as an explicit step (Proxmox API, not Terraform).
3. VM start is a separate explicit step after HA rule assignment.
4. Secrets managed via n8n Global Constants instead of filesystem-based `.tfvars` files.
5. Per-VM Terraform state files via `-state` flag.
**Why:** Cloud-init uses `ip=dhcp`, so the DHCP reservation must exist before first boot. The bpg/proxmox Terraform provider doesn't support Proxmox 9 HA affinity rules, so n8n handles placement via API. Secrets in n8n's credential store keeps city-hall stateless.
### Git Repository Structure
```
homelab/
├── ansible/
│ ├── inventory/
│ │ ├── netbox.yml # NetBox dynamic inventory plugin
│ │ └── static.yml # Legacy static inventory
│ ├── playbooks/
│ │ ├── vm-provision.yml # Main playbook (Semaphore trigger)
│ │ ├── add_technitium_dns_entry.yml
│ │ ├── add_dhcp_reservation.yml
│ │ ├── update-linux-os.yml
│ │ └── set_hostname.yml
│ ├── roles/
│ │ ├── vm-baseline/ # NEW — packages, SSH, DNS, NetBox status
│ │ ├── dns-manager/ # Existing Technitium DNS role
│ │ ├── common/ # Base system config
│ │ ├── haproxy/
│ │ ├── n8n/
│ │ ├── observer/
│ │ └── time-sync/
│ ├── tasks/ # Shared task files
│ ├── group_vars/
│ ├── host_vars/
│ ├── templates/
│ ├── scripts/
│ ├── ansible.cfg
│ └── requirements.yml
├── terraform/
│ ├── proxmox/
│ │ └── vm/
│ │ ├── main.tf # bpg/proxmox VM resource (creates stopped)
│ │ ├── variables.tf # hostname, ip, template, proxmox_node
│ │ ├── outputs.tf # vm_id, mac_address
│ │ ├── providers.tf
│ │ ├── terraform.tfvars.example
│ │ └── states/ # Per-VM state files (.gitignore)
│ ├── unifi/
│ │ └── dhcp/
│ │ ├── main.tf # Unifi user resource (DHCP reservation)
│ │ ├── variables.tf # hostname, mac, ip
│ │ ├── providers.tf
│ │ ├── terraform.tfvars.example
│ │ └── states/ # Per-VM state files (.gitignore)
│ └── dns/
│ ├── dns.tf # Existing DNS terraform
│ └── provider.tf
├── packer/
│ ├── ubuntu-24.04/ # Ubuntu template build
│ └── fedora-42/ # Fedora template build
├── n8n/
│ └── workflows/
│ └── vm-provisioning.json # Exported n8n workflow
├── netbox/
│ └── initializers/
│ ├── custom_fields.yml
│ ├── vlans.yml
│ └── prefixes.yml
├── docs/
│ ├── decisions/
│ │ ├── vm-provisioning-flow.md # This document
│ │ └── tool-responsibility-matrix.md
│ └── n8n-setup.md # n8n dependencies and configuration
├── README.md
├── LICENSE
└── .gitignore
```
## Consequences
- **Positive:** Single visual workflow in n8n shows entire pipeline. Ansible simplified to pure configuration management. Terraform usage consistent (always invoked via n8n SSH, never rendered from templates). Secrets never stored on disk outside n8n's encrypted database. Per-VM state files prevent conflicts.
- **Negative:** n8n becomes a critical dependency — if tiki-room is down, no provisioning. Two separate `terraform apply` calls per VM (Proxmox + Unifi) instead of one. HA rule management is imperative API calls rather than declarative Terraform.
- **Mitigated by:** n8n workflow JSON committed to Git. Both Terraform modules can be run manually from city-hall if n8n is unavailable. HA rules can be managed via `ha-manager` CLI on any Proxmox node.